use crate::{Result, RuntimeError, ToolRegistry};
use futures::stream::Stream;
use reqwest::Client;
use serde_json::{json, Value};
use std::path::{Path, PathBuf};
use std::pin::Pin;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Duration;
use tokio::sync::{mpsc, RwLock};
use tokio_util::sync::CancellationToken;
mod anthropic_quota;
mod api;
mod api_sync;
pub mod attachments;
mod auth;
mod axel_context;
#[cfg(test)]
mod body_golden;
pub mod budget;
mod cache_diagnosis;
pub mod capture_worker;
pub mod chat_capture;
pub(crate) mod cloud_invoke;
pub mod compaction;
pub mod context;
pub mod continuation;
pub mod google_gemini;
pub mod google_vertex;
pub(crate) mod helpers;
mod http_phases;
mod image_limits;
mod image_retention;
pub mod keepwarm;
pub mod memory_context;
pub mod memory_history;
pub mod openai;
pub mod relay;
mod request;
mod sse;
mod sse_types;
mod stream;
pub mod subagent;
pub mod subagent_watch;
pub mod telemetry;
pub mod trace;
pub(crate) mod transport;
mod types;
use api::ApiMethods;
use auth::AuthMethods;
use helpers::HelperMethods;
use stream::StreamMethods;
use types::AuthState;
pub use types::{AgentEvent, CancelCause, LlmEvent, SessionEvent, StreamEvent, TurnCompletion};
pub use stream::activation_policy;
pub enum BeforeToolCallDecision {
Continue { input: Value },
Block { reason: String },
}
pub async fn emit_before_tool_call(
hook_bus: &Arc<crate::extensions::hooks::HookBus>,
tool_name: &str,
runtime_tool_name: Option<&str>,
input: Value,
session_id: Option<&str>,
) -> crate::extensions::hooks::events::HookResult {
let mut event = crate::extensions::hooks::events::HookEvent::before_tool_call(tool_name, input)
.with_session(session_id);
if let Some(runtime_tool_name) = runtime_tool_name {
event.tool_runtime_name = Some(runtime_tool_name.to_string());
}
hook_bus.emit(&event).await
}
pub use crate::tools::CONFIRM_ANSWER_ALLOW_ALL;
const MAX_CONFIRM_MESSAGE_CHARS: usize = 1200;
const MAX_CONFIRM_MESSAGE_LINES: usize = 12;
fn cap_confirm_message(message: &str) -> String {
let message = redact_secret_text(&crate::session::sanitize_prompt_text(message));
let lines: Vec<&str> = message.lines().collect();
let over_lines = lines.len() > MAX_CONFIRM_MESSAGE_LINES;
let over_chars = message.chars().count() > MAX_CONFIRM_MESSAGE_CHARS;
if !over_lines && !over_chars {
return message;
}
let kept = lines[..lines.len().min(MAX_CONFIRM_MESSAGE_LINES)].join("\n");
let kept: String = kept.chars().take(MAX_CONFIRM_MESSAGE_CHARS).collect();
format!("{kept}\n…[message truncated by host]")
}
const MAX_REWRITE_PREVIEW_LINES: usize = 6;
const MAX_REWRITE_PREVIEW_CHARS: usize = 400;
fn redact_secret_text(text: &str) -> String {
crate::runtime::trace::export::redact_command_text(text)
}
fn redact_preview_value(value: &mut Value) {
crate::runtime::trace::export::redact_value(value);
match value {
Value::Object(map) => map.values_mut().for_each(redact_preview_value),
Value::Array(items) => items.iter_mut().for_each(redact_preview_value),
Value::String(s) => *s = redact_secret_text(s),
_ => {}
}
}
fn final_input_preview(input: &Value) -> String {
let mut input = input.clone();
redact_preview_value(&mut input);
let full = match input.get("command").and_then(Value::as_str) {
Some(cmd) => cmd.to_string(),
None => serde_json::to_string_pretty(&input).unwrap_or_else(|_| input.to_string()),
};
let lines: Vec<&str> = full.lines().collect();
let mut out: String = lines[..lines.len().min(MAX_REWRITE_PREVIEW_LINES)]
.iter()
.map(|l| format!(" {l}"))
.collect::<Vec<_>>()
.join("\n");
let mut cut = lines.len() > MAX_REWRITE_PREVIEW_LINES;
if out.chars().count() > MAX_REWRITE_PREVIEW_CHARS {
out = out.chars().take(MAX_REWRITE_PREVIEW_CHARS).collect();
cut = true;
}
if cut {
out.push_str("\n …[preview truncated]");
}
out
}
fn confirm_prompt_body(
message: &str,
sources: &[String],
tool: Option<&str>,
rewrite: Option<(&[String], &Value)>,
) -> String {
let source = if sources.is_empty() {
"(unknown)".to_string()
} else {
sources.join(", ")
};
let rewritten = match rewrite {
Some((by, input)) => format!(
"Input rewritten by: {}\nInput that will run:\n{}\n",
if by.is_empty() { "(unknown)".to_string() } else { by.join(", ") },
final_input_preview(input)
),
None => String::new(),
};
crate::session::sanitize_prompt_text(&format!(
"Extension: {source}\nTool: {}\n{rewritten}\nMessage from {source}:\n{}\n\nAllow this tool call?",
tool.unwrap_or("(unknown)"),
cap_confirm_message(message)
))
}
fn approved(
modified_input: Option<Value>,
) -> crate::extensions::hooks::events::HookResult {
match modified_input {
Some(input) => crate::extensions::hooks::events::HookResult::Modify { input },
None => crate::extensions::hooks::events::HookResult::Continue,
}
}
pub async fn resolve_before_tool_call_result(
hook_result: crate::extensions::hooks::events::HookResult,
secret_prompt: Option<&crate::tools::SecretPromptHandle>,
auto_approve_confirms: bool,
session_allow_all: Option<&crate::tools::SessionConfirmLatch>,
) -> crate::extensions::hooks::events::HookResult {
match hook_result {
crate::extensions::hooks::events::HookResult::Confirm {
message,
sources,
generations,
tool,
modified_input,
modified_by,
} => {
let keys = crate::tools::latch_keys(&sources, &generations);
if auto_approve_confirms {
tracing::info!(message = %message, "confirm auto-approved (auto_approve_confirms=true)");
return approved(modified_input);
}
if session_allow_all.is_some_and(|latch| latch.allows_all(&keys)) {
tracing::info!(
message = %message,
extensions = ?sources,
"confirm auto-approved (extension allow-all latched)"
);
return approved(modified_input);
}
let Some(prompt) = secret_prompt else {
return crate::extensions::hooks::events::HookResult::Block {
reason: format!(
"Tool call requires confirmation but no interactive prompt is available: {}",
redact_secret_text(&crate::session::sanitize_prompt_text(&message))
),
};
};
let scope = (session_allow_all.is_some() && !sources.is_empty()).then(|| {
crate::session::ConfirmScope {
extensions: sources.clone(),
tool: tool.clone(),
rewritten_by: if modified_input.is_some() {
modified_by.clone()
} else {
Vec::new()
},
}
});
let response = prompt
.prompt_scoped(
crate::session::CONFIRM_PROMPT_TITLE.to_string(),
confirm_prompt_body(
&message,
&sources,
tool.as_deref(),
modified_input.as_ref().map(|i| (modified_by.as_slice(), i)),
),
scope,
)
.await;
match response.as_deref().map(str::trim) {
Some(answer)
if answer.eq_ignore_ascii_case("yes") || answer.eq_ignore_ascii_case("y") =>
{
approved(modified_input)
}
Some(answer) if answer.eq_ignore_ascii_case(CONFIRM_ANSWER_ALLOW_ALL) => {
match session_allow_all {
Some(latch) if latch.allow(&keys) => tracing::warn!(
extensions = ?sources,
"user chose Allow all: confirms from these extensions are auto-approved for the rest of this conversation"
),
Some(_) => tracing::warn!(
"user chose Allow all on an unattributed or revoked confirm; approving this call only"
),
None => tracing::warn!(
"user chose Allow all but this path has no session latch; approving this call only"
),
}
approved(modified_input)
}
_ => crate::extensions::hooks::events::HookResult::Block {
reason: format!(
"Tool call confirmation denied: {}",
redact_secret_text(&crate::session::sanitize_prompt_text(&message))
),
},
}
}
other => other,
}
}
pub async fn resolve_before_tool_call_decision(
original_input: Value,
hook_result: crate::extensions::hooks::events::HookResult,
secret_prompt: Option<&crate::tools::SecretPromptHandle>,
auto_approve_confirms: bool,
session_allow_all: Option<&crate::tools::SessionConfirmLatch>,
) -> BeforeToolCallDecision {
match resolve_before_tool_call_result(
hook_result,
secret_prompt,
auto_approve_confirms,
session_allow_all,
)
.await
{
crate::extensions::hooks::events::HookResult::Block { reason } => {
BeforeToolCallDecision::Block { reason }
}
crate::extensions::hooks::events::HookResult::Modify { input } => {
BeforeToolCallDecision::Continue { input }
}
_ => BeforeToolCallDecision::Continue {
input: original_input,
},
}
}
const MAX_REPLACE_OUTPUT: usize = 1024 * 1024;
pub async fn emit_after_tool_call(
hook_bus: &Arc<crate::extensions::hooks::HookBus>,
tool_name: &str,
runtime_tool_name: Option<&str>,
input: Value,
output: String,
max_tool_output: usize,
session_id: Option<&str>,
) -> String {
emit_after_tool_call_outcome(
hook_bus,
tool_name,
runtime_tool_name,
input,
output,
max_tool_output,
session_id,
)
.await
.output
}
pub(super) struct AfterToolCallOutcome {
pub output: String,
pub replaced: bool,
}
pub(super) async fn emit_after_tool_call_outcome(
hook_bus: &Arc<crate::extensions::hooks::HookBus>,
tool_name: &str,
runtime_tool_name: Option<&str>,
input: Value,
output: String,
max_tool_output: usize,
session_id: Option<&str>,
) -> AfterToolCallOutcome {
use crate::extensions::hooks::events::HookResult;
let original = output.clone();
let mut event =
crate::extensions::hooks::events::HookEvent::after_tool_call(tool_name, input, output)
.with_session(session_id);
if let Some(runtime_tool_name) = runtime_tool_name {
event.tool_runtime_name = Some(runtime_tool_name.to_string());
}
let mut replaced = false;
let post_hook = match hook_bus.emit(&event).await {
HookResult::Replace { mut output } => {
replaced = true;
if output.len() > MAX_REPLACE_OUTPUT {
tracing::warn!(
tool = %tool_name,
len = output.len(),
cap = MAX_REPLACE_OUTPUT,
"Extension Replace output exceeds cap — clamping",
);
let mut boundary = MAX_REPLACE_OUTPUT;
while boundary > 0 && !output.is_char_boundary(boundary) {
boundary -= 1;
}
output.truncate(boundary);
}
output
}
HookResult::Continue => original,
other => {
tracing::warn!(
tool = %tool_name,
?other,
"Unexpected after_tool_call result — using original output",
);
original
}
};
AfterToolCallOutcome {
output: crate::runtime::helpers::HelperMethods::truncate_tool_result(
&post_hook,
max_tool_output,
),
replaced,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ReasoningClamp {
pub from: agent_core::reasoning::ReasoningLevel,
pub to: agent_core::reasoning::ReasoningLevel,
}
fn validated_single_tool_output(
model: &str,
output: crate::ToolOutput,
) -> (String, Option<Vec<Value>>) {
let (summary, blocks) = output.into_parts();
if let Some(ref blocks) = blocks {
if let Err(error) = attachments::validate_tool_blocks(model, blocks) {
return (format!("Attachment not sent: {error}"), None);
}
}
(summary, blocks)
}
fn retain_single_tool_blocks(
blocks: Option<Vec<Value>>,
original: &str,
hooked: &str,
) -> Option<Vec<Value>> {
if original == hooked {
blocks
} else {
None
}
}
fn single_tool_result_content(
result: &str,
blocks: Option<Vec<Value>>,
max_tool_output: usize,
) -> Value {
match blocks {
Some(blocks) => Value::Array(blocks),
None => Value::String(HelperMethods::truncate_tool_result(result, max_tool_output)),
}
}
#[derive(Clone)]
struct PromptReloadSource {
manifest: PathBuf,
context: agent_core::prompt::SelectionContext,
user_module: Option<agent_core::prompt::PromptModule>,
delegation_policy_digest: Option<String>,
}
pub struct Runtime {
client: Client,
anthropic_stream_client: Arc<std::sync::OnceLock<Client>>,
auth: Arc<RwLock<AuthState>>,
model: String,
tools: Arc<RwLock<ToolRegistry>>,
system_prompt: Option<String>,
effective_prompt: Option<agent_core::prompt::PromptStack>,
prompt_generation: u64,
prompt_reload_source: Option<PromptReloadSource>,
thinking_budget: u32,
named_level: Option<agent_core::reasoning::ReasoningLevel>,
explicit_reasoning: bool,
codex_request_role: crate::runtime::openai::catalog::CodexRequestRole,
context_window_override: Option<u64>,
pub(crate) continuation: continuation::SharedContinuation,
compaction_model: Option<String>,
compaction_mode: agent_core::compaction::CompactionMode,
compaction_exclusions: Vec<agent_core::compaction::ContentClass>,
remote_summarization_attempts: Arc<std::sync::atomic::AtomicU64>,
subagent_registry: Arc<Mutex<crate::runtime::subagent::SubagentRegistry>>,
orchestration: Option<Arc<crate::orchestration::OrchestrationRuntime>>,
event_queue: Arc<crate::events::EventQueue>,
pub watcher_exit_path: Option<PathBuf>,
max_tool_output: usize,
bash_timeout: u64,
bash_max_timeout: u64,
subagent_timeout: u64,
api_retries: u32,
refusal_retries: u32,
telemetry_level: crate::runtime::telemetry::TelemetryLevel,
telemetry_writer: Option<crate::runtime::telemetry::TelemetryWriter>,
trace_ctx: trace::TraceContext,
trace_controls: std::sync::Arc<trace::TraceControls>,
memory_context_state: std::sync::Arc<std::sync::Mutex<memory_context::SessionMemoryState>>,
memory_backend: crate::memory_backend::MemoryBinding,
memory_backend_config: Option<crate::config::MemoryBackendConfig>,
memory_backend_reconfigure_denied: Arc<std::sync::atomic::AtomicBool>,
#[cfg(any(test, feature = "testing"))]
capture_provider_for_test: std::sync::Arc<
std::sync::Mutex<Option<std::sync::Arc<dyn capture_worker::CaptureProvider>>>,
>,
pending_history_import_preview:
std::sync::Arc<std::sync::Mutex<Option<memory_history::HistoryImportPreview>>>,
history_import_plan: std::sync::Arc<std::sync::Mutex<Option<memory_history::ImportPlan>>>,
pending_memory_segment:
std::sync::Arc<std::sync::Mutex<Option<memory_context::ContextSegment>>>,
retained_recall_turn:
std::sync::Arc<std::sync::Mutex<Option<memory_context::RetainedRecallTurn>>>,
one_shot_trace_writer:
std::sync::Arc<std::sync::Mutex<Option<crate::runtime::telemetry::TelemetryWriter>>>,
capture_dir: std::path::PathBuf,
cache_diagnostics: bool,
cache_ttl: crate::core::config::CacheTtl,
ttl_downgrade_notified: std::sync::Arc<std::sync::atomic::AtomicBool>,
saw_1h_honored: std::sync::Arc<std::sync::atomic::AtomicBool>,
cache_anchor: std::sync::Arc<keepwarm::CacheAnchorCell>,
last_msg_id: Arc<Mutex<Option<String>>>,
cache_diag_rejected: Arc<std::sync::atomic::AtomicBool>,
session_manager: std::sync::Arc<crate::tools::shell::SessionManager>,
hook_bus: Arc<crate::extensions::hooks::HookBus>,
#[allow(dead_code)]
reaper_handle: Option<tokio::task::JoinHandle<()>>,
#[allow(dead_code)]
reaper_cancel: Option<tokio_util::sync::CancellationToken>,
credential_source: crate::auth::CredentialSource,
token_cache: crate::auth::TokenCache,
trusted_worker_models: Vec<agent_core::prompt::QualifiedModelId>,
subagent_limits: crate::config::SubagentLimits,
progressive_tool_disclosure: bool,
deferred_tool_loading: bool,
activation_confirm: agent_core::config::ActivationConfirm,
delegation_parent: Option<String>,
session_allow_all: std::sync::Arc<crate::tools::SessionConfirmLatch>,
mcp_runtime: Option<std::sync::Arc<crate::mcp::McpRuntimeManager>>,
mcp_session_scope: Option<std::sync::Arc<crate::mcp::McpSessionEndGuard>>,
extension_runtime: Option<std::sync::Arc<crate::extensions::lease::ExtensionRuntimeManager>>,
extension_session_scope:
Option<std::sync::Arc<crate::extensions::lease::ExtensionSessionEndGuard>>,
turn_budget: crate::runtime::budget::TurnBudget,
owner_steer_budget: Option<Arc<crate::runtime::budget::OwnerSteerBudget>>,
host_tool_session: crate::tools::activation::SessionId,
tool_catalog_prompt: Arc<std::sync::Mutex<Option<Option<String>>>>,
retained_tool_set: crate::tools::activation::RetainedSessionToolSet,
pending_tool_restore: crate::tools::activation::PendingToolRestore,
session_id: Option<String>,
cwd: Option<PathBuf>,
env: Option<crate::session::types::SessionEnv>,
env_stripped: Vec<String>,
env_warned: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
}
fn fresh_host_tool_session() -> crate::tools::activation::SessionId {
crate::tools::activation::SessionId::parse(&format!(
"runtime-{}-{}",
std::process::id(),
uuid::Uuid::new_v4()
))
.expect("generated runtime session id is always valid")
}
fn fresh_memory_context_state(
) -> std::sync::Arc<std::sync::Mutex<memory_context::SessionMemoryState>> {
let session = memory_context::SessionId::parse(&format!(
"memctx-{}-{}",
std::process::id(),
uuid::Uuid::new_v4()
))
.expect("generated memory session id is always valid");
std::sync::Arc::new(std::sync::Mutex::new(
memory_context::SessionMemoryState::new(session),
))
}
fn memory_project_scope() -> std::result::Result<agent_core::memory::store::ProjectScope, String> {
let cwd = std::env::current_dir()
.map_err(|error| format!("cannot resolve current project directory: {error}"))?;
agent_core::memory::store::ProjectScope::discover(&cwd)
.map_err(|error| format!("cannot resolve project scope: {error}"))
}
fn memory_project_id() -> memory_context::ProjectId {
match memory_project_scope() {
Ok(scope) => memory_context::ProjectId::parse(scope.key())
.expect("host project scope key is always a valid identifier"),
Err(_) => memory_context::ProjectId::parse("project-unresolved")
.expect("static fallback project id is valid"),
}
}
fn memory_capture_worker() -> &'static capture_worker::CaptureWorker {
static WORKER: std::sync::OnceLock<capture_worker::CaptureWorker> = std::sync::OnceLock::new();
WORKER.get_or_init(|| capture_worker::CaptureWorker::new(32))
}
struct ExtensionCaptureProvider {
manager: std::sync::Arc<crate::extensions::lease::ExtensionRuntimeManager>,
session: crate::tools::activation::SessionId,
plugin: String,
digest: crate::tools::catalog::SchemaDigest,
handle: tokio::runtime::Handle,
}
impl capture_worker::CaptureProvider for ExtensionCaptureProvider {
fn capture(
&self,
capture: chat_capture::ChatTurnCapture,
) -> std::result::Result<(), capture_worker::CaptureFailure> {
let params = memory_context::capture_request_wire(&capture);
self.handle
.block_on(
crate::extensions::lease::ExtensionLeaseCapability::new(
self.session.clone(),
self.manager.clone(),
)
.call_exact(
&self.plugin,
memory_context::MEMORY_CAPTURE_TOOL_NAME,
&self.digest,
params,
),
)
.map(|_| ())
.map_err(|_| capture_worker::CaptureFailure {
code: "provider_call_failed",
})
}
fn contains_capture(
&self,
capture_id: &[u8; 32],
) -> std::result::Result<capture_worker::CaptureCommitState, capture_worker::CaptureFailure>
{
let capture_id = chat_capture::CaptureId::from_bytes(*capture_id);
let response = self
.handle
.block_on(
crate::extensions::lease::ExtensionLeaseCapability::new(
self.session.clone(),
self.manager.clone(),
)
.call_exact(
&self.plugin,
memory_context::MEMORY_CAPTURE_TOOL_NAME,
&self.digest,
memory_context::capture_query_wire(&capture_id),
),
)
.map_err(|_| capture_worker::CaptureFailure {
code: "provider_query_failed",
})?;
Ok(
if response
.get("committed")
.and_then(serde_json::Value::as_bool)
== Some(true)
{
capture_worker::CaptureCommitState::Committed
} else {
capture_worker::CaptureCommitState::Absent
},
)
}
fn capture_summary(
&self,
capture: chat_capture::ConversationSummaryCapture,
) -> std::result::Result<(), capture_worker::CaptureFailure> {
let params = memory_context::summary_capture_request_wire(&capture);
self.handle
.block_on(
crate::extensions::lease::ExtensionLeaseCapability::new(
self.session.clone(),
self.manager.clone(),
)
.call_exact(
&self.plugin,
memory_context::MEMORY_CAPTURE_TOOL_NAME,
&self.digest,
params,
),
)
.map(|_| ())
.map_err(|_| capture_worker::CaptureFailure {
code: "provider_call_failed",
})
}
}
fn terminal_capture_history(
lease: &memory_context::MemoryContextLease,
messages: &[crate::SharedMessage],
started_at: std::time::SystemTime,
) -> chat_capture::TerminalTurnHistory {
use agent_core::core::disclosure::DisclosureClass;
let mut items = Vec::new();
for message in messages {
let Some(role) = message.get("role").and_then(serde_json::Value::as_str) else {
continue;
};
let class = match role {
"user" => chat_capture::CaptureContentClass::UserMessage,
"assistant" => chat_capture::CaptureContentClass::AssistantFinal,
_ => continue,
};
let text = message
.get("content")
.and_then(serde_json::Value::as_str)
.map(str::to_owned)
.unwrap_or_else(|| {
message
.get("content")
.map(ToString::to_string)
.unwrap_or_default()
});
items.push(chat_capture::CanonicalCaptureItem {
project_id: lease.project_id.clone(),
class,
disclosure: DisclosureClass::ModelVisible,
sensitivity: chat_capture::Sensitivity::Normal,
text,
tool_name: None,
});
}
chat_capture::TerminalTurnHistory {
project_id: lease.project_id.clone(),
session_id: lease.session_id.clone(),
turn_id: memory_context::TurnId::parse(&format!("turn-{}", uuid::Uuid::new_v4()))
.expect("generated turn id"),
turn_ordinal: 0,
started_at,
completed_at: std::time::SystemTime::now(),
outcome: agent_core::TurnOutcome::Completed,
items,
compaction: None,
}
}
fn memory_provider_id() -> memory_context::ContextProviderId {
memory_context::ContextProviderId::parse("axel-memory")
.expect("static provider id is always valid")
}
fn terminal_capture_start(messages: &[crate::SharedMessage]) -> Option<crate::SharedMessage> {
messages
.iter()
.rfind(|message| {
message["role"] == "user"
&& (message["content"].is_string()
|| message["content"].as_array().is_some_and(|blocks| {
blocks.iter().any(|block| block["type"] != "tool_result")
}))
})
.cloned()
}
fn terminal_capture_messages<'a>(
messages: &'a [crate::SharedMessage],
start: &crate::SharedMessage,
) -> Option<&'a [crate::SharedMessage]> {
let index = messages
.iter()
.position(|message| Arc::ptr_eq(message, start))?;
Some(&messages[index..])
}
fn dispatch_completed_terminal_capture(
completed: bool,
final_history: &std::sync::Mutex<Option<Vec<crate::SharedMessage>>>,
state: &std::sync::Mutex<memory_context::SessionMemoryState>,
selected: Option<(
memory_context::MemoryContextLease,
Arc<dyn capture_worker::CaptureProvider>,
)>,
started_at: std::time::SystemTime,
turn_start: Option<&crate::SharedMessage>,
) -> bool {
let history = final_history
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.take();
let (Some(messages), Some((lease, provider))) = (history, selected) else {
return false;
};
if !completed {
return false;
}
let Some(messages) = turn_start.and_then(|start| terminal_capture_messages(&messages, start))
else {
return false;
};
let state = state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if state
.capture_lease_at(std::time::SystemTime::now())
.as_ref()
!= Some(&lease)
{
return false;
}
memory_capture_worker()
.submit_terminal(
&lease,
terminal_capture_history(&lease, messages, started_at),
memory_context::RetentionClass::Standard,
provider,
)
.unwrap_or(false)
}
const HTTP_READ_TIMEOUT: Duration = Duration::from_secs(90);
fn build_http_client(read_timeout: Duration) -> reqwest::Result<Client> {
Client::builder()
.tls_built_in_webpki_certs(true)
.tls_built_in_native_certs(true)
.connect_timeout(Duration::from_secs(10))
.read_timeout(read_timeout)
.build()
}
pub fn build_host_http_client() -> Result<Client> {
build_http_client(HTTP_READ_TIMEOUT)
.map_err(|e| RuntimeError::Config(format!("Failed to build HTTP client: {}", e)))
}
pub(crate) struct RuntimeParts {
pub host: crate::host::HostParts,
pub session_manager: Arc<crate::tools::shell::SessionManager>,
pub reaper: Option<(tokio::task::JoinHandle<()>, CancellationToken)>,
}
impl RuntimeParts {
pub(crate) fn with_reaper(host: crate::host::HostParts) -> Self {
let session_manager = fresh_session_manager();
let cancel = CancellationToken::new();
let handle =
crate::tools::shell::session::start_reaper(session_manager.clone(), cancel.clone());
Self {
host,
session_manager,
reaper: Some((handle, cancel)),
}
}
#[cfg_attr(not(any(test, feature = "testing")), allow(dead_code))]
pub(crate) fn without_reaper(host: crate::host::HostParts) -> Self {
Self {
host,
session_manager: fresh_session_manager(),
reaper: None,
}
}
}
fn fresh_session_manager() -> Arc<crate::tools::shell::SessionManager> {
let config = crate::tools::shell::ShellConfig::default();
crate::tools::shell::SessionManager::new(config)
}
fn fresh_host_parts() -> Result<crate::host::HostParts> {
let client = build_host_http_client()?;
let capture_dir = trace::default_capture_dir();
let _ = trace::sweep_expired_captures(&capture_dir);
Ok(crate::host::HostParts {
client,
tools: Arc::new(RwLock::new(ToolRegistry::new())),
hook_bus: Arc::new(crate::extensions::hooks::HookBus::new()),
credential_source: crate::auth::CredentialSource::Local,
token_cache: crate::auth::TokenCache::new(),
mcp_runtime: None,
extension_runtime: None,
capture_dir,
progressive_tool_disclosure: false,
})
}
fn canonical_trusted_worker_model(model: &str) -> String {
let model = model.trim();
if let Some(id) = model.strip_prefix("claude/") {
format!("anthropic/{id}")
} else if model.starts_with("claude-") {
format!("anthropic/{model}")
} else {
model.to_owned()
}
}
impl Drop for Runtime {
fn drop(&mut self) {
if let Some(c) = &self.reaper_cancel {
c.cancel();
}
}
}
impl Runtime {
pub async fn new() -> Result<Self> {
Ok(Self::from_parts(RuntimeParts::with_reaper(
fresh_host_parts()?,
)))
}
pub(crate) fn from_parts(parts: RuntimeParts) -> Self {
let (auth_token, auth_type, refresh_token, token_expires) =
(String::new(), "oauth".to_string(), None, Some(0));
let RuntimeParts {
host,
session_manager,
reaper,
} = parts;
let (reaper_handle, reaper_cancel) = match reaper {
Some((h, c)) => (Some(h), Some(c)),
None => (None, None),
};
let mcp_runtime = host.mcp_runtime;
let extension_runtime = host.extension_runtime;
let mut runtime = Runtime {
client: host.client,
anthropic_stream_client: Default::default(),
auth: Arc::new(RwLock::new(AuthState {
auth_token,
auth_type,
refresh_token,
token_expires,
auth_file_mtime: None,
bound_credential: None,
})),
model: crate::models::default_model().to_string(),
tools: host.tools,
system_prompt: None,
effective_prompt: None,
prompt_generation: 0,
prompt_reload_source: None,
thinking_budget: 4096,
named_level: None,
explicit_reasoning: false,
codex_request_role: crate::runtime::openai::catalog::CodexRequestRole::Foreground,
context_window_override: None,
compaction_model: None,
compaction_mode: agent_core::compaction::CompactionMode::default(),
compaction_exclusions: Vec::new(),
remote_summarization_attempts: Arc::new(std::sync::atomic::AtomicU64::new(0)),
subagent_registry: Arc::new(Mutex::new(
crate::runtime::subagent::SubagentRegistry::new(),
)),
orchestration: None,
event_queue: Arc::new(crate::events::EventQueue::new(1000)),
watcher_exit_path: None,
max_tool_output: 30000,
bash_timeout: 30,
bash_max_timeout: 300,
subagent_timeout: 300,
api_retries: 3,
refusal_retries: 2,
telemetry_level: crate::runtime::telemetry::TelemetryLevel::Off,
telemetry_writer: None,
trace_ctx: trace::TraceContext::disabled(),
trace_controls: std::sync::Arc::new(trace::TraceControls::new()),
memory_context_state: fresh_memory_context_state(),
memory_backend: crate::memory_backend::MemoryBinding::legacy_current(),
memory_backend_config: None,
memory_backend_reconfigure_denied: Arc::new(std::sync::atomic::AtomicBool::new(false)),
continuation: Arc::new(Mutex::new(continuation::ContinuationState::default())),
pending_history_import_preview: std::sync::Arc::new(std::sync::Mutex::new(None)),
history_import_plan: std::sync::Arc::new(std::sync::Mutex::new(None)),
#[cfg(any(test, feature = "testing"))]
capture_provider_for_test: std::sync::Arc::new(std::sync::Mutex::new(None)),
pending_memory_segment: std::sync::Arc::new(std::sync::Mutex::new(None)),
retained_recall_turn: std::sync::Arc::new(std::sync::Mutex::new(None)),
one_shot_trace_writer: std::sync::Arc::new(std::sync::Mutex::new(None)),
capture_dir: host.capture_dir,
cache_diagnostics: false,
cache_ttl: crate::core::config::CacheTtl::default(),
ttl_downgrade_notified: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
saw_1h_honored: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
cache_anchor: Default::default(),
last_msg_id: Arc::new(Mutex::new(None)),
cache_diag_rejected: Default::default(),
session_manager,
hook_bus: host.hook_bus,
reaper_handle,
reaper_cancel,
credential_source: host.credential_source,
token_cache: host.token_cache,
trusted_worker_models: Vec::new(),
subagent_limits: crate::config::SubagentLimits::default(),
progressive_tool_disclosure: host.progressive_tool_disclosure,
deferred_tool_loading: true,
activation_confirm: agent_core::config::ActivationConfirm::default(),
delegation_parent: None,
session_allow_all: std::sync::Arc::new(crate::tools::SessionConfirmLatch::new()),
mcp_runtime: None,
mcp_session_scope: None,
extension_runtime: None,
extension_session_scope: None,
turn_budget: crate::runtime::budget::TurnBudget::for_role(
crate::runtime::budget::TurnRole::Foreground,
),
owner_steer_budget: None,
host_tool_session: fresh_host_tool_session(),
tool_catalog_prompt: Arc::new(std::sync::Mutex::new(None)),
retained_tool_set: Default::default(),
pending_tool_restore: Default::default(),
session_id: None,
cwd: None,
env: None,
env_stripped: Vec::new(),
env_warned: Default::default(),
};
if let Some(m) = mcp_runtime {
runtime.install_mcp_runtime(m);
}
if let Some(m) = extension_runtime {
runtime.install_extension_runtime(m);
}
runtime
}
#[cfg(any(test, feature = "testing"))]
pub fn new_headless() -> Self {
let client = build_http_client(HTTP_READ_TIMEOUT)
.expect("reqwest client construction is infallible with built-in roots");
let host = crate::host::HostParts {
client,
tools: Arc::new(RwLock::new(ToolRegistry::new())),
hook_bus: Arc::new(crate::extensions::hooks::HookBus::new()),
credential_source: crate::auth::CredentialSource::Local,
token_cache: crate::auth::TokenCache::new(),
mcp_runtime: None,
extension_runtime: None,
capture_dir: trace::default_capture_dir(),
progressive_tool_disclosure: false,
};
let mut runtime = Self::from_parts(RuntimeParts::without_reaper(host));
runtime.auth = Arc::new(RwLock::new(AuthState {
auth_token: "test-token".to_string(),
auth_type: "api_key".to_string(),
refresh_token: None,
token_expires: None,
auth_file_mtime: None,
bound_credential: None,
}));
runtime
}
pub fn set_system_prompt(&mut self, prompt: String) {
self.system_prompt = Some(prompt);
}
pub fn system_prompt(&self) -> Option<&str> {
self.system_prompt.as_deref()
}
pub async fn effective_system_prompt(&self) -> Option<String> {
let base = self.orchestrated_system_prompt().await;
if self.effective_prompt.is_some() {
return base;
}
match self.frozen_tool_catalog().await {
Some(catalog) => Some(match base {
Some(base) if !base.is_empty() => format!("{base}\n\n{catalog}"),
_ => catalog,
}),
None => base,
}
}
pub async fn frozen_tool_catalog(&self) -> Option<String> {
if !self.progressive_tool_disclosure
|| self.activation_confirm == agent_core::config::ActivationConfirm::Deny
{
return None;
}
if let Some(frozen) = self
.tool_catalog_prompt
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
{
return frozen;
}
let rendered = {
let registry = self.tools.read().await;
if registry.get("activate_tools").is_none() {
None
} else {
let core_set = continuation::context_tool_set(
self.host_tool_session.clone(),
registry.catalog(),
true,
self.context_management_enabled(),
);
let core: std::collections::HashSet<String> = core_set
.core_ids()
.map(|id| id.as_str().to_string())
.collect();
crate::tools::prompt_catalog::render(
registry.catalog(),
&core,
crate::tools::activation::is_activatable_record,
)
}
};
let mut slot = self
.tool_catalog_prompt
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
slot.get_or_insert(rendered).clone()
}
async fn orchestrated_system_prompt(&self) -> Option<String> {
if self.effective_prompt.is_some() {
return self.system_prompt.clone();
}
if self.tools.read().await.get("subagent_start").is_none() {
return self.system_prompt.clone();
}
let Ok(model) = crate::orchestration::canonical_foreground_identity(&self.model) else {
return self.system_prompt.clone();
};
let workflow_mode = match self.reasoning_level() {
agent_core::reasoning::ReasoningLevel::UltraCode => {
Some(agent_core::prompt::WorkflowMode::UltraCode)
}
agent_core::reasoning::ReasoningLevel::Max => {
Some(agent_core::prompt::WorkflowMode::Max)
}
agent_core::reasoning::ReasoningLevel::XHigh => {
Some(agent_core::prompt::WorkflowMode::XHigh)
}
_ => None,
};
let Ok(context) = agent_core::prompt::SelectionContext::new(model, None)
.map(|context| context.with_workflow_mode(workflow_mode))
else {
return self.system_prompt.clone();
};
agent_core::prompt::compose_orchestration_prompt(self.system_prompt.as_deref(), &context)
}
pub fn effective_prompt(&self) -> Option<&agent_core::prompt::PromptStack> {
self.effective_prompt.as_ref()
}
pub fn prompt_generation(&self) -> u64 {
self.prompt_generation
}
pub fn retain_prompt_reload_source(
&mut self,
manifest: PathBuf,
context: agent_core::prompt::SelectionContext,
user_module: Option<agent_core::prompt::PromptModule>,
delegation_policy_digest: Option<String>,
) {
self.prompt_reload_source = Some(PromptReloadSource {
manifest,
context,
user_module,
delegation_policy_digest,
});
}
pub fn reload_prompt(&mut self) -> std::result::Result<u64, agent_core::prompt::PromptError> {
let source = self.prompt_reload_source.clone().ok_or_else(|| {
agent_core::prompt::PromptError::Invalid("no prompt manifest is active".into())
})?;
let raw = std::fs::read_to_string(&source.manifest).map_err(|_| {
agent_core::prompt::PromptError::Invalid("prompt manifest is unavailable".into())
})?;
let manifest = agent_core::prompt::PromptManifest::parse(&raw)?;
let reload_catalog = crate::orchestration::OrchestrationRuntime::trusted_catalog(
source.context.model(),
manifest.delegation_catalog_candidates(),
)
.map_err(|error| agent_core::prompt::PromptError::Invalid(error.into()))?;
let candidate_policy_digest = manifest
.delegation_policy(source.context.model().clone(), &reload_catalog)?
.map(|policy| policy.digest());
if candidate_policy_digest != source.delegation_policy_digest {
return Err(agent_core::prompt::PromptError::Invalid(
"hot reload cannot safely change delegation policy".into(),
));
}
let registry = manifest.registry(source.manifest.parent())?;
let candidate = agent_core::prompt::compile_prompt_stack(
&manifest,
®istry,
&source.context,
source.user_module,
)?;
self.apply_prompt_stack(candidate)
}
pub fn apply_prompt_stack(
&mut self,
candidate: agent_core::prompt::PromptStack,
) -> std::result::Result<u64, agent_core::prompt::PromptError> {
if let Some(current) = &self.effective_prompt {
current.validate_hot_reload(&candidate)?;
}
let composed = candidate.composed().to_owned();
self.effective_prompt = Some(candidate);
self.system_prompt = Some(composed);
self.prompt_generation = self.prompt_generation.saturating_add(1);
Ok(self.prompt_generation)
}
pub fn prompt_inspection_json(&self) -> Option<String> {
let mode = self
.orchestration
.as_ref()
.map(|runtime| runtime.enforcement_mode())
.unwrap_or(agent_core::orchestration::EnforcementMode::Off);
self.effective_prompt.as_ref().and_then(|stack| {
serde_json::to_string(&serde_json::json!({
"generation": self.prompt_generation,
"effective": stack.inspect(mode),
"token_estimate": stack.composed().len().div_ceil(4),
}))
.ok()
})
}
pub fn install_mcp_runtime(&mut self, manager: std::sync::Arc<crate::mcp::McpRuntimeManager>) {
self.mcp_session_scope = Some(std::sync::Arc::new(crate::mcp::McpSessionEndGuard::new(
self.host_tool_session.clone(),
std::sync::Arc::clone(&manager),
)));
self.mcp_runtime = Some(manager);
}
pub fn set_turn_budget(&mut self, budget: crate::runtime::budget::TurnBudget) {
self.turn_budget = budget;
}
pub fn set_owner_steer_budget(
&mut self,
lift: Option<Arc<crate::runtime::budget::OwnerSteerBudget>>,
) {
self.owner_steer_budget = lift;
}
pub fn turn_budget(&self) -> &crate::runtime::budget::TurnBudget {
&self.turn_budget
}
pub fn install_extension_runtime(
&mut self,
manager: std::sync::Arc<crate::extensions::lease::ExtensionRuntimeManager>,
) {
manager.bind_host_scope(self.host_tool_session.clone());
self.extension_session_scope = Some(std::sync::Arc::new(
crate::extensions::lease::ExtensionSessionEndGuard::new(
self.host_tool_session.clone(),
std::sync::Arc::clone(&manager),
),
));
self.extension_runtime = Some(manager);
}
pub fn install_orchestration(
&mut self,
runtime: Arc<crate::orchestration::OrchestrationRuntime>,
) {
for model in &self.trusted_worker_models {
if let Err(error) = runtime.grant_worker_model(model.as_str()) {
tracing::warn!(
model = model.as_str(),
error = %error,
"failed to apply configured worker-model trust"
);
}
}
self.orchestration = Some(runtime);
}
pub fn install_worker_orchestration(
&mut self,
runtime: Arc<crate::orchestration::OrchestrationRuntime>,
) {
self.orchestration = Some(runtime);
}
pub fn subagent_limits(&self) -> crate::config::SubagentLimits {
self.subagent_limits
}
pub fn set_delegation_parent(&mut self, parent: Option<String>) {
self.delegation_parent = parent;
}
pub fn orchestration(&self) -> Option<&Arc<crate::orchestration::OrchestrationRuntime>> {
self.orchestration.as_ref()
}
pub fn grant_worker_model(&self, model: &str) -> std::result::Result<(), String> {
self.orchestration
.as_ref()
.ok_or_else(|| "delegation policy unavailable".to_string())?
.grant_worker_model(model)
}
pub fn set_model(&mut self, model: String) -> Option<ReasoningClamp> {
self.try_set_model(model).ok().flatten()
}
pub fn try_set_model(
&mut self,
model: String,
) -> std::result::Result<Option<ReasoningClamp>, String> {
let trimmed = model.trim();
let cleaned = trimmed
.split_once(char::is_whitespace)
.and_then(|(_, rest)| {
let rest = rest.trim_start();
let (latency, candidate) = rest.split_once(char::is_whitespace)?;
let millis = latency.strip_suffix("ms")?;
if !millis.is_empty() && millis.chars().all(|c| c.is_ascii_digit()) {
let candidate = candidate.trim();
if !candidate.is_empty() && !candidate.chars().any(char::is_whitespace) {
return Some(candidate);
}
}
None
})
.unwrap_or(trimmed);
if self.prompt_reload_source.is_none()
&& self
.orchestration
.as_ref()
.is_some_and(|current| !current.unreconciled_runtime_handles().is_empty())
{
return Err("model change blocked: workers require collection/reconciliation".into());
}
let orchestration_replacement = if self.prompt_reload_source.is_none()
&& self.orchestration.is_some()
{
let foreground = crate::orchestration::canonical_foreground_identity(cleaned)
.map_err(|_| "model change blocked: unresolved foreground model".to_string())?;
let replacement = crate::orchestration::OrchestrationRuntime::from_limits(
foreground,
self.subagent_limits,
)
.map_err(|_| "model change blocked: trusted worker catalog unavailable".to_string())?;
for trusted in &self.trusted_worker_models {
replacement
.grant_worker_model(trusted.as_str())
.map_err(|_| {
"model change blocked: configured worker trust invalid".to_string()
})?;
}
Some(Arc::new(replacement))
} else {
None
};
self.model = cleaned.to_owned();
if let Some(replacement) = orchestration_replacement {
self.orchestration = Some(replacement);
}
if !self.explicit_reasoning {
if let Some(level) =
crate::runtime::openai::catalog::validation::default_level_for_model(cleaned)
{
self.set_reasoning_level(level);
}
}
Ok(self.clamp_reasoning_to_model())
}
fn clamp_reasoning_to_model(&mut self) -> Option<ReasoningClamp> {
use crate::runtime::openai::catalog::validation::{
default_level_for_model, thinking_options_for_model, validate_reasoning_mutation,
};
let model = self.model.as_str();
let from = self.reasoning_level();
if validate_reasoning_mutation(model, from).is_ok() {
return None;
}
let to = default_level_for_model(model)
.filter(|level| validate_reasoning_mutation(model, *level).is_ok())
.or_else(|| {
thinking_options_for_model(model)
.iter()
.filter_map(|opt| agent_core::reasoning::ReasoningLevel::parse(opt))
.find(|level| validate_reasoning_mutation(model, *level).is_ok())
})?;
let explicit = self.explicit_reasoning;
self.set_reasoning_level(to);
self.explicit_reasoning = explicit;
Some(ReasoningClamp { from, to })
}
pub fn set_tools(&mut self, tools: ToolRegistry) {
self.tools = Arc::new(RwLock::new(tools));
}
pub fn subagent_registry(&self) -> &Arc<Mutex<crate::runtime::subagent::SubagentRegistry>> {
&self.subagent_registry
}
pub fn event_queue(&self) -> &Arc<crate::events::EventQueue> {
&self.event_queue
}
pub fn set_event_queue(&mut self, queue: Arc<crate::events::EventQueue>) {
self.event_queue = queue;
}
pub fn hook_bus(&self) -> &Arc<crate::extensions::hooks::HookBus> {
&self.hook_bus
}
pub fn session_manager(&self) -> &Arc<crate::tools::shell::SessionManager> {
&self.session_manager
}
pub fn host_tool_session_id(&self) -> &crate::tools::activation::SessionId {
&self.host_tool_session
}
pub fn set_session_id(&mut self, id: Option<String>) {
self.session_id = id;
}
pub fn begin_conversation(&mut self, id: Option<String>) {
self.session_allow_all.revoke();
self.session_allow_all = std::sync::Arc::new(crate::tools::SessionConfirmLatch::new());
*self
.last_msg_id
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
self.set_session_id(id);
}
pub fn session_id(&self) -> Option<&str> {
self.session_id.as_deref()
}
pub fn set_cwd(&mut self, cwd: Option<PathBuf>) {
self.cwd = cwd;
}
pub fn cwd(&self) -> Option<&Path> {
self.cwd.as_deref()
}
pub fn set_env(&mut self, env: Option<crate::session::types::SessionEnv>) {
self.env = env;
}
pub fn set_env_stripped(&mut self, stripped: Vec<String>) {
self.env_stripped = stripped;
}
pub fn env(&self) -> Option<&crate::session::types::SessionEnv> {
self.env.as_ref()
}
pub fn env_stripped(&self) -> &[String] {
&self.env_stripped
}
pub fn tools_shared(&self) -> Arc<RwLock<ToolRegistry>> {
Arc::clone(&self.tools)
}
pub fn model(&self) -> &str {
&self.model
}
pub fn http_client(&self) -> &Client {
&self.client
}
pub async fn prepare_model_capabilities(&self, model: &str) {
if model.starts_with("muse/") {
let broker = crate::auth::broker_from_source(
&self.credential_source,
&self.token_cache,
self.client.clone(),
);
crate::runtime::openai::catalog::ensure_muse_catalog_for(model, &broker).await;
}
}
pub fn progressive_tool_disclosure(&self) -> bool {
self.progressive_tool_disclosure
}
pub fn carried_tool_activations(&self) -> Vec<crate::tools::activation::CarriedActivation> {
let retained = self
.retained_tool_set
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
if let Some(shared) = retained {
return shared
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.carried_activations();
}
self.pending_tool_restore
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone()
.unwrap_or_default()
}
pub fn restore_tool_activations(
&self,
carried: Vec<crate::tools::activation::CarriedActivation>,
) {
if carried.is_empty() {
return;
}
let mut pending = self
.pending_tool_restore
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match pending.as_mut() {
Some(list) => {
for c in carried {
if !list.iter().any(|p| p.tool_id == c.tool_id) {
list.push(c);
}
}
}
None => *pending = Some(carried),
}
}
pub fn activation_confirm(&self) -> agent_core::config::ActivationConfirm {
self.activation_confirm
}
pub fn set_activation_confirm(&mut self, mode: agent_core::config::ActivationConfirm) {
self.activation_confirm = mode;
}
#[cfg(any(test, feature = "testing"))]
pub fn http_client_pool_id(&self) -> usize {
const _: () = assert!(std::mem::size_of::<Client>() == std::mem::size_of::<usize>());
unsafe { std::mem::transmute_copy::<Client, usize>(&self.client) }
}
pub fn set_thinking_budget(&mut self, budget: u32) {
self.thinking_budget = budget;
self.named_level = Some(agent_core::reasoning::ReasoningLevel::from_legacy_budget(
budget,
));
self.explicit_reasoning = false;
}
pub fn set_reasoning_level(&mut self, level: agent_core::reasoning::ReasoningLevel) {
self.named_level = Some(level);
if let Some(budget) = level.to_legacy_budget() {
self.thinking_budget = budget;
}
self.explicit_reasoning = false;
}
pub fn set_reasoning_level_explicit(&mut self, level: agent_core::reasoning::ReasoningLevel) {
self.set_reasoning_level(level);
self.explicit_reasoning = true;
}
pub fn restore_session_reasoning(&mut self, thinking_level: &str) -> Option<ReasoningClamp> {
if let Some(level) = agent_core::reasoning::ReasoningLevel::parse(thinking_level) {
self.set_reasoning_level_explicit(level);
} else if let Some(budget) = crate::models::budget_for_thinking_level(thinking_level) {
self.set_thinking_budget_explicit(budget);
} else {
return None;
}
self.clamp_reasoning_to_model()
}
pub fn set_thinking_budget_explicit(&mut self, budget: u32) {
self.thinking_budget = budget;
self.named_level = Some(agent_core::reasoning::ReasoningLevel::from_legacy_budget(
budget,
));
self.explicit_reasoning = true;
}
pub fn is_reasoning_explicit(&self) -> bool {
self.explicit_reasoning
}
pub fn thinking_budget_raw(&self) -> u32 {
self.thinking_budget
}
pub fn set_reasoning_level_checked(
&mut self,
level: agent_core::reasoning::ReasoningLevel,
) -> std::result::Result<(), String> {
crate::runtime::openai::catalog::validation::validate_reasoning_mutation(
&self.model,
level,
)?;
if self.model.starts_with("anthropic/")
&& level == agent_core::reasoning::ReasoningLevel::UltraCode
{
if self.codex_request_role()
!= crate::runtime::openai::catalog::ExecutionRole::Foreground
{
return Err("ultracode_requires_foreground".into());
}
if self.effective_prompt.is_some() {
return Err("typed_prompt_manifest_blocks_required_doctrine".into());
}
if self.orchestration.is_none() {
return Err("ultracode_requires_orchestration".into());
}
let tools = self.tools.try_read().map_err(|_| {
"ultracode prerequisite state is busy; refusing mutation".to_string()
})?;
let required = ["subagent_start", "subagent_status", "subagent_collect"];
if !required.iter().all(|name| {
tools
.get(name)
.is_some_and(|tool| tool.extension_id().is_none())
}) {
return Err("ultracode_requires_lifecycle_tools".into());
}
}
self.set_reasoning_level_explicit(level);
Ok(())
}
pub fn set_thinking_budget_checked(&mut self, budget: u32) -> std::result::Result<(), String> {
let level = agent_core::reasoning::ReasoningLevel::from_legacy_budget(budget);
crate::runtime::openai::catalog::validation::validate_reasoning_mutation(
&self.model,
level,
)?;
self.set_thinking_budget_explicit(budget);
Ok(())
}
pub fn reasoning_level(&self) -> agent_core::reasoning::ReasoningLevel {
self.named_level.unwrap_or_else(|| {
agent_core::reasoning::ReasoningLevel::from_legacy_budget(self.thinking_budget)
})
}
pub(crate) fn set_codex_request_role(
&mut self,
role: crate::runtime::openai::catalog::CodexRequestRole,
) {
self.codex_request_role = role;
}
pub(crate) fn codex_request_role(&self) -> crate::runtime::openai::catalog::CodexRequestRole {
self.codex_request_role
}
pub(crate) fn codex_delegation_plan(
model: &str,
level: agent_core::reasoning::ReasoningLevel,
role: crate::runtime::openai::catalog::CodexRequestRole,
) -> Option<crate::runtime::openai::catalog::CodexExecutionPlan> {
use crate::runtime::openai::catalog::{plan_codex_execution, CodexRequestRole};
if role != CodexRequestRole::Foreground
|| level != agent_core::reasoning::ReasoningLevel::Ultra
|| !model.starts_with("openai-codex/")
{
return None;
}
plan_codex_execution(model, level, role, None).ok()
}
async fn authorized_anthropic_plan(
&self,
) -> Result<Option<crate::runtime::openai::catalog::AnthropicExecutionPlan>> {
if !self.model.starts_with("anthropic/") {
return Ok(None);
}
let tools = self.tools.read().await;
let builtin = |name: &str| {
tools
.get(name)
.is_some_and(|tool| tool.extension_id().is_none())
};
let readiness = self
.orchestration
.as_ref()
.and_then(|runtime| runtime.ultracode_readiness(&self.model).ok());
let prerequisites = crate::runtime::openai::catalog::AnthropicPlanPrerequisites {
orchestration_policy: self.orchestration.is_some(),
foreground_worker_authorized: readiness.is_some(),
concurrent_limit: readiness.map_or(0, |limits| limits.0),
total_limit: readiness.map_or(0, |limits| limits.1),
lifecycle_start: builtin("subagent_start"),
lifecycle_status: builtin("subagent_status"),
lifecycle_steer: builtin("subagent_steer"),
lifecycle_collect: builtin("subagent_collect"),
lifecycle_resume: builtin("subagent_resume"),
};
crate::runtime::openai::catalog::plan_anthropic_execution(
&self.model,
self.reasoning_level(),
self.codex_request_role(),
prerequisites,
crate::runtime::openai::catalog::capability_cache::get(&self.model).and_then(|entry| {
match entry.reasoning {
crate::runtime::openai::catalog::ReasoningSupport::AnthropicAdaptive {
adaptive,
} => Some(adaptive),
crate::runtime::openai::catalog::ReasoningSupport::None => Some(false),
_ => None,
}
}),
)
.map(Some)
.map_err(|error| RuntimeError::Config(error.to_string()))
}
async fn validate_request_preflight(&self) -> Result<()> {
self.validate_request_preflight_for(&self.model, self.codex_request_role)
.await
}
pub(crate) async fn validate_session_driver_preflight(&self) -> Result<()> {
self.validate_request_preflight().await
}
async fn validate_request_preflight_for(
&self,
model: &str,
role: crate::runtime::openai::catalog::CodexRequestRole,
) -> Result<()> {
if self
.memory_backend_reconfigure_denied
.load(std::sync::atomic::Ordering::SeqCst)
{
return Err(RuntimeError::Config(
"memory backend cannot be reconfigured at runtime (including live legacy extension processes); restart required; request denied".into(),
));
}
let level = self.reasoning_level();
if model.starts_with("anthropic/")
&& level == agent_core::reasoning::ReasoningLevel::UltraCode
&& self.effective_prompt.is_some()
{
return Err(RuntimeError::Config(
"Anthropic execution plan rejected: typed_prompt_manifest_blocks_required_doctrine"
.into(),
));
}
if model.starts_with("anthropic/")
&& matches!(
level,
agent_core::reasoning::ReasoningLevel::Max
| agent_core::reasoning::ReasoningLevel::UltraCode
| agent_core::reasoning::ReasoningLevel::Ultra
)
{
let tools = self.tools.read().await;
let builtin = |name: &str| {
tools
.get(name)
.is_some_and(|tool| tool.extension_id().is_none())
};
let readiness = self
.orchestration
.as_ref()
.and_then(|runtime| runtime.ultracode_readiness(model).ok());
let prerequisites = crate::runtime::openai::catalog::AnthropicPlanPrerequisites {
orchestration_policy: self.orchestration.is_some(),
foreground_worker_authorized: readiness.is_some(),
concurrent_limit: readiness.map_or(0, |limits| limits.0),
total_limit: readiness.map_or(0, |limits| limits.1),
lifecycle_start: builtin("subagent_start"),
lifecycle_status: builtin("subagent_status"),
lifecycle_steer: builtin("subagent_steer"),
lifecycle_collect: builtin("subagent_collect"),
lifecycle_resume: builtin("subagent_resume"),
};
drop(tools);
let result = crate::runtime::openai::catalog::plan_anthropic_execution(
model,
level,
role,
prerequisites,
crate::runtime::openai::catalog::capability_cache::get(model).and_then(|entry| {
match entry.reasoning {
crate::runtime::openai::catalog::ReasoningSupport::AnthropicAdaptive {
adaptive,
} => Some(adaptive),
crate::runtime::openai::catalog::ReasoningSupport::None => Some(false),
_ => None,
}
}),
);
match result {
Ok(plan) => {
tracing::debug!(event = "anthropic_mode_plan", qualified_model = %model, requested_level = %level, runtime_role = role.as_str(), execution_mode = ?plan.mode, wire_effort = plan.wire_effort.map_or("omitted", |effort| effort.as_str()), workflow = ?plan.workflow, credentials_attempted = false, network_attempted = false);
return Ok(());
}
Err(error) => {
tracing::debug!(event = "anthropic_mode_plan", qualified_model = %model, requested_level = %level, runtime_role = role.as_str(), decision = "deny", deny_code = error.code().as_str(), credentials_attempted = false, network_attempted = false);
return Err(RuntimeError::Config(error.to_string()));
}
}
}
if model.starts_with("openai-codex/") {
let plan =
crate::runtime::openai::catalog::plan_codex_execution(model, level, role, None)
.map_err(|error| {
tracing::debug!(
event = "codex_mode_plan",
qualified_model = %model,
requested_level = %level,
runtime_role = role.as_str(),
decision = "deny",
deny_code = error.code().as_str(),
network_attempted = false,
"Codex request preflight denied"
);
RuntimeError::Config(error.to_string())
})?;
if plan.automatic_delegation() {
if self.orchestration.is_none() {
tracing::debug!(
event = "codex_mode_plan",
qualified_model = %model,
requested_level = %level,
runtime_role = role.as_str(),
decision = "deny",
deny_code = "ultra_requires_orchestration",
network_attempted = false,
"Codex request preflight denied"
);
return Err(RuntimeError::Config(
"Ultra requires an installed orchestration policy".to_string(),
));
}
let tools = self.tools.read().await;
let required = ["subagent_start", "subagent_status", "subagent_collect"];
let tools_ready = required.iter().all(|name| {
tools
.get(name)
.is_some_and(|tool| tool.extension_id().is_none())
});
if !tools_ready {
tracing::debug!(
event = "codex_mode_plan",
qualified_model = %model,
requested_level = %level,
runtime_role = role.as_str(),
decision = "deny",
deny_code = "ultra_requires_subagent_tools",
network_attempted = false,
"Codex request preflight denied"
);
return Err(RuntimeError::Config(
"Ultra requires built-in subagent_start, subagent_status, and subagent_collect tools"
.to_string(),
));
}
}
return Ok(());
}
crate::runtime::openai::catalog::validation::validate_reasoning_mutation(model, level)
.map_err(|error| {
tracing::debug!(
event = "codex_mode_plan",
qualified_model = %model,
requested_level = %level,
runtime_role = role.as_str(),
decision = "deny",
deny_code = "unsupported_reasoning_level",
network_attempted = false,
"request preflight denied"
);
RuntimeError::Config(error)
})
}
pub fn set_compaction_model(&mut self, model: Option<String>) {
self.compaction_model = model;
}
pub fn set_compaction_mode(&mut self, mode: agent_core::compaction::CompactionMode) {
self.compaction_mode = mode;
}
pub fn set_compaction_exclusions(
&mut self,
exclude: Vec<agent_core::compaction::ContentClass>,
) {
self.compaction_exclusions = exclude;
}
pub fn remote_summarization_attempts(&self) -> u64 {
self.remote_summarization_attempts
.load(std::sync::atomic::Ordering::SeqCst)
}
pub fn compaction_policy(&self) -> crate::runtime::compaction::DisclosurePolicy {
crate::runtime::compaction::DisclosurePolicy {
mode: self.compaction_mode,
exclude: self.compaction_exclusions.clone(),
}
}
pub fn set_context_window(&mut self, window: Option<u64>) {
self.context_window_override = window;
}
pub fn compaction_model(&self) -> &str {
self.compaction_model
.as_deref()
.unwrap_or("claude-sonnet-4-6")
}
pub fn context_window(&self) -> u64 {
self.context_window_override
.unwrap_or_else(|| crate::models::context_window_for_model(&self.model))
}
pub async fn assess_context(
&self,
messages: &[crate::SharedMessage],
) -> context::ContextAssessment {
let system = self.effective_system_prompt().await;
let schema = self.tools.read().await.tools_schema();
let memory_rendered: Option<String> = self
.pending_memory_segment
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.map(|segment| segment.rendered_text().to_string());
let memory_contents: Vec<&str> = memory_rendered.as_deref().into_iter().collect();
context::assess(&context::ContextBudgetInputs {
model: &self.model,
provider_window: self.context_window(),
system_prompt: system.as_deref(),
tools_schema: &schema,
messages,
skill_contents: &[],
memory_contents: &memory_contents,
thinking_budget_tokens: self.thinking_budget as u64,
next_tool_result_bytes: self.max_tool_output as u64,
output_reserve_tokens: HelperMethods::max_tokens_for_model(&self.model),
})
}
#[allow(dead_code)] pub(crate) fn hold_memory_contribution(
&self,
contribution: memory_context::MemoryContextContribution,
) -> std::result::Result<(), memory_context::MemoryContextError> {
let budget = memory_context::memory_budget_tokens(self.context_window())
.ok_or(memory_context::MemoryContextError::RecallBudgetBelowMinimum)?;
memory_context::validate_contribution(
&contribution,
&self.memory_context_project_id(),
budget,
&memory_context::DisclosureGrantSet::model_visible_only(),
)?;
*self
.pending_memory_segment
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
Some(memory_context::ContextSegment::Memory(contribution));
Ok(())
}
pub(crate) fn clear_memory_contribution(&self) {
*self
.pending_memory_segment
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
}
pub fn memory_recall_why(&self) -> Option<memory_context::RecallTurnMetadata> {
self.retained_recall_turn
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.map(|retained| retained.why.clone())
}
async fn apply_turn_memory_recall(&self, messages: &mut Vec<crate::SharedMessage>) {
if (self.memory_backend.exclusive() && !self.memory_backend.is_axel())
|| self
.memory_backend_reconfigure_denied
.load(std::sync::atomic::Ordering::SeqCst)
{
self.clear_memory_contribution();
tracing::debug!("memory recall unavailable for the selected host backend");
return;
}
let extension_runtime = self.extension_runtime.clone();
let binding = self.memory_backend.clone();
let session = self.host_tool_session.clone();
let outcome = memory_context::resolve_turn_recall(
&self.memory_context_state,
&self.retained_recall_turn,
&self.memory_context_project_id(),
self.context_window(),
messages,
memory_context::RECALL_HARD_TIMEOUT,
move |lease: memory_context::MemoryContextLease,
request: memory_context::RecallRequest| async move {
if binding.is_axel() {
return axel_context::recall(binding, lease, request).await;
}
let Some(manager) = extension_runtime else {
return Err(memory_context::RecallCallError::ProviderUnavailable);
};
let mut parts = lease.provider_id.as_str().splitn(3, ':');
let (Some("extension"), Some(plugin), Some(local)) =
(parts.next(), parts.next(), parts.next())
else {
return Err(memory_context::RecallCallError::ProviderUnavailable);
};
let Some(digest) =
manager.declared_tool_digest(plugin, memory_context::MEMORY_RECALL_TOOL_NAME)
else {
return Err(memory_context::RecallCallError::ProviderUnavailable);
};
let mut response =
crate::extensions::lease::ExtensionLeaseCapability::new(session, manager)
.call_exact(
plugin,
memory_context::MEMORY_RECALL_TOOL_NAME,
&digest,
memory_context::recall_request_wire(&request),
)
.await
.map_err(|_| memory_context::RecallCallError::CallFailed)?;
if response["provider_id"] != local
&& response["provider_id"] != lease.provider_id.as_str()
{
return Err(memory_context::RecallCallError::CallFailed);
}
response["provider_id"] = json!(lease.provider_id.as_str());
Ok(response)
},
)
.await;
match outcome {
memory_context::TurnRecallOutcome::Injected
| memory_context::TurnRecallOutcome::ReusedRetained => {
let contribution = self
.retained_recall_turn
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.as_ref()
.map(|retained| retained.contribution.clone());
if let Some(contribution) = contribution {
*self
.pending_memory_segment
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) =
Some(memory_context::ContextSegment::Memory(contribution));
}
}
memory_context::TurnRecallOutcome::NotEligible => {
self.clear_memory_contribution();
}
memory_context::TurnRecallOutcome::SkippedOpen(reason) => {
self.clear_memory_contribution();
tracing::debug!(
reason = reason.as_str(),
"memory recall skipped; turn proceeds without memory"
);
}
}
}
#[cfg(any(test, feature = "testing"))]
pub async fn apply_turn_memory_recall_for_harness(
&self,
messages: &mut Vec<crate::SharedMessage>,
) {
self.apply_turn_memory_recall(messages).await;
}
#[cfg(any(test, feature = "testing"))]
pub fn capture_completed_turn_for_harness(
&self,
messages: Vec<crate::SharedMessage>,
turn_ordinal: u64,
) -> std::result::Result<(), &'static str> {
let started_at = std::time::SystemTime::now();
let lease = self
.memory_context_lock()
.capture_lease_at(started_at)
.ok_or("capture lease unavailable")?;
let provider = self
.extension_capture_provider(&lease)
.ok_or("capture provider unavailable")?;
let mut history = terminal_capture_history(&lease, &messages, started_at);
history.turn_ordinal = turn_ordinal;
memory_capture_worker()
.submit_terminal(
&lease,
history,
memory_context::RetentionClass::Standard,
provider,
)
.map_err(|_| "capture build failed")?;
Ok(())
}
#[cfg(any(test, feature = "testing"))]
pub fn capture_compaction_summary_for_harness(
&self,
first_turn_ordinal: u64,
last_turn_ordinal: u64,
source_digest: &str,
source_message_count: usize,
summary_text: &str,
) -> std::result::Result<(), &'static str> {
let summarized_at = std::time::SystemTime::now();
let lease = self
.memory_context_lock()
.capture_lease_at(summarized_at)
.ok_or("capture lease unavailable")?;
let source_digest = chat_capture::MessageRangeDigest::from_hex(source_digest)
.ok_or("invalid source digest")?;
let provider = self
.extension_capture_provider(&lease)
.ok_or("capture provider unavailable")?;
let source = chat_capture::CompactionSource {
schema: chat_capture::CompactionSchemaVersion::V1,
project_id: lease.project_id.clone(),
source_session_id: lease.session_id.clone(),
first_turn_ordinal,
last_turn_ordinal,
source_digest,
summary_origin: chat_capture::CompactionSummaryOrigin::LocalOnly,
prompt_stack_digest: chat_capture::PromptStackDigest::from_bytes([7; 32]),
redaction: chat_capture::RedactionPolicy::HostRedacted,
content_classes: vec![agent_core::compaction::ContentClass::UserText],
summarized_at,
};
memory_capture_worker()
.submit_summary(
&lease,
source,
source_message_count,
summary_text,
agent_core::compaction::RedactionPolicy::PolicyExclusions,
memory_context::RetentionClass::Standard,
provider,
)
.map_err(|_| "summary capture build failed")?;
Ok(())
}
#[cfg(any(test, feature = "testing"))]
pub fn cancel_memory_forwarding_for_harness(&self) {
self.clear_memory_contribution();
self.memory_context_disable();
}
#[cfg(any(test, feature = "testing"))]
pub fn extension_lease_count_for_harness(&self) -> usize {
self.extension_runtime
.as_ref()
.map_or(0, |manager| manager.lease_count())
}
pub(crate) fn inherit_memory_backend(&mut self, binding: crate::memory_backend::MemoryBinding) {
self.memory_backend = binding.fork_for_worker();
}
pub fn apply_memory_backend_config(&mut self, config: &crate::config::MemoryBackendConfig) {
if let Some(selected) = &self.memory_backend_config {
if selected != config {
self.memory_backend_reconfigure_denied
.store(true, std::sync::atomic::Ordering::SeqCst);
tracing::warn!("memory backend change denied: live extension processes cannot be reconfigured at runtime; original binding retained; restart required");
self.memory_context_disable();
self.clear_memory_contribution();
}
return;
}
if config.kind != crate::config::MemoryBackendKind::Legacy {
self.memory_context_disable();
}
self.memory_backend = crate::memory_backend::MemoryBinding::from_config_with_cwd(
config,
self.cwd.clone().or_else(|| std::env::current_dir().ok()),
);
self.memory_backend_config = Some(config.clone());
}
#[cfg(test)]
#[allow(dead_code)] pub(crate) fn memory_backend_for_test(&self) -> crate::memory_backend::MemoryBinding {
self.memory_backend.clone()
}
pub fn memory_backend_is_axel(&self) -> bool {
self.memory_backend.is_axel()
}
pub fn memory_backend_exclusive(&self) -> bool {
self.memory_backend.exclusive()
}
fn memory_context_project_id(&self) -> memory_context::ProjectId {
if self.memory_backend.exclusive() {
self.memory_backend
.scope()
.ok()
.and_then(|scope| memory_context::ProjectId::parse(scope.key()).ok())
.unwrap_or_else(|| {
memory_context::ProjectId::parse("project-unresolved").expect("static id")
})
} else {
memory_project_id()
}
}
pub fn memory_tool_capability(&self) -> Option<memory_context::MemoryContextCapability> {
if !self.memory_backend.exclusive() {
return None;
}
let runtime = self.clone();
Some(memory_context::MemoryContextCapability::control_only(
self.memory_context_state.clone(),
self.memory_context_project_id(),
memory_context::ContextProviderId::parse(axel_context::PROVIDER_ID).expect("static id"),
Arc::new(move || runtime.memory_context_disable()),
))
}
pub fn apply_config(&mut self, config: &crate::config::SynapsConfig) {
self.apply_config_inner(config, true);
}
pub(crate) fn apply_config_keep_tools(&mut self, config: &crate::config::SynapsConfig) {
self.apply_config_inner(config, false);
}
fn apply_config_inner(&mut self, config: &crate::config::SynapsConfig, disable_tools: bool) {
self.apply_memory_backend_config(&config.memory_backend);
if (self.memory_backend.exclusive() && !self.memory_backend.is_axel())
|| self
.memory_backend_reconfigure_denied
.load(std::sync::atomic::Ordering::SeqCst)
{
self.memory_context_disable();
self.clear_memory_contribution();
*self
.retained_recall_turn
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = None;
}
if let Some(ref model) = config.model {
self.set_model(model.clone());
}
if let Some(level) = config.thinking_level {
self.set_reasoning_level_explicit(level);
} else if let Some(budget) = config.thinking_budget {
self.set_thinking_budget_explicit(budget);
}
if let Some(clamp) = self.clamp_reasoning_to_model() {
tracing::warn!(
from = clamp.from.as_str(),
to = clamp.to.as_str(),
model = %self.model,
"configured reasoning level not supported by model; clamped"
);
}
self.context_window_override = config.context_window;
self.continuation
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.config = config.context_management;
self.compaction_model = config.compaction_model.clone();
self.compaction_mode = config.compaction_mode;
self.compaction_exclusions = config.compaction_exclude.clone();
self.max_tool_output = config.max_tool_output;
self.bash_timeout = config.bash_timeout;
self.bash_max_timeout = config.bash_max_timeout;
self.subagent_timeout = config.subagent_timeout;
self.api_retries = config.api_retries;
self.refusal_retries = config.refusal_retries;
self.telemetry_level =
crate::runtime::telemetry::TelemetryLevel::from_str_key(&config.telemetry);
self.sync_observability();
self.cache_diagnostics = config.cache_diagnostics;
self.cache_ttl = config.cache_ttl;
self.progressive_tool_disclosure = config.progressive_tool_disclosure;
self.deferred_tool_loading = config.tools_deferred_loading;
self.activation_confirm = config.tools_activation_confirm;
tracing::info!(
mode = config.tools_activation_confirm.as_str(),
auto_approve_confirms = config.server.auto_approve_confirms,
"tools.activation_confirm: model-initiated activate_tools policy"
);
self.trusted_worker_models = config
.favorite_models
.iter()
.filter_map(|model| {
let canonical = canonical_trusted_worker_model(model);
match agent_core::prompt::QualifiedModelId::parse(canonical) {
Ok(model) => Some(model),
Err(_) => {
tracing::warn!(
model = model.as_str(),
"ignoring invalid favorite worker-model identity"
);
None
}
}
})
.collect();
self.trusted_worker_models.sort();
self.trusted_worker_models.dedup();
self.subagent_limits = config.subagent;
if let Some(orchestration) = &self.orchestration {
for model in &self.trusted_worker_models {
if let Err(error) = orchestration.grant_worker_model(model.as_str()) {
tracing::warn!(
model = model.as_str(),
error = %error,
"failed to apply configured worker-model trust"
);
}
}
}
self.apply_auth_config(config);
if disable_tools && !config.disabled_tools.is_empty() {
if let Ok(mut reg) = self.tools.try_write() {
reg.disable(&config.disabled_tools);
}
}
}
pub fn apply_auth_config(&mut self, config: &crate::config::SynapsConfig) {
let new_source = config.auth.credential_source();
if new_source != self.credential_source {
self.token_cache.invalidate("anthropic");
}
self.credential_source = new_source;
if self.credential_source.is_remote() {
if let Ok(mut auth) = self.auth.try_write() {
AuthMethods::scrub_for_remote(&mut auth);
}
}
if let Ok(mut auth) = self.auth.try_write() {
if auth.auth_type == "oauth" && !auth.auth_token.is_empty() {
let selector = config
.auth
.account_selector(crate::auth::OAuthProviderId::Anthropic);
let check = crate::runtime::auth::check_binding(
&self.credential_source,
&selector,
auth.bound_credential.as_deref(),
crate::runtime::auth::RefreshPoint::TurnStart,
);
if check != crate::runtime::auth::BindingCheck::Matches {
AuthMethods::scrub_for_account_switch(&mut auth);
self.token_cache.invalidate("anthropic");
}
}
}
crate::auth::set_global_broker(crate::auth::broker_from_source(
&self.credential_source,
&self.token_cache,
self.client.clone(),
));
}
pub fn thinking_budget(&self) -> u32 {
self.thinking_budget
}
pub fn max_tool_output(&self) -> usize {
self.max_tool_output
}
pub fn bash_timeout(&self) -> u64 {
self.bash_timeout
}
pub fn bash_max_timeout(&self) -> u64 {
self.bash_max_timeout
}
pub fn subagent_timeout(&self) -> u64 {
self.subagent_timeout
}
pub fn api_retries(&self) -> u32 {
self.api_retries
}
pub fn set_max_tool_output(&mut self, v: usize) {
self.max_tool_output = v;
}
pub fn set_bash_timeout(&mut self, v: u64) {
self.bash_timeout = v;
}
pub fn set_bash_max_timeout(&mut self, v: u64) {
self.bash_max_timeout = v;
}
pub fn set_subagent_timeout(&mut self, v: u64) {
self.subagent_timeout = v;
}
pub(crate) fn apply_retry_config(&mut self, config: &crate::SynapsConfig) {
self.api_retries = config.api_retries;
self.refusal_retries = config.refusal_retries;
}
pub fn refusal_retries(&self) -> u32 {
self.refusal_retries
}
pub fn set_api_retries(&mut self, v: u32) {
self.api_retries = v;
}
pub fn telemetry_level(&self) -> crate::runtime::telemetry::TelemetryLevel {
self.telemetry_level
}
pub fn set_telemetry_level(&mut self, level: crate::runtime::telemetry::TelemetryLevel) {
self.telemetry_level = level;
self.sync_observability();
}
pub fn worker_telemetry(&self) -> crate::runtime::telemetry::WorkerTelemetry {
crate::runtime::telemetry::WorkerTelemetry {
level: self.telemetry_level,
writer: self.telemetry_writer.clone(),
}
}
pub fn inherit_telemetry(&mut self, parent: &crate::runtime::telemetry::WorkerTelemetry) {
match (&parent.writer, parent.level.enabled()) {
(Some(writer), true) => {
self.telemetry_level = parent.level;
self.trace_ctx = trace::TraceContext::with_sink(Arc::new(
crate::runtime::telemetry::WriterTraceSink::new(writer.clone()),
));
self.telemetry_writer = Some(writer.clone());
}
_ => self.set_telemetry_level(crate::runtime::telemetry::TelemetryLevel::Off),
}
}
fn sync_observability(&mut self) {
if self.telemetry_level.enabled() {
if self.telemetry_writer.is_none() {
let writer = crate::runtime::telemetry::TelemetryWriter::new(
crate::runtime::telemetry::WriterOptions::default(),
);
self.trace_ctx = trace::TraceContext::with_sink(Arc::new(
crate::runtime::telemetry::WriterTraceSink::new(writer.clone()),
));
self.telemetry_writer = Some(writer);
}
} else {
self.telemetry_writer = None;
self.trace_ctx = trace::TraceContext::disabled();
}
}
pub fn trace_context(&self) -> trace::TraceContext {
self.trace_ctx.clone()
}
pub(crate) fn effective_trace_context(&self) -> trace::TraceContext {
match self.trace_controls.consume() {
None => self.trace_ctx.clone(),
Some(with_content) => {
let base = if self.trace_ctx.enabled() {
self.trace_ctx.clone()
} else {
let writer = crate::runtime::telemetry::TelemetryWriter::new(
crate::runtime::telemetry::WriterOptions::default(),
);
*self
.one_shot_trace_writer
.lock()
.expect("one-shot trace writer lock poisoned") = Some(writer.clone());
self.trace_ctx
.fork_with_sink(Arc::new(crate::runtime::telemetry::WriterTraceSink::new(
writer,
)))
.with_one_shot_request_gate()
};
if with_content {
base.with_content_capture(Arc::new(trace::ContentCapture::new(
self.capture_dir.clone(),
)))
} else {
base
}
}
}
}
pub fn trace_arm_next(&self, with_content: bool) {
self.trace_controls.arm_next(with_content);
}
pub fn trace_status(&self) -> trace::TraceStatusReport {
let _ = trace::sweep_expired_captures(&self.capture_dir);
trace::TraceStatusReport {
persistent_enabled: self.telemetry_writer.is_some(),
arm: self.trace_controls.peek(),
trace_path: crate::runtime::telemetry::default_trace_log_path(),
writer_stats: self.telemetry_writer.as_ref().map(|w| w.stats()),
degraded_records: self.trace_ctx.degraded_records(),
}
}
fn memory_context_lock(&self) -> std::sync::MutexGuard<'_, memory_context::SessionMemoryState> {
self.memory_context_state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub fn memory_context_status(&self) -> memory_context::MemoryContextStatus {
self.memory_context_lock().status()
}
pub fn memory_context_disable(&self) -> memory_context::MemoryContextStatus {
let (status, bound, session) = {
let mut state = self.memory_context_lock();
let session = state.session_id().clone();
let bound = state.bound_provider_ids();
let status = match memory_context::apply_memory_context_action(
&mut state,
memory_context::AuthorizedMemoryAction::Disable {
session: session.clone(),
},
) {
Ok(status) => status,
Err(_) => state.status(),
};
(status, bound, session)
};
memory_context::emit_memory_observability_event(
&memory_context::MemoryObservabilityEvent::context_disabled(
&session,
&self.memory_context_project_id(),
),
);
self.revoke_memory_provider_leases(&bound);
status
}
fn revoke_memory_provider_leases(&self, bound: &[memory_context::ContextProviderId]) {
let Some(extension_runtime) = &self.extension_runtime else {
return;
};
for provider in bound {
let mut parts = provider.as_str().splitn(3, ':');
if let (Some("extension"), Some(plugin), Some(_local_id)) =
(parts.next(), parts.next(), parts.next())
{
if !plugin.is_empty() {
extension_runtime.revoke_plugin_lease(&self.host_tool_session, plugin);
}
}
}
}
fn resolve_memory_provider(
&self,
requested: Option<&str>,
) -> std::result::Result<memory_context::ContextProviderId, memory_context::MemoryContextError>
{
use crate::extensions::context_provider as ext_cp;
if memory_context::provider_unavailable_for_test() {
return Err(memory_context::MemoryContextError::ProviderNotRegistered);
}
if (self.memory_backend.exclusive() && !self.memory_backend.is_axel())
|| self
.memory_backend_reconfigure_denied
.load(std::sync::atomic::Ordering::SeqCst)
{
return Err(memory_context::MemoryContextError::ProviderNotRegistered);
}
if self.memory_backend.is_axel() {
self.memory_backend
.validate_axel_config()
.map_err(|_| memory_context::MemoryContextError::ProviderNotRegistered)?;
if requested.is_some_and(|id| id != axel_context::PROVIDER_ID) {
return Err(memory_context::MemoryContextError::ProviderNotRegistered);
}
return memory_context::ContextProviderId::parse(axel_context::PROVIDER_ID);
}
let Some(extension_runtime) = &self.extension_runtime else {
return Ok(memory_provider_id());
};
let requested_id = match requested {
None => None,
Some(raw) => Some(
ext_cp::ContextProviderId::parse(raw)
.map_err(|_| memory_context::MemoryContextError::ProviderNotRegistered)?,
),
};
match extension_runtime.resolve_context_provider(requested_id.as_ref()) {
Ok(descriptor) => {
memory_context::ContextProviderId::parse(&descriptor.runtime_address())
}
Err(ext_cp::ContextProviderLookupError::NotRegistered) => {
Err(memory_context::MemoryContextError::ProviderNotRegistered)
}
Err(ext_cp::ContextProviderLookupError::Ambiguous) => {
Err(memory_context::MemoryContextError::ProviderAmbiguous)
}
}
}
pub(crate) fn memory_context_enable(
&self,
mode: memory_context::MemoryContextMode,
proof: memory_context::UserIntentProof,
) -> std::result::Result<memory_context::MemoryContextStatus, memory_context::MemoryContextError>
{
self.memory_context_enable_resolved(mode, proof, None)
}
pub(crate) fn memory_context_enable_resolved(
&self,
mode: memory_context::MemoryContextMode,
proof: memory_context::UserIntentProof,
requested_provider: Option<&str>,
) -> std::result::Result<memory_context::MemoryContextStatus, memory_context::MemoryContextError>
{
let mut state = self.memory_context_lock();
let lease = self.grant_command_memory_lease(&state, mode, proof, requested_provider)?;
let enabled_event = memory_context::MemoryObservabilityEvent::context_enabled(&lease);
let result = memory_context::apply_memory_context_action(
&mut state,
memory_context::AuthorizedMemoryAction::Enable { lease },
);
if result.is_ok() {
memory_context::emit_memory_observability_event(&enabled_event);
}
result
}
pub(crate) fn apply_memory_default_mode(
&self,
config: &agent_core::config::MemoryConfig,
) -> Option<
std::result::Result<
memory_context::MemoryContextStatus,
memory_context::MemoryContextError,
>,
> {
use memory_context::MemoryContextMode as Mode;
if !config.default_mode_confirmed {
return None;
}
let mode = match config.default_mode.as_str() {
"recall_once" => Mode::RecallOnce,
"recall_each_prompt" => Mode::RecallEachPrompt,
"capture_only" => Mode::CaptureOnly,
"capture_and_recall" => Mode::CaptureAndRecall,
_ => return None,
};
if self.memory_context_status().durable != memory_context::DurableStatus::Off {
return None;
}
let proof = memory_context::UserIntentProof::OperatorConfig;
let result = if mode == Mode::RecallOnce {
self.memory_context_recall_once(proof)
} else {
self.memory_context_enable(mode, proof)
};
match &result {
Ok(_) => tracing::info!(
mode = mode.as_str(),
"memory.default_mode applied to session"
),
Err(e) => tracing::warn!(
mode = mode.as_str(),
error = %e,
"memory.default_mode not applied; session starts with memory off"
),
}
Some(result)
}
pub(crate) fn restore_session_memory_mode(&self, mode: crate::session::SessionMemoryMode) {
let Some(lease) = mode.lease_mode() else {
return;
};
if let Err(e) =
self.memory_context_enable(lease, memory_context::mint_explicit_command_proof())
{
tracing::warn!(
mode = lease.as_str(),
error = %e,
"session memory mode not restored; memory stays off"
);
}
}
pub(crate) fn memory_context_recall_once(
&self,
proof: memory_context::UserIntentProof,
) -> std::result::Result<memory_context::MemoryContextStatus, memory_context::MemoryContextError>
{
let mut state = self.memory_context_lock();
let lease = self.grant_command_memory_lease(
&state,
memory_context::MemoryContextMode::RecallOnce,
proof,
None,
)?;
let enabled_event = memory_context::MemoryObservabilityEvent::context_enabled(&lease);
let result = memory_context::apply_memory_context_action(
&mut state,
memory_context::AuthorizedMemoryAction::RecallOnce { lease },
);
if result.is_ok() {
memory_context::emit_memory_observability_event(&enabled_event);
}
result
}
fn grant_command_memory_lease(
&self,
state: &memory_context::SessionMemoryState,
mode: memory_context::MemoryContextMode,
proof: memory_context::UserIntentProof,
requested_provider: Option<&str>,
) -> std::result::Result<memory_context::MemoryContextLease, memory_context::MemoryContextError>
{
let provider_id = self.resolve_memory_provider(requested_provider)?;
memory_context::MemoryContextLease::grant(
memory_context::MemoryLeaseId::parse(&format!("memctx-cmd-{}", uuid::Uuid::new_v4()))?,
state.session_id().clone(),
self.memory_context_project_id(),
provider_id,
mode,
memory_context::CapturePolicy::default(),
memory_context::RecallPolicy::default(),
proof,
std::time::SystemTime::now(),
None,
)
}
#[cfg(test)]
pub(crate) fn set_capture_provider_for_test(
&self,
provider: std::sync::Arc<dyn capture_worker::CaptureProvider>,
) {
*self
.capture_provider_for_test
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(provider);
}
pub(crate) fn compaction_capture_lease_at(
&self,
now: std::time::SystemTime,
) -> Option<memory_context::MemoryContextLease> {
self.memory_context_lock().capture_lease_at(now)
}
pub(crate) fn submit_compaction_summary_capture(
&self,
capture_lease: Option<memory_context::MemoryContextLease>,
current: &agent_core::session::Session,
api_messages: &[crate::SharedMessage],
outcome: &compaction::CompactionOutcome,
summarized_at: std::time::SystemTime,
) {
let Some(lease) = capture_lease else {
return;
};
let Ok(source_session_id) = memory_context::SessionId::parse(¤t.id) else {
return;
};
if api_messages.is_empty() {
return;
}
let first_turn_ordinal = 0;
let last_turn_ordinal = (api_messages.len() - 1) as u64;
let Some(source_digest) = chat_capture::MessageRangeDigest::from_hex(
&agent_core::compaction::message_range_digest(api_messages),
) else {
return;
};
let Some(prompt_stack_digest) =
chat_capture::PromptStackDigest::from_hex(&outcome.prompt_stack_digest)
else {
return;
};
let summary_origin = if outcome.local_only {
chat_capture::CompactionSummaryOrigin::LocalOnly
} else {
chat_capture::CompactionSummaryOrigin::Provider {
provider_id: outcome.summary_provider.clone(),
model_id: outcome.summary_model.clone(),
}
};
let content_classes = outcome.included_classes.clone();
let source = chat_capture::CompactionSource {
schema: chat_capture::CompactionSchemaVersion::V1,
project_id: lease.project_id.clone(),
source_session_id,
first_turn_ordinal,
last_turn_ordinal,
source_digest,
summary_origin,
prompt_stack_digest,
redaction: match outcome.redaction_policy {
agent_core::compaction::RedactionPolicy::TruncationOnly => {
chat_capture::RedactionPolicy::None
}
agent_core::compaction::RedactionPolicy::PolicyExclusions => {
chat_capture::RedactionPolicy::HostRedacted
}
},
content_classes,
summarized_at,
};
#[cfg(any(test, feature = "testing"))]
let test_provider = self
.capture_provider_for_test
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.clone();
#[cfg(not(any(test, feature = "testing")))]
let test_provider: Option<std::sync::Arc<dyn capture_worker::CaptureProvider>> = None;
let provider = test_provider.or_else(|| self.extension_capture_provider(&lease));
if let Some(provider) = provider {
let _ = memory_capture_worker().submit_summary(
&lease,
source,
api_messages.len(),
&outcome.summary_text,
outcome.redaction_policy,
memory_context::RetentionClass::Standard,
provider,
);
}
}
fn extension_capture_provider(
&self,
lease: &memory_context::MemoryContextLease,
) -> Option<std::sync::Arc<dyn capture_worker::CaptureProvider>> {
if (self.memory_backend.exclusive() && !self.memory_backend.is_axel())
|| self
.memory_backend_reconfigure_denied
.load(std::sync::atomic::Ordering::SeqCst)
{
return None;
}
if self.memory_backend.is_axel() {
if lease.provider_id.as_str() != axel_context::PROVIDER_ID
|| lease.project_id != self.memory_context_project_id()
{
return None;
}
return Some(Arc::new(axel_context::AxelCaptureProvider {
binding: self.memory_backend.clone(),
state: self.memory_context_state.clone(),
lease: lease.clone(),
}));
}
let manager = self.extension_runtime.clone()?;
let mut parts = lease.provider_id.as_str().splitn(3, ':');
let (Some("extension"), Some(plugin), Some(_)) = (parts.next(), parts.next(), parts.next())
else {
return None;
};
let digest =
manager.declared_tool_digest(plugin, memory_context::MEMORY_CAPTURE_TOOL_NAME)?;
let handle = tokio::runtime::Handle::try_current().ok()?;
Some(std::sync::Arc::new(ExtensionCaptureProvider {
manager,
session: self.host_tool_session.clone(),
plugin: plugin.to_owned(),
digest,
handle,
}))
}
pub fn memory_history_preview(
&self,
) -> std::result::Result<memory_history::HistoryImportPreview, memory_history::HistoryImportError>
{
let host = memory_history::HistoryImportHostState::from_current_host()?;
let mut io = memory_history::CanonicalHistoryMetadataIo::new();
let preview = memory_history::preview_history_import(&host, &mut io)?;
*self
.pending_history_import_preview
.lock()
.expect("history preview mutex poisoned") = Some(preview.clone());
Ok(preview)
}
pub(crate) fn memory_history_confirm(
&self,
) -> std::result::Result<memory_history::HistoryImportReport, memory_history::HistoryImportError>
{
let preview = self
.pending_history_import_preview
.lock()
.expect("history preview mutex poisoned")
.take()
.ok_or(memory_history::HistoryImportError::ConsentRequired)?;
let intent = memory_history::ConfirmedUserIntent::from_explicit_command(
memory_context::mint_explicit_command_id(),
);
let mut io = memory_history::CanonicalHistoryMetadataIo::new();
match memory_history::authorize_history_import(
preview,
memory_history::HistoryImportConsent::Confirmed(intent),
memory_history::RequestAuthority::UserFrontend,
&mut io,
)? {
memory_history::HistoryImportOutcome::Ready(plan) => {
*self
.history_import_plan
.lock()
.expect("history plan mutex poisoned") = Some(plan.clone());
let lease = self
.memory_context_lock()
.capture_lease_at(std::time::SystemTime::now())
.ok_or(memory_history::HistoryImportError::CaptureLeaseUnavailable)?;
let provider = self
.extension_capture_provider(&lease)
.ok_or(memory_history::HistoryImportError::CaptureProviderUnavailable)?;
let sessions_dir = agent_core::config::get_active_config_dir().join("sessions");
memory_history::import_history_from_dir(
&plan,
&lease,
&sessions_dir,
memory_history::IMPORT_BATCH_MAX_RECORDS,
memory_capture_worker(),
provider,
)
}
memory_history::HistoryImportOutcome::Declined => {
Err(memory_history::HistoryImportError::ConsentRequired)
}
}
}
pub(crate) fn memory_history_decline(&self) -> bool {
self.pending_history_import_preview
.lock()
.expect("history preview mutex poisoned")
.take()
.is_some()
}
#[cfg(test)]
pub(crate) fn memory_durable_proof_for_test(&self) -> Option<memory_context::UserIntentProof> {
self.memory_context_lock().durable_proof().cloned()
}
#[cfg(test)]
pub(crate) fn memory_one_shot_proof_for_test(&self) -> Option<memory_context::UserIntentProof> {
self.memory_context_lock().one_shot_pending_proof().cloned()
}
#[cfg(test)]
pub(crate) fn memory_bound_providers_for_test(&self) -> Vec<memory_context::ContextProviderId> {
self.memory_context_lock().bound_provider_ids()
}
pub fn context_report(&self, history: Option<&[crate::SharedMessage]>) -> trace::ContextReport {
use trace::ReportValue;
let (history_messages, history_bytes) = match history {
Some(msgs) => (
ReportValue::Count(msgs.len() as u64),
ReportValue::Count(
msgs.iter()
.map(|m| {
serde_json::to_vec(&**m)
.map(|v| v.len() as u64)
.unwrap_or(0)
})
.sum(),
),
),
None => (ReportValue::Unavailable, ReportValue::Unavailable),
};
let tool_count = match self.tools.try_read() {
Ok(tools) => ReportValue::Count(tools.iter_tools_sorted().len() as u64),
Err(_) => ReportValue::Unavailable,
};
trace::ContextReport {
model: self.model.clone(),
system_prompt_bytes: ReportValue::Count(
self.system_prompt().map(|s| s.len() as u64).unwrap_or(0),
),
tool_count,
history_messages,
history_bytes,
loaded_skills: ReportValue::Unavailable,
loaded_memories: ReportValue::Unavailable,
cache: self.trace_ctx.cache_snapshots().last_activity(),
trace_enabled: self.trace_ctx.enabled(),
writer_stats: self.telemetry_writer.as_ref().map(|w| w.stats()),
degraded_records: self.trace_ctx.degraded_records(),
}
}
pub fn telemetry_writer(&self) -> Option<crate::runtime::telemetry::TelemetryWriter> {
self.telemetry_writer.clone()
}
pub fn shutdown_observability(
&self,
timeout: std::time::Duration,
) -> Option<crate::runtime::telemetry::ShutdownOutcome> {
let _ = trace::sweep_expired_captures(&self.capture_dir);
let ephemeral = self
.one_shot_trace_writer
.lock()
.expect("one-shot trace writer lock poisoned")
.take();
let ephemeral_outcome = ephemeral.map(|w| w.shutdown(timeout));
self.telemetry_writer
.as_ref()
.map(|w| w.shutdown(timeout))
.or(ephemeral_outcome)
}
pub async fn shutdown_observability_async(
&self,
timeout: std::time::Duration,
) -> Option<crate::runtime::telemetry::ShutdownOutcome> {
let capture_dir = self.capture_dir.clone();
let _ =
tokio::task::spawn_blocking(move || trace::sweep_expired_captures(&capture_dir)).await;
let ephemeral = self
.one_shot_trace_writer
.lock()
.expect("one-shot trace writer lock poisoned")
.take();
let ephemeral_outcome = match ephemeral {
Some(writer) => Some(writer.shutdown_async(timeout).await),
None => None,
};
match self.telemetry_writer.clone() {
Some(writer) => Some(writer.shutdown_async(timeout).await),
None => ephemeral_outcome,
}
}
#[cfg(any(test, feature = "testing"))]
pub fn install_observability_for_tests(
&mut self,
writer: crate::runtime::telemetry::TelemetryWriter,
) {
self.trace_ctx = trace::TraceContext::with_sink(Arc::new(
crate::runtime::telemetry::WriterTraceSink::new(writer.clone()),
));
self.telemetry_writer = Some(writer);
}
pub fn cache_diagnostics(&self) -> bool {
self.cache_diagnostics
}
pub fn set_cache_diagnostics(&mut self, v: bool) {
self.cache_diagnostics = v;
}
pub fn cache_ttl(&self) -> crate::core::config::CacheTtl {
self.cache_ttl
}
pub fn share_ttl_latches(&mut self, parent: &Runtime) {
self.ttl_downgrade_notified = parent.ttl_downgrade_notified.clone();
self.saw_1h_honored = parent.saw_1h_honored.clone();
self.cache_anchor = parent.cache_anchor.clone();
self.last_msg_id = parent.last_msg_id.clone();
self.cache_diag_rejected = parent.cache_diag_rejected.clone();
}
pub(crate) fn cache_diagnosis(&self) -> Option<cache_diagnosis::CacheDiagnosis> {
self.cache_diagnostics.then(|| {
cache_diagnosis::CacheDiagnosis::new(
Arc::clone(&self.last_msg_id),
Arc::clone(&self.cache_diag_rejected),
)
})
}
pub fn set_cache_ttl(&mut self, ttl: crate::core::config::CacheTtl) {
self.cache_ttl = ttl;
}
pub fn thinking_level(&self) -> &str {
match self.named_level {
Some(level) => level.as_str(),
None => crate::core::models::thinking_level_for_budget(self.thinking_budget),
}
}
pub async fn refresh_if_needed(&self) -> Result<()> {
self.refresh_if_needed_for_model(&self.model).await
}
pub async fn reload_auth(&self) -> Result<String> {
AuthMethods::reload_after_rejection(
Arc::clone(&self.auth),
&self.client,
&self.credential_source,
&self.token_cache,
None,
)
.await?;
let expires = self.auth.read().await.token_expires;
let source = if self.credential_source.is_remote() {
"remote broker"
} else {
"auth.json"
};
Ok(match expires {
Some(exp) => {
let mins = exp.saturating_sub(crate::epoch_millis()) / 60_000;
format!("Anthropic credentials reloaded from {source} (valid ~{mins} min)")
}
None => format!("Anthropic credentials reloaded from {source}"),
})
}
async fn refresh_if_needed_for_model(&self, model: &str) -> Result<()> {
if !crate::runtime::auth::model_is_anthropic(model) {
return Ok(());
}
AuthMethods::refresh_if_needed(
Arc::clone(&self.auth),
&self.client,
&self.credential_source,
&self.token_cache,
Some(model),
crate::runtime::auth::RefreshPoint::TurnStart,
)
.await
}
pub async fn compact_call(&self, messages: Vec<crate::SharedMessage>) -> Result<String> {
self.remote_summarization_attempts
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let model = self.compaction_model();
self.validate_request_preflight_for(
model,
crate::runtime::openai::catalog::CodexRequestRole::Internal,
)
.await?;
self.refresh_if_needed_for_model(model).await?;
use crate::runtime::compaction::COMPACTION_SYSTEM_PROMPT;
ApiMethods::call_api_simple(
&self.auth,
&self.client,
model,
COMPACTION_SYSTEM_PROMPT,
self.thinking_budget,
self.reasoning_level(),
&messages,
self.api_retries,
&api::ApiOptions {
trace: self.trace_ctx.clone(),
telemetry: self.telemetry_writer.clone(),
..api::ApiOptions::default()
},
)
.await
}
pub async fn run_single(&self, prompt: &str) -> Result<String> {
if self
.continuation
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.durability_blocked
{
return Err(RuntimeError::Session(
"context head save is unresolved; reload the session before further inference"
.into(),
));
}
self.validate_request_preflight().await?;
let anthropic_execution_plan = self.authorized_anthropic_plan().await?;
self.refresh_if_needed().await?;
let mut messages: Vec<crate::SharedMessage> = vec![std::sync::Arc::new(
json!({"role": "user", "content": prompt}),
)];
let system_prompt = self.effective_system_prompt().await;
loop {
let response = ApiMethods::call_api(
&self.auth,
&self.client,
&self.model,
&*self.tools.read().await,
&system_prompt,
self.thinking_budget,
self.reasoning_level(),
&messages,
self.api_retries,
&api::ApiOptions {
memory_backend: None,
use_1m_context: self.context_window_override == Some(1_000_000),
cache_ttl: self.cache_ttl,
ttl_downgrade_notified: self.ttl_downgrade_notified.clone(),
saw_1h_honored: self.saw_1h_honored.clone(),
credential_source: self.credential_source.clone(),
token_cache: self.token_cache.clone(),
anthropic_base_url: None,
anthropic_stream_client: self.anthropic_stream_client.clone(),
anthropic_execution_plan: anthropic_execution_plan.clone(),
codex_request_role: self.codex_request_role(),
trace: self.effective_trace_context(),
request_correlation: None,
suppress_stream_deltas: true,
telemetry: self.telemetry_writer.clone(),
tool_session_id: Some(self.host_tool_session.clone()),
session_tool_set: None,
request_tools_schema: None,
anthropic_tool_split: None,
usage_counters: None,
cache_anchor: None,
keepwarm: None,
cache_diagnosis: None,
},
)
.await?;
if let Some(content) = response["content"].as_array() {
let mut response_text = String::new();
let mut tool_uses = Vec::new();
for item in content {
match item["type"].as_str() {
Some("text") => {
if let Some(text) = item["text"].as_str() {
response_text.push_str(text);
}
}
Some("tool_use") => {
tool_uses.push(item.clone());
}
_ => {}
}
}
if tool_uses.is_empty() {
if let Some(orchestration) = &self.orchestration {
if let agent_core::orchestration::CompletionGate::Blocked { workers } =
orchestration.completion_gate()
{
return Err(crate::RuntimeError::Tool(format!(
"completion blocked: {} worker(s) require collection/reconciliation: {} (call subagent_collect with reconciled=true after inspecting each result)",
workers.len(),
workers.join(", ")
)));
}
}
return Ok(response_text);
}
messages.push(std::sync::Arc::new(json!({
"role": "assistant",
"content": content
})));
let mut tool_results = Vec::new();
if tool_uses.len() == 1 {
let tool_use = &tool_uses[0];
if let (Some(tool_name), Some(tool_id)) =
(tool_use["name"].as_str(), tool_use["id"].as_str())
{
let input = &tool_use["input"];
let (result, rich_blocks) = match self.tools.read().await.get(tool_name).cloned() {
Some(tool) => {
let input = self
.tools
.read()
.await
.translate_input_for_api_tool(tool_name, input.clone());
let runtime_name = self
.tools
.read()
.await
.runtime_name_for_api(tool_name)
.to_string();
let ctx = crate::ToolContext {
channels: crate::tools::ToolChannels {
tx_delta: None,
tx_events: None,
},
capabilities: crate::tools::ToolCapabilities {
session_allow_all: Some(self.session_allow_all.clone()),
launch_cancel: None,
memory_backend: Some(self.memory_backend.clone()),
telemetry: Some(self.worker_telemetry()),
watcher_exit_path: self.watcher_exit_path.clone(),
tool_register_tx: None,
session_manager: Some(self.session_manager.clone()),
subagent_registry: Some(self.subagent_registry.clone()),
delegation_parent: None,
codex_parent_plan: Self::codex_delegation_plan(
&self.model,
self.reasoning_level(),
self.codex_request_role(),
),
event_queue: Some(self.event_queue.clone()),
secret_prompt: None,
orchestration: self.orchestration.clone(),
tool_activation: None,
mcp_leases: None,
extension_leases: None,
memory_context: self.memory_tool_capability(),
cwd: self.cwd.clone(),
env: self.env.clone(),
env_stripped: self.env_stripped.clone(),
env_warned: self.env_warned.clone(),
session_id: self.session_id.clone(),
},
limits: crate::tools::ToolLimits {
max_tool_output: self.max_tool_output,
max_tool_buffer: 256 * 1024,
bash_timeout: self.bash_timeout,
bash_max_timeout: self.bash_max_timeout,
subagent_timeout: self.subagent_timeout,
},
};
let decision = resolve_before_tool_call_decision(
input.clone(),
emit_before_tool_call(
&self.hook_bus,
tool_name,
Some(&runtime_name),
input.clone(),
self.session_id.as_deref(),
)
.await,
None,
false,
Some(&self.session_allow_all),
)
.await;
if let BeforeToolCallDecision::Block { reason } = decision {
(format!("Tool call blocked by extension: {}", reason), None)
} else {
let BeforeToolCallDecision::Continue { input } = decision
else {
unreachable!()
};
let input_for_hook = input.clone();
let (output, rich_blocks) = match tool.execute_rich(input, ctx).await {
Ok(output) => validated_single_tool_output(&self.model, output),
Err(e) => (e.to_string(), None),
};
let hooked_output = emit_after_tool_call(
&self.hook_bus,
tool_name,
Some(&runtime_name),
input_for_hook,
output.clone(),
self.max_tool_output,
self.session_id.as_deref(),
)
.await;
let rich_blocks = retain_single_tool_blocks(rich_blocks, &output, &hooked_output);
(hooked_output, rich_blocks)
}
}
None => (format!("Unknown tool: {}", tool_name), None),
};
tool_results.push(json!({
"type": "tool_result",
"tool_use_id": tool_id,
"content": single_tool_result_content(&result, rich_blocks, self.max_tool_output)
}));
}
} else {
let mut join_set = tokio::task::JoinSet::new();
let cfg_max_tool_output = self.max_tool_output;
let cfg_bash_timeout = self.bash_timeout;
let cfg_bash_max_timeout = self.bash_max_timeout;
let cfg_subagent_timeout = self.subagent_timeout;
let session_mgr = self.session_manager.clone();
let cfg_subagent_registry = self.subagent_registry.clone();
let cfg_event_queue = self.event_queue.clone();
let cfg_hook_bus = self.hook_bus.clone();
let cfg_orchestration = self.orchestration.clone();
let codex_parent_plan = Self::codex_delegation_plan(
&self.model,
self.reasoning_level(),
self.codex_request_role(),
);
let cfg_cwd = self.cwd.clone();
let cfg_env = self.env.clone();
let cfg_session_id = self.session_id.clone();
for tool_use in &tool_uses {
if let (Some(tool_name), Some(tool_id)) = (
tool_use["name"].as_str().map(|s| s.to_string()),
tool_use["id"].as_str().map(|s| s.to_string()),
) {
let input = tool_use["input"].clone();
let tools_snapshot = self.tools.read().await;
let input =
tools_snapshot.translate_input_for_api_tool(&tool_name, input);
let runtime_name =
tools_snapshot.runtime_name_for_api(&tool_name).to_string();
let tool = tools_snapshot.get(&tool_name).cloned();
drop(tools_snapshot);
let exit_path = self.watcher_exit_path.clone();
let session_mgr_inner = session_mgr.clone();
let registry_inner = cfg_subagent_registry.clone();
let event_queue_inner = cfg_event_queue.clone();
let hook_bus_inner = cfg_hook_bus.clone();
let session_allow_all_inner = self.session_allow_all.clone();
let orchestration_inner = cfg_orchestration.clone();
let memory_backend_inner = self.memory_backend.clone();
let worker_telemetry_inner = self.worker_telemetry();
let memory_context_inner = self.memory_tool_capability();
let codex_parent_plan_inner = codex_parent_plan.clone();
let cwd_inner = cfg_cwd.clone();
let env_inner = cfg_env.clone();
let env_stripped_inner = self.env_stripped.clone();
let env_warned_inner = self.env_warned.clone();
let session_id_inner = cfg_session_id.clone();
let attachment_model = self.model.clone();
let tool_name_for_hook = tool_name.clone();
let runtime_name_for_hook = runtime_name.clone();
join_set.spawn(async move {
let result = match tool {
Some(t) => {
let decision =
crate::runtime::resolve_before_tool_call_decision(
input.clone(),
crate::runtime::emit_before_tool_call(
&hook_bus_inner,
&tool_name_for_hook,
Some(&runtime_name_for_hook),
input.clone(),
session_id_inner.as_deref(),
)
.await,
None,
false,
Some(&session_allow_all_inner),
)
.await;
if let crate::runtime::BeforeToolCallDecision::Block {
reason,
} = decision
{
(format!("Tool call blocked by extension: {}", reason), None)
} else {
let crate::runtime::BeforeToolCallDecision::Continue {
input,
} = decision
else {
unreachable!()
};
let ctx = crate::ToolContext {
channels: crate::tools::ToolChannels {
tx_delta: None,
tx_events: None,
},
capabilities: crate::tools::ToolCapabilities {
session_allow_all: Some(session_allow_all_inner.clone()),
launch_cancel: None,
memory_backend: Some(memory_backend_inner),
telemetry: Some(worker_telemetry_inner),
watcher_exit_path: exit_path,
tool_register_tx: None,
session_manager: Some(session_mgr_inner),
subagent_registry: Some(registry_inner),
delegation_parent: None,
codex_parent_plan: codex_parent_plan_inner,
event_queue: Some(event_queue_inner),
secret_prompt: None,
orchestration: orchestration_inner,
tool_activation: None,
mcp_leases: None,
extension_leases: None,
memory_context: memory_context_inner,
cwd: cwd_inner,
env: env_inner,
env_stripped: env_stripped_inner,
env_warned: env_warned_inner,
session_id: session_id_inner.clone(),
},
limits: crate::tools::ToolLimits {
max_tool_output: cfg_max_tool_output,
max_tool_buffer: 256 * 1024,
bash_timeout: cfg_bash_timeout,
bash_max_timeout: cfg_bash_max_timeout,
subagent_timeout: cfg_subagent_timeout,
},
};
let input_for_hook = input.clone();
let (output, rich_blocks) =
match t.execute_rich(input, ctx).await {
Ok(output) => validated_single_tool_output(
&attachment_model,
output,
),
Err(e) => (e.to_string(), None),
};
let hooked_output =
crate::runtime::emit_after_tool_call(
&hook_bus_inner,
&tool_name_for_hook,
Some(&runtime_name_for_hook),
input_for_hook,
output.clone(),
cfg_max_tool_output,
session_id_inner.as_deref(),
)
.await;
let rich_blocks = retain_single_tool_blocks(
rich_blocks,
&output,
&hooked_output,
);
(hooked_output, rich_blocks)
}
}
None => (format!("Unknown tool: {}", tool_name), None),
};
(tool_id, result)
});
}
}
let mut results_map = std::collections::HashMap::new();
while let Some(res) = join_set.join_next().await {
match res {
Ok((tool_id, result)) => {
results_map.insert(tool_id, result);
}
Err(e) => {
tracing::error!("Parallel tool task panicked: {}", e);
}
}
}
for tool_use in &tool_uses {
if let Some(tool_id) = tool_use["id"].as_str() {
let (result, rich_blocks) = results_map.remove(tool_id).unwrap_or_else(|| {
("Tool execution failed: task panicked".to_string(), None)
});
tool_results.push(json!({
"type": "tool_result",
"tool_use_id": tool_id,
"content": single_tool_result_content(&result, rich_blocks, self.max_tool_output)
}));
}
}
}
messages.push(std::sync::Arc::new(json!({
"role": "user",
"content": tool_results
})));
} else {
return Err(RuntimeError::Tool("Invalid response format".to_string()));
}
}
}
pub async fn run_stream(
&self,
prompt: String,
cancel: CancellationToken,
) -> Pin<Box<dyn Stream<Item = StreamEvent> + Send>> {
self.run_stream_with_messages(
vec![std::sync::Arc::new(
json!({"role": "user", "content": prompt}),
)],
cancel,
None,
None,
false,
)
.await
}
pub fn session_allow_all(&self) -> &std::sync::Arc<crate::tools::SessionConfirmLatch> {
&self.session_allow_all
}
pub(crate) fn inherit_session_allow_all(
&mut self,
parent: Option<&std::sync::Arc<crate::tools::SessionConfirmLatch>>,
) {
if let Some(parent) = parent {
self.session_allow_all = parent.clone();
}
}
pub async fn run_stream_with_messages(
&self,
messages: Vec<crate::SharedMessage>,
cancel: CancellationToken,
steering_rx: Option<mpsc::UnboundedReceiver<String>>,
secret_prompt: Option<crate::tools::SecretPromptHandle>,
auto_approve_confirms: bool,
) -> Pin<Box<dyn Stream<Item = StreamEvent> + Send>> {
self.run_stream_tracked(
messages,
cancel,
steering_rx,
secret_prompt,
auto_approve_confirms,
TurnCompletion::new(),
)
.await
}
pub async fn run_stream_tracked(
&self,
messages: Vec<crate::SharedMessage>,
cancel: CancellationToken,
steering_rx: Option<mpsc::UnboundedReceiver<String>>,
secret_prompt: Option<crate::tools::SecretPromptHandle>,
auto_approve_confirms: bool,
completion: TurnCompletion,
) -> Pin<Box<dyn Stream<Item = StreamEvent> + Send>> {
let (tx, internal_rx) = mpsc::unbounded_channel();
let bounded_rx =
crate::runtime::relay::spawn_bounded_stream_relay(internal_rx, cancel.clone());
let turn_correlation_id = agent_core::next_turn_correlation_id();
if self
.continuation
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.durability_blocked
{
let error = RuntimeError::Session(
"context head save is unresolved; reload the session before further inference"
.into(),
);
let _ = tx.send(StreamEvent::Session(SessionEvent::Error(
helpers::turn_error_for(&error, &turn_correlation_id),
)));
let _ = tx.send(StreamEvent::Session(SessionEvent::Done));
return Box::pin(tokio_stream::wrappers::ReceiverStream::new(bounded_rx));
}
if let Err(error) = self.validate_request_preflight().await {
let _ = tx.send(StreamEvent::Session(SessionEvent::Error(
helpers::turn_error_for(&error, &turn_correlation_id),
)));
let _ = tx.send(StreamEvent::Session(SessionEvent::Done));
return Box::pin(tokio_stream::wrappers::ReceiverStream::new(bounded_rx));
}
let anthropic_execution_plan = match self.authorized_anthropic_plan().await {
Ok(plan) => plan,
Err(error) => {
let _ = tx.send(StreamEvent::Session(SessionEvent::Error(
helpers::turn_error_for(&error, &turn_correlation_id),
)));
let _ = tx.send(StreamEvent::Session(SessionEvent::Done));
return Box::pin(tokio_stream::wrappers::ReceiverStream::new(bounded_rx));
}
};
if let Err(e) = self.refresh_if_needed().await {
let _ = tx.send(StreamEvent::Session(SessionEvent::Error(
helpers::turn_error_for(&e, &turn_correlation_id),
)));
let _ = tx.send(StreamEvent::Session(SessionEvent::Done));
return Box::pin(tokio_stream::wrappers::ReceiverStream::new(bounded_rx));
}
let capture_started_at = std::time::SystemTime::now();
let capture_turn_start = terminal_capture_start(&messages);
let capture_lease = self
.memory_context_lock()
.capture_lease_at(capture_started_at);
let mut messages = messages;
self.apply_turn_memory_recall(&mut messages).await;
let messages = messages;
let auth = Arc::clone(&self.auth);
let client = self.client.clone();
let credential_source = self.credential_source.clone();
let token_cache = self.token_cache.clone();
let model = self.model.clone();
let tools = self.tools.clone();
let system_prompt = self.effective_system_prompt().await;
let thinking_budget = self.thinking_budget;
let reasoning_level = self.reasoning_level();
let watcher_exit_path = self.watcher_exit_path.clone();
let max_tool_output = self.max_tool_output;
let bash_timeout = self.bash_timeout;
let bash_max_timeout = self.bash_max_timeout;
let subagent_timeout = self.subagent_timeout;
let api_retries = self.api_retries;
let refusal_retries = self.refusal_retries;
let session_manager = self.session_manager.clone();
let subagent_registry = self.subagent_registry.clone();
let reaper_registry = Arc::clone(&subagent_registry);
let reaper_orchestration = self.orchestration.clone();
let capture_provider = capture_lease
.as_ref()
.and_then(|lease| self.extension_capture_provider(lease));
let capture_state = self.memory_context_state.clone();
let event_queue = self.event_queue.clone();
let options = api::ApiOptions {
memory_backend: None,
use_1m_context: self.context_window_override == Some(1_000_000),
cache_ttl: self.cache_ttl,
ttl_downgrade_notified: self.ttl_downgrade_notified.clone(),
saw_1h_honored: self.saw_1h_honored.clone(),
credential_source: self.credential_source.clone(),
token_cache: self.token_cache.clone(),
anthropic_base_url: None,
anthropic_stream_client: self.anthropic_stream_client.clone(),
anthropic_execution_plan,
codex_request_role: self.codex_request_role(),
trace: self.effective_trace_context(),
request_correlation: None,
suppress_stream_deltas: false,
telemetry: self.telemetry_writer.clone(),
tool_session_id: Some(self.host_tool_session.clone()),
session_tool_set: None,
request_tools_schema: None,
anthropic_tool_split: None,
usage_counters: None,
cache_anchor: Some(self.cache_anchor.clone()),
keepwarm: None,
cache_diagnosis: self.cache_diagnosis(),
};
let final_capture_history: Arc<Mutex<Option<Vec<crate::SharedMessage>>>> = Arc::new(Mutex::new(None));
let session = crate::runtime::stream::StreamSession {
memory_backend: self.memory_backend.clone(),
memory_context: self.memory_tool_capability(),
final_capture_history: final_capture_history.clone(),
turn_completion: completion,
context_window: self.context_window(),
continuation: self.continuation.clone(),
auth,
client,
credential_source,
token_cache,
options,
api_retries,
refusal_retries,
model,
tools,
system_prompt,
thinking_budget,
reasoning_level,
tx: tx.clone(),
cancel,
steering_rx,
watcher_exit_path,
max_tool_output,
bash_timeout,
bash_max_timeout,
subagent_timeout,
session_manager,
subagent_registry,
event_queue,
secret_prompt,
hook_bus: self.hook_bus.clone(),
session_id: self.session_id.clone(),
cwd: self.cwd.clone(),
env: self.env.clone(),
env_stripped: self.env_stripped.clone(),
env_warned: self.env_warned.clone(),
auto_approve_confirms,
session_allow_all: self.session_allow_all.clone(),
telemetry_level: self.telemetry_level,
orchestration: self.orchestration.clone(),
delegation_parent: self.delegation_parent.clone(),
turn_correlation_id: turn_correlation_id.clone(),
progressive_tool_disclosure: self.progressive_tool_disclosure,
deferred_tool_loading: self.deferred_tool_loading,
activation_confirm: self.activation_confirm,
tool_session_id: self.host_tool_session.clone(),
retained_tool_set: std::sync::Arc::clone(&self.retained_tool_set),
pending_tool_restore: std::sync::Arc::clone(&self.pending_tool_restore),
mcp_runtime: self.mcp_runtime.clone(),
mcp_session_scope: self.mcp_session_scope.clone(),
extension_runtime: self.extension_runtime.clone(),
extension_session_scope: self.extension_session_scope.clone(),
turn_budget: self.turn_budget.clone(),
owner_steer_budget: self.owner_steer_budget.clone(),
};
tokio::spawn(async move {
let completed = match StreamMethods::run_stream_internal(session, messages).await {
Ok(()) => true,
Err(e) => {
let _ = tx.send(StreamEvent::Session(SessionEvent::Error(
helpers::turn_error_for(&e, &turn_correlation_id),
)));
false
}
};
dispatch_completed_terminal_capture(
completed,
&final_capture_history,
&capture_state,
capture_lease.zip(capture_provider),
capture_started_at,
capture_turn_start.as_ref(),
);
crate::runtime::subagent::reap_finished(
&reaper_registry,
reaper_orchestration.as_deref(),
);
agent_core::core::memstat::log_turn_memory();
let _ = tx.send(StreamEvent::Session(SessionEvent::Done));
});
Box::pin(tokio_stream::wrappers::ReceiverStream::new(bounded_rx))
}
}
impl Clone for Runtime {
fn clone(&self) -> Self {
Self {
client: self.client.clone(),
anthropic_stream_client: self.anthropic_stream_client.clone(),
auth: Arc::clone(&self.auth),
model: self.model.clone(),
tools: self.tools.clone(),
system_prompt: self.system_prompt.clone(),
effective_prompt: self.effective_prompt.clone(),
prompt_generation: self.prompt_generation,
prompt_reload_source: self.prompt_reload_source.clone(),
thinking_budget: self.thinking_budget,
named_level: self.named_level,
explicit_reasoning: self.explicit_reasoning,
codex_request_role: self.codex_request_role,
context_window_override: self.context_window_override,
continuation: Arc::clone(&self.continuation),
compaction_model: self.compaction_model.clone(),
compaction_mode: self.compaction_mode,
compaction_exclusions: self.compaction_exclusions.clone(),
remote_summarization_attempts: Arc::clone(&self.remote_summarization_attempts),
subagent_registry: self.subagent_registry.clone(),
orchestration: self.orchestration.clone(),
event_queue: self.event_queue.clone(),
watcher_exit_path: self.watcher_exit_path.clone(),
max_tool_output: self.max_tool_output,
bash_timeout: self.bash_timeout,
bash_max_timeout: self.bash_max_timeout,
subagent_timeout: self.subagent_timeout,
api_retries: self.api_retries,
refusal_retries: self.refusal_retries,
telemetry_level: self.telemetry_level,
telemetry_writer: self.telemetry_writer.clone(),
trace_ctx: self.trace_ctx.clone(),
trace_controls: Arc::clone(&self.trace_controls),
memory_context_state: std::sync::Arc::clone(&self.memory_context_state),
memory_backend: self.memory_backend.clone(),
memory_backend_config: self.memory_backend_config.clone(),
memory_backend_reconfigure_denied: Arc::clone(&self.memory_backend_reconfigure_denied),
pending_history_import_preview: std::sync::Arc::clone(
&self.pending_history_import_preview,
),
history_import_plan: std::sync::Arc::clone(&self.history_import_plan),
#[cfg(any(test, feature = "testing"))]
capture_provider_for_test: std::sync::Arc::clone(&self.capture_provider_for_test),
pending_memory_segment: std::sync::Arc::clone(&self.pending_memory_segment),
retained_recall_turn: std::sync::Arc::clone(&self.retained_recall_turn),
one_shot_trace_writer: Arc::clone(&self.one_shot_trace_writer),
capture_dir: self.capture_dir.clone(),
cache_diagnostics: self.cache_diagnostics,
cache_ttl: self.cache_ttl,
ttl_downgrade_notified: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
saw_1h_honored: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
cache_anchor: Default::default(),
last_msg_id: Arc::new(Mutex::new(None)),
cache_diag_rejected: Arc::clone(&self.cache_diag_rejected),
session_manager: self.session_manager.clone(),
hook_bus: self.hook_bus.clone(),
reaper_handle: None, reaper_cancel: None, credential_source: self.credential_source.clone(),
token_cache: self.token_cache.clone(), trusted_worker_models: self.trusted_worker_models.clone(),
subagent_limits: self.subagent_limits,
progressive_tool_disclosure: self.progressive_tool_disclosure,
deferred_tool_loading: self.deferred_tool_loading,
activation_confirm: self.activation_confirm,
delegation_parent: self.delegation_parent.clone(),
session_allow_all: self.session_allow_all.clone(),
mcp_runtime: self.mcp_runtime.clone(),
mcp_session_scope: self.mcp_session_scope.clone(),
extension_runtime: self.extension_runtime.clone(),
extension_session_scope: self.extension_session_scope.clone(),
turn_budget: self.turn_budget.clone(),
owner_steer_budget: self.owner_steer_budget.clone(),
host_tool_session: self.host_tool_session.clone(),
tool_catalog_prompt: Arc::clone(&self.tool_catalog_prompt),
retained_tool_set: std::sync::Arc::clone(&self.retained_tool_set),
pending_tool_restore: std::sync::Arc::clone(&self.pending_tool_restore),
session_id: self.session_id.clone(),
cwd: self.cwd.clone(),
env: self.env.clone(),
env_stripped: self.env_stripped.clone(),
env_warned: self.env_warned.clone(),
}
}
}
#[cfg(test)]
mod rich_output_validation_tests {
use super::*;
const MODEL: &str = "anthropic/claude-sonnet-4-6";
fn rich_output(summary: &str) -> crate::ToolOutput {
crate::ToolOutput::Blocks {
summary: summary.to_string(),
blocks: vec![json!({
"type": "image",
"source": {
"type": "base64",
"media_type": "image/png",
"data": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+/l+QAAAAASUVORK5CYII="
}
})],
}
}
#[test]
fn unchanged_summary_retains_rich_array_without_text_truncation() {
let (summary, blocks) = validated_single_tool_output(MODEL, rich_output("image"));
assert_eq!(summary, "image");
let expected = blocks.clone().expect("supported image");
assert_eq!(expected[0], json!({"type": "text", "text": summary}));
assert_eq!(expected[1]["type"], "image");
let retained = retain_single_tool_blocks(blocks, &summary, &summary);
assert_eq!(
single_tool_result_content(&summary, retained, summary.len()),
Value::Array(expected)
);
}
#[test]
fn rewritten_or_truncated_summary_drops_rich_blocks() {
let (summary, blocks) = validated_single_tool_output(MODEL, rich_output("image summary"));
assert!(blocks.is_some());
for hooked in [
"redacted".to_string(),
String::new(),
helpers::HelperMethods::truncate_tool_result(&summary, 5),
] {
let retained = retain_single_tool_blocks(blocks.clone(), &summary, &hooked);
assert!(retained.is_none());
assert_eq!(
single_tool_result_content(&hooked, retained, 5),
Value::String(helpers::HelperMethods::truncate_tool_result(&hooked, 5))
);
}
}
#[test]
fn unsupported_or_malformed_media_becomes_explicit_text() {
let mut malformed = rich_output("private summary");
if let crate::ToolOutput::Blocks { blocks, .. } = &mut malformed {
blocks[0]["source"]["data"] = json!("private invalid payload");
}
for (model, output) in [
(
"google-gemini/unsupported-rich-test",
rich_output("private summary"),
),
(MODEL, malformed),
] {
let (summary, blocks) = validated_single_tool_output(model, output);
assert!(summary.starts_with("Attachment not sent: "), "{summary}");
assert!(!summary.contains("private"));
assert!(blocks.is_none());
assert_eq!(
single_tool_result_content(&summary, blocks, 1024),
Value::String(summary)
);
}
}
#[test]
fn plain_text_and_errors_keep_legacy_truncation() {
for text in ["plain output", "tool error", "Unknown tool: missing"] {
let (summary, blocks) = validated_single_tool_output(
"google-gemini/unsupported-rich-test",
crate::ToolOutput::Text(text.to_string()),
);
assert_eq!(summary, text);
assert!(blocks.is_none());
assert_eq!(
single_tool_result_content(&summary, blocks, 5),
Value::String(helpers::HelperMethods::truncate_tool_result(text, 5))
);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn host_tool_session_shared_by_clones_fresh_per_runtime() {
let rt = Runtime::new_headless();
let clone = rt.clone();
assert_eq!(
rt.host_tool_session_id(),
clone.host_tool_session_id(),
"clones share the live registry and therefore the host session"
);
let other = Runtime::new_headless();
assert_ne!(
rt.host_tool_session_id(),
other.host_tool_session_id(),
"independently constructed runtimes must never share a session"
);
}
#[test]
fn handed_activations_are_carried_on_before_the_first_turn() {
use crate::tools::activation::{CarriedActivation, CarriedProvenance};
let probe = |name: &str| CarriedActivation {
tool_id: format!("builtin:{name}"),
schema_digest: "ab".repeat(32),
provenance: CarriedProvenance::BuiltinRuntime,
};
let rt = Runtime::new_headless();
assert!(rt.carried_tool_activations().is_empty());
rt.restore_tool_activations(vec![probe("subagent_start")]);
rt.restore_tool_activations(Vec::new());
rt.restore_tool_activations(vec![probe("subagent_start"), probe("subagent_status")]);
assert_eq!(
rt.carried_tool_activations(),
vec![probe("subagent_start"), probe("subagent_status")],
"merged without duplicates; an empty hand-off clears nothing"
);
assert_eq!(rt.clone().carried_tool_activations().len(), 2);
assert!(Runtime::new_headless().carried_tool_activations().is_empty());
}
#[test]
fn telemetry_level_gates_shared_observability_sink() {
let mut rt = Runtime::new_headless();
assert!(!rt.trace_context().enabled(), "off by default");
assert!(rt.telemetry_writer().is_none());
rt.set_telemetry_level(crate::runtime::telemetry::TelemetryLevel::Basic);
assert!(rt.trace_context().enabled(), "basic enables the trace sink");
assert!(rt.telemetry_writer().is_some());
let ctx_before = format!("{:?}", rt.trace_context());
rt.set_telemetry_level(crate::runtime::telemetry::TelemetryLevel::Full);
assert_eq!(format!("{:?}", rt.trace_context()), ctx_before);
let clone = rt.clone();
assert!(clone.trace_context().enabled());
assert_eq!(format!("{:?}", clone.trace_context()), ctx_before);
assert!(clone.telemetry_writer().is_some());
rt.set_telemetry_level(crate::runtime::telemetry::TelemetryLevel::Off);
assert!(!rt.trace_context().enabled(), "off disables both");
assert!(rt.telemetry_writer().is_none());
assert!(rt
.shutdown_observability(std::time::Duration::ZERO)
.is_none());
}
#[tokio::test]
async fn shutdown_observability_async_epilogue_contract() {
let rt = Runtime::new_headless();
let budget = crate::runtime::telemetry::DEFAULT_SHUTDOWN_FLUSH_TIMEOUT;
assert!(rt.shutdown_observability_async(budget).await.is_none());
assert!(rt.shutdown_observability_async(budget).await.is_none());
let tmp = tempfile::tempdir().unwrap();
let telemetry_path = tmp.path().join("synaps/api-log.jsonl");
let mut rt = Runtime::new_headless();
let writer = crate::runtime::telemetry::TelemetryWriter::new(
crate::runtime::telemetry::WriterOptions {
telemetry_path: Some(telemetry_path.clone()),
trace_path: Some(tmp.path().join("synaps/request-trace.jsonl")),
..Default::default()
},
);
rt.install_observability_for_tests(writer.clone());
writer.enqueue_telemetry(crate::runtime::telemetry::TelemetryRecord {
ts: 7,
model: "claude-sonnet-4-6".to_string(),
..Default::default()
});
let outcome = rt
.shutdown_observability_async(budget)
.await
.expect("writer installed");
assert!(outcome.is_flushed());
assert_eq!(outcome.stats().written, 1);
assert_eq!(
std::fs::read_to_string(&telemetry_path)
.unwrap()
.lines()
.count(),
1,
"queued final record must be on disk after the epilogue flush"
);
assert!(rt
.shutdown_observability_async(budget)
.await
.expect("writer still installed")
.is_flushed());
let mut rt = Runtime::new_headless();
let slow = crate::runtime::telemetry::TelemetryWriter::new(
crate::runtime::telemetry::WriterOptions {
telemetry_path: Some(tmp.path().join("slow/api-log.jsonl")),
trace_path: Some(tmp.path().join("slow/request-trace.jsonl")),
write_delay: Some(std::time::Duration::from_millis(300)),
..Default::default()
},
);
rt.install_observability_for_tests(slow.clone());
for _ in 0..10 {
slow.enqueue_telemetry(crate::runtime::telemetry::TelemetryRecord::default());
}
let start = std::time::Instant::now();
let outcome = rt
.shutdown_observability_async(std::time::Duration::from_millis(200))
.await
.expect("writer installed");
assert!(!outcome.is_flushed(), "10×300ms cannot drain in 200ms");
assert!(
start.elapsed() < std::time::Duration::from_millis(1500),
"bounded epilogue must not block exit: {:?}",
start.elapsed()
);
}
#[tokio::test]
async fn one_shot_ephemeral_writer_is_retained_and_drained_at_shutdown() {
let rt = Runtime::new_headless();
assert!(!rt.trace_context().enabled(), "telemetry off");
rt.trace_arm_next(false);
let armed = rt.effective_trace_context();
assert!(armed.enabled(), "armed ephemeral context traces");
let budget = crate::runtime::telemetry::DEFAULT_SHUTDOWN_FLUSH_TIMEOUT;
let outcome = rt
.shutdown_observability_async(budget)
.await
.expect("retained one-shot writer must be drained by the epilogue");
assert!(outcome.is_flushed());
assert!(rt.shutdown_observability_async(budget).await.is_none());
}
#[tokio::test]
#[serial_test::serial(synaps_base_dir)]
async fn startup_and_shutdown_paths_sweep_expired_captures() {
struct BaseDirGuard(Option<String>);
impl Drop for BaseDirGuard {
fn drop(&mut self) {
match self.0.take() {
Some(old) => std::env::set_var("SYNAPS_BASE_DIR", old),
None => std::env::remove_var("SYNAPS_BASE_DIR"),
}
}
}
let tmp = tempfile::tempdir().unwrap();
let guard = BaseDirGuard(std::env::var("SYNAPS_BASE_DIR").ok());
std::env::set_var("SYNAPS_BASE_DIR", tmp.path());
let cap_dir = trace::default_capture_dir();
assert!(
cap_dir.starts_with(tmp.path()),
"capture dir must be private to the test"
);
let stale_id = trace::TraceId::new("req-stale").unwrap();
let plant_stale = || {
agent_core::core::private_fs::ensure_private_dir(&cap_dir).unwrap();
let stale = trace::controls::ContentCaptureBundle {
schema: trace::CONTENT_CAPTURE_SCHEMA.to_string(),
request_id: stale_id.clone(),
created_unix_ms: 1_000,
expires_unix_ms: 2_000, redacted: true,
over_budget: false,
body: Some(serde_json::json!({"old": true})),
};
let path = trace::controls::capture_path(&cap_dir, &stale_id);
std::fs::write(&path, serde_json::to_vec(&stale).unwrap()).unwrap();
path
};
let stale_path = plant_stale();
let rt = Runtime::new().await.expect("runtime constructs");
assert!(
!stale_path.exists(),
"Runtime startup must physically remove expired capture bundles"
);
let stale_path = plant_stale();
let _ = rt.shutdown_observability(std::time::Duration::ZERO);
assert!(
!stale_path.exists(),
"sync shutdown epilogue must remove expired capture bundles"
);
let stale_path = plant_stale();
let _ = rt
.shutdown_observability_async(std::time::Duration::from_millis(100))
.await;
assert!(
!stale_path.exists(),
"async shutdown epilogue must remove expired capture bundles"
);
drop(guard);
}
#[tokio::test]
#[serial_test::serial(synaps_base_dir)]
async fn capture_root_binds_at_construction_and_survives_env_churn() {
let base_a = tempfile::tempdir().unwrap();
let base_b = tempfile::tempdir().unwrap();
let old = std::env::var("SYNAPS_BASE_DIR").ok();
std::env::set_var("SYNAPS_BASE_DIR", base_a.path());
let rt_a = Runtime::new().await.expect("runtime A");
std::env::set_var("SYNAPS_BASE_DIR", base_b.path());
let rt_b = Runtime::new().await.expect("runtime B");
let cap_a = base_a.path().join("trace").join("capture");
let cap_b = base_b.path().join("trace").join("capture");
let plant = |dir: &std::path::Path, name: &str| {
agent_core::core::private_fs::ensure_private_dir(dir).unwrap();
let id = trace::TraceId::new(name).unwrap();
let stale = trace::controls::ContentCaptureBundle {
schema: trace::CONTENT_CAPTURE_SCHEMA.to_string(),
request_id: id.clone(),
created_unix_ms: 1_000,
expires_unix_ms: 2_000,
redacted: true,
over_budget: false,
body: Some(serde_json::json!({"old": true})),
};
let path = trace::controls::capture_path(dir, &id);
std::fs::write(&path, serde_json::to_vec(&stale).unwrap()).unwrap();
path
};
let decoy = tempfile::tempdir().unwrap();
std::env::set_var("SYNAPS_BASE_DIR", decoy.path());
let stale_a = plant(&cap_a, "req-stale-a");
let stale_b = plant(&cap_b, "req-stale-b");
let _ = rt_a.shutdown_observability(std::time::Duration::ZERO);
assert!(
!stale_a.exists(),
"runtime A must sweep the root bound at ITS construction"
);
assert!(
stale_b.exists(),
"runtime A must never sweep another runtime's root"
);
let _ = rt_b
.shutdown_observability_async(std::time::Duration::from_millis(100))
.await;
assert!(
!stale_b.exists(),
"runtime B must sweep the root bound at ITS construction"
);
match old {
Some(v) => std::env::set_var("SYNAPS_BASE_DIR", v),
None => std::env::remove_var("SYNAPS_BASE_DIR"),
}
}
#[test]
fn reload_rejects_policy_change_without_partial_apply() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("prompt.yaml");
let manifest = |total: &str, content: &str| {
format!(
"schema: synaps-prompt/1\nkernel: kernel\nmodules:\n - id: kernel\n version: v1\n source: builtin\n priority: 0\n selectors: {{}}\n mutability: mutable_guidance\n content: {content}\npolicies:\n delegation:\n mode: enforced\n allowed_models: [anthropic/claude-sonnet-4-6]\n max_concurrent_workers: 1\n max_total_workers: {total}\n"
)
};
std::fs::write(&path, manifest("1", "before")).unwrap();
let parsed =
agent_core::prompt::PromptManifest::parse(&std::fs::read_to_string(&path).unwrap())
.unwrap();
let model =
agent_core::prompt::QualifiedModelId::parse("anthropic/claude-sonnet-4-6").unwrap();
let context = agent_core::prompt::SelectionContext::new(model.clone(), None).unwrap();
let stack = agent_core::prompt::compile_prompt_stack(
&parsed,
&parsed.registry(path.parent()).unwrap(),
&context,
None,
)
.unwrap();
let catalog = crate::orchestration::OrchestrationRuntime::trusted_catalog(
&model,
parsed.delegation_catalog_candidates(),
)
.unwrap();
let digest = parsed
.delegation_policy(model, &catalog)
.unwrap()
.map(|p| p.digest());
let mut runtime = Runtime::new_headless();
runtime.apply_prompt_stack(stack).unwrap();
runtime.retain_prompt_reload_source(path.clone(), context, None, digest.clone());
let generation = runtime.prompt_generation();
let composed = runtime.effective_prompt().unwrap().composed().to_owned();
std::fs::write(&path, manifest("2", "after")).unwrap();
let error = runtime.reload_prompt().unwrap_err().to_string();
assert!(error.contains("cannot safely change delegation policy"));
assert_eq!(runtime.prompt_generation(), generation);
assert_eq!(runtime.effective_prompt().unwrap().composed(), composed);
assert_eq!(
runtime
.prompt_reload_source
.as_ref()
.unwrap()
.delegation_policy_digest,
digest
);
}
#[test]
fn worker_install_does_not_replay_process_global_favorite_grants() {
let foreground =
crate::orchestration::canonical_foreground_identity("anthropic/claude-fable-5")
.unwrap();
let session = Arc::new(
crate::orchestration::OrchestrationRuntime::baseline(foreground, 4, 8).unwrap(),
);
let favorite = "openai-codex/gpt-5.6-sol";
assert!(
session.preflight(favorite).is_err(),
"precondition: the parent session did not grant this identity"
);
let mut worker = Runtime::new_headless();
worker.apply_config(&crate::config::SynapsConfig {
favorite_models: vec![favorite.to_owned()],
..Default::default()
});
worker.install_worker_orchestration(Arc::clone(&session));
assert!(
session.preflight(favorite).is_err(),
"constructing a child must not mint/replay a favorite-model grant"
);
assert!(!session.effective_choices().contains(&favorite.to_owned()));
}
#[test]
fn host_install_may_apply_explicit_operator_favorites() {
let mut runtime = Runtime::new_headless();
let config = crate::config::SynapsConfig {
favorite_models: vec![
"openai-codex/gpt-5.6-luna".to_owned(),
"anthropic/claude-opus-4-6".to_owned(),
"claude/claude-fable-5".to_owned(),
"not-qualified".to_owned(),
],
..Default::default()
};
runtime.apply_config(&config);
let foreground =
agent_core::prompt::QualifiedModelId::parse("openai-codex/gpt-5.6-sol").unwrap();
runtime.install_orchestration(std::sync::Arc::new(
crate::orchestration::OrchestrationRuntime::baseline(foreground, 3, 8).unwrap(),
));
let orchestration = runtime.orchestration().unwrap();
for trusted in [
"openai-codex/gpt-5.6-sol",
"openai-codex/gpt-5.6-luna",
"anthropic/claude-opus-4-6",
"anthropic/claude-fable-5",
] {
assert!(
orchestration
.effective_choices()
.contains(&trusted.to_owned()),
"missing configured worker choice {trusted}: {:?}",
orchestration.effective_choices()
);
}
let authorized = orchestration
.resolve_and_authorize("sa_cross_provider", Some("anthropic/claude-opus-4-6"))
.expect("an explicitly favorited cross-provider model must be authorized");
assert_eq!(authorized.model.as_str(), "anthropic/claude-opus-4-6");
assert!(!orchestration
.effective_choices()
.contains(&"not-qualified".to_owned()));
}
#[test]
fn configured_worker_choices_survive_manifestless_foreground_changes() {
let mut runtime = Runtime::new_headless();
runtime.apply_config(&crate::config::SynapsConfig {
favorite_models: vec!["anthropic/claude-opus-4-6".to_owned()],
..Default::default()
});
let foreground =
agent_core::prompt::QualifiedModelId::parse("openai-codex/gpt-5.6-sol").unwrap();
runtime.install_orchestration(std::sync::Arc::new(
crate::orchestration::OrchestrationRuntime::baseline(foreground, 3, 8).unwrap(),
));
runtime
.try_set_model("xai-auth/grok-4.5-latest".to_owned())
.unwrap();
let orchestration = runtime.orchestration().unwrap();
assert_eq!(orchestration.foreground_model(), "xai-auth/grok-4.5-latest");
assert!(orchestration
.effective_choices()
.contains(&"anthropic/claude-opus-4-6".to_owned()));
orchestration
.resolve_and_authorize("sa_after_switch", Some("anthropic/claude-opus-4-6"))
.expect("configured worker trust must survive policy replacement");
}
#[test]
fn configured_subagent_limits_size_the_policy_rebuilt_on_model_change() {
let mut runtime = Runtime::new_headless();
assert_eq!(
runtime.subagent_limits(),
crate::config::SubagentLimits::default(),
"defaults before any config"
);
runtime.apply_config(&crate::config::SynapsConfig {
subagent: crate::config::SubagentLimits {
max_concurrent: 24,
max_total: 96,
max_depth: 2,
},
..Default::default()
});
let foreground =
agent_core::prompt::QualifiedModelId::parse("openai-codex/gpt-5.6-sol").unwrap();
runtime.install_orchestration(std::sync::Arc::new(
crate::orchestration::OrchestrationRuntime::from_limits(
foreground,
runtime.subagent_limits(),
)
.unwrap(),
));
runtime
.try_set_model("xai-auth/grok-4.5-latest".to_owned())
.unwrap();
let orchestration = runtime.orchestration().unwrap();
assert_eq!(orchestration.foreground_model(), "xai-auth/grok-4.5-latest");
assert_eq!(
orchestration.ultracode_readiness("xai-auth/grok-4.5-latest"),
Ok((24, 96)),
"the replacement policy uses the configured limits, not a hardcoded 8/64"
);
assert_eq!(orchestration.reserve_delegation("a", None), Ok(1));
assert_eq!(orchestration.reserve_delegation("b", Some("a")), Ok(2));
assert_eq!(
orchestration.reserve_delegation("c", Some("b")),
Err(crate::orchestration::DelegationTreeDenied::DepthLimit { max_depth: 2 })
);
}
#[test]
fn set_model_replaces_manifestless_orchestration_foreground_snapshot() {
let mut runtime = Runtime::new_headless();
let glm = agent_core::prompt::QualifiedModelId::parse("openrouter/z-ai/glm-5.2").unwrap();
runtime.install_orchestration(std::sync::Arc::new(
crate::orchestration::OrchestrationRuntime::baseline(glm, 3, 8).unwrap(),
));
runtime.set_model("openrouter/deepseek/deepseek-v4-pro".to_owned());
let orchestration = runtime.orchestration().unwrap();
assert_eq!(
orchestration.foreground_model(),
"openrouter/deepseek/deepseek-v4-pro"
);
assert!(orchestration
.effective_choices()
.contains(&"openrouter/moonshotai/kimi-k2.7-code".to_owned()));
assert!(orchestration
.resolve_and_authorize("sa_kimi", Some("openrouter/moonshotai/kimi-k2.7-code"))
.is_ok());
}
#[test]
fn set_model_to_uncached_muse_id_rebuilds_orchestration() {
let _guard = crate::runtime::openai::catalog::muse::test_cache_guard();
crate::runtime::openai::catalog::capability_cache::replace_provider("muse", &[]);
let mut runtime = Runtime::new_headless();
let glm = agent_core::prompt::QualifiedModelId::parse("openrouter/z-ai/glm-5.2").unwrap();
runtime.install_orchestration(std::sync::Arc::new(
crate::orchestration::OrchestrationRuntime::baseline(glm, 3, 8).unwrap(),
));
for model in ["muse/muse-wonder-1.0", "muse/muse-live-only-zz5"] {
runtime
.try_set_model(model.to_owned())
.unwrap_or_else(|e| panic!("{model}: {e}"));
assert_eq!(runtime.model(), model);
assert_eq!(runtime.orchestration().unwrap().foreground_model(), model);
}
assert!(runtime.try_set_model("muse/Bad Id".to_owned()).is_err());
assert_eq!(runtime.model(), "muse/muse-live-only-zz5");
}
#[test]
fn set_model_refuses_to_orphan_unreconciled_policy_workers() {
let mut runtime = Runtime::new_headless();
let glm = agent_core::prompt::QualifiedModelId::parse("openrouter/z-ai/glm-5.2").unwrap();
let orchestration = std::sync::Arc::new(
crate::orchestration::OrchestrationRuntime::baseline(glm, 3, 8).unwrap(),
);
orchestration
.resolve_and_authorize("sa_active", None)
.unwrap();
runtime.install_orchestration(orchestration.clone());
runtime.model = "openrouter/z-ai/glm-5.2".to_owned();
let error = runtime
.try_set_model("openrouter/deepseek/deepseek-v4-pro".to_owned())
.unwrap_err();
assert!(error.contains("collection/reconciliation"));
assert_eq!(runtime.model(), "openrouter/z-ai/glm-5.2");
assert!(std::sync::Arc::ptr_eq(
runtime.orchestration().unwrap(),
&orchestration
));
assert!(orchestration.is_unreconciled("sa_active"));
}
#[test]
fn set_model_keeps_active_typed_manifest_orchestration_unchanged() {
let dir = tempfile::tempdir().unwrap();
let manifest_path = dir.path().join("prompt.yaml");
let glm = agent_core::prompt::QualifiedModelId::parse("openrouter/z-ai/glm-5.2").unwrap();
let mut runtime = Runtime::new_headless();
let orchestration = std::sync::Arc::new(
crate::orchestration::OrchestrationRuntime::baseline(glm.clone(), 3, 8).unwrap(),
);
runtime.install_orchestration(orchestration.clone());
runtime.retain_prompt_reload_source(
manifest_path,
agent_core::prompt::SelectionContext::new(glm, None).unwrap(),
None,
None,
);
runtime.set_model("openrouter/deepseek/deepseek-v4-pro".to_owned());
assert!(std::sync::Arc::ptr_eq(
runtime.orchestration().unwrap(),
&orchestration
));
assert_eq!(
runtime.orchestration().unwrap().foreground_model(),
"openrouter/z-ai/glm-5.2"
);
}
#[test]
fn set_model_preserves_qualified_provider_ids_and_bare_claude() {
let cases = [
(
"github-copilot/claude-opus-4.8",
"github-copilot/claude-opus-4.8",
),
("anthropic/claude-sonnet-4-6", "anthropic/claude-sonnet-4-6"),
("google/gemini-2.5-pro", "google/gemini-2.5-pro"),
("openai/gpt-5", "openai/gpt-5"),
("claude-opus-4-8", "claude-opus-4-8"),
];
for (input, expected) in cases {
let mut runtime = Runtime::new_headless();
runtime.set_model(input.to_owned());
assert_eq!(runtime.model(), expected, "input: {input}");
}
}
#[test]
fn set_model_strips_only_legacy_health_status_decoration() {
let cases = [
("✅ 339ms groq/llama-3.3-70b", "groq/llama-3.3-70b"),
(
"✅ 339ms github-copilot/claude-opus-4.8",
"github-copilot/claude-opus-4.8",
),
(
"⚠️ 1200ms anthropic/claude-sonnet-4-6",
"anthropic/claude-sonnet-4-6",
),
];
for (input, expected) in cases {
let mut runtime = Runtime::new_headless();
runtime.set_model(input.to_owned());
assert_eq!(runtime.model(), expected, "input: {input}");
}
}
#[tokio::test]
async fn confirm_without_prompt_fails_closed() {
let result = resolve_before_tool_call_result(
crate::extensions::hooks::events::HookResult::confirm("Run deploy?"),
None,
false,
None,
)
.await;
assert!(matches!(
result,
crate::extensions::hooks::events::HookResult::Block { reason }
if reason.contains("requires confirmation") && reason.contains("Run deploy?")
));
}
#[tokio::test]
async fn modify_result_replaces_tool_input() {
let result = resolve_before_tool_call_decision(
serde_json::json!({"command":"rm -rf /"}),
crate::extensions::hooks::events::HookResult::Modify {
input: serde_json::json!({"command":"echo safe"}),
},
None,
false,
None,
)
.await;
match result {
BeforeToolCallDecision::Continue { input } => {
assert_eq!(input, serde_json::json!({"command":"echo safe"}));
}
BeforeToolCallDecision::Block { reason } => panic!("unexpected block: {reason}"),
}
}
#[tokio::test]
async fn confirm_prompt_yes_continues() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let handle = crate::tools::SecretPromptHandle::new(tx);
let task = tokio::spawn(async move {
let request = rx.recv().await.expect("confirm prompt request");
assert_eq!(request.title, "Confirm tool call");
assert!(request.prompt.contains("Run deploy?"));
let _ = request.response_tx.send(Some("yes".to_string()));
});
let result = resolve_before_tool_call_result(
crate::extensions::hooks::events::HookResult::confirm("Run deploy?"),
Some(&handle),
false,
None,
)
.await;
task.await.unwrap();
assert!(matches!(
result,
crate::extensions::hooks::events::HookResult::Continue
));
}
#[tokio::test]
async fn confirm_prompt_non_yes_blocks() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let handle = crate::tools::SecretPromptHandle::new(tx);
let task = tokio::spawn(async move {
let request = rx.recv().await.expect("confirm prompt request");
let _ = request.response_tx.send(Some("no".to_string()));
});
let result = resolve_before_tool_call_result(
crate::extensions::hooks::events::HookResult::confirm("Run deploy?"),
Some(&handle),
false,
None,
)
.await;
task.await.unwrap();
assert!(matches!(
result,
crate::extensions::hooks::events::HookResult::Block { reason }
if reason.contains("confirmation denied")
));
}
fn ext(ids: &[&str]) -> Vec<String> {
ids.iter().map(|s| s.to_string()).collect()
}
fn key(ids: &[&str]) -> Vec<crate::tools::LatchKey> {
ids.iter().map(|s| (s.to_string(), 0)).collect()
}
fn sourced_confirm(
message: &str,
source: &str,
tool: &str,
) -> crate::extensions::hooks::events::HookResult {
crate::extensions::hooks::events::HookResult::Confirm {
message: message.into(),
sources: ext(&[source]),
tool: Some(tool.into()),
generations: vec![],
modified_input: None,
modified_by: vec![],
}
}
#[test]
fn session_allow_all_clones_share_but_independent_runtimes_do_not() {
use std::sync::Arc;
let parent = Runtime::new_headless();
let cloned = parent.clone();
let independent = Runtime::new_headless();
assert!(Arc::ptr_eq(
parent.session_allow_all(),
cloned.session_allow_all()
));
assert!(!Arc::ptr_eq(
parent.session_allow_all(),
independent.session_allow_all()
));
cloned.session_allow_all().allow(&key(&["jev"]));
assert!(parent.session_allow_all().allows_all(&key(&["jev"])));
assert!(!independent.session_allow_all().allows_all(&key(&["jev"])));
}
#[test]
fn session_allow_all_conversation_boundary_isolates_old_clones() {
use std::sync::Arc;
let mut runtime = Runtime::new_headless();
let old_worker = runtime.clone();
old_worker.session_allow_all().allow(&key(&["jev"]));
runtime.begin_conversation(Some("next".into()));
assert_eq!(runtime.session_id(), Some("next"));
assert!(runtime.session_allow_all().is_empty());
assert!(!Arc::ptr_eq(
runtime.session_allow_all(),
old_worker.session_allow_all()
));
assert!(
old_worker.session_allow_all().is_empty(),
"old latch must be cleared, not just replaced"
);
assert!(!old_worker.session_allow_all().allow(&key(&["jev"])));
assert!(old_worker.session_allow_all().is_empty());
assert!(runtime.session_allow_all().is_empty());
let new_worker = runtime.clone();
new_worker.session_allow_all().allow(&key(&["jev"]));
assert!(runtime.session_allow_all().allows_all(&key(&["jev"])));
runtime.begin_conversation(Some("next".into()));
assert!(runtime.session_allow_all().is_empty());
assert!(!Arc::ptr_eq(
runtime.session_allow_all(),
new_worker.session_allow_all()
));
}
#[test]
fn conversation_boundary_resets_the_cache_diagnosis_chain() {
let mut runtime = Runtime::new_headless();
runtime.set_cache_diagnostics(true);
let mut driver = runtime.clone();
driver.share_ttl_latches(&runtime);
runtime.cache_diagnosis().unwrap().record("msg_old");
runtime.set_session_id(Some("successor".into()));
assert_eq!(
runtime
.cache_diagnosis()
.unwrap()
.previous_message_id()
.as_deref(),
Some("msg_old")
);
runtime.begin_conversation(Some("next".into()));
assert_eq!(
runtime.cache_diagnosis().unwrap().previous_message_id(),
None
);
assert_eq!(
driver.cache_diagnosis().unwrap().previous_message_id(),
None
);
}
#[test]
fn session_allow_all_identity_change_preserves_compaction_consent() {
use std::sync::Arc;
let mut runtime = Runtime::new_headless();
let worker = runtime.clone();
worker.session_allow_all().allow(&key(&["jev"]));
runtime.set_session_id(Some("linked-successor".into()));
assert!(Arc::ptr_eq(
runtime.session_allow_all(),
worker.session_allow_all()
));
assert!(runtime.session_allow_all().allows_all(&key(&["jev"])));
}
#[tokio::test]
async fn confirm_prompt_always_latches_only_the_source_extension() {
let latch = crate::tools::SessionConfirmLatch::new();
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let handle = crate::tools::SecretPromptHandle::new(tx);
let task = tokio::spawn(async move {
let first = rx.recv().await.expect("first confirm prompt");
assert_eq!(
first.confirm_scope,
Some(crate::session::ConfirmScope {
extensions: vec!["jev".into()],
tool: Some("bash".into()),
rewritten_by: vec![],
})
);
assert!(first.prompt.starts_with("Extension: jev\nTool: bash\n"), "{}", first.prompt);
let _ = first
.response_tx
.send(Some(CONFIRM_ANSWER_ALLOW_ALL.to_string()));
let other = rx.recv().await.expect("other extension still prompts");
assert_eq!(
other.confirm_scope.as_ref().map(|s| s.extensions.clone()),
Some(vec!["other".to_string()])
);
let _ = other.response_tx.send(None);
assert!(rx.recv().await.is_none(), "no further prompts");
});
let first = resolve_before_tool_call_result(
sourced_confirm("Run deploy?", "jev", "bash"),
Some(&handle),
false,
Some(&latch),
)
.await;
assert!(matches!(
first,
crate::extensions::hooks::events::HookResult::Continue
));
assert_eq!(latch.extensions(), ext(&["jev"]), "latch keyed by source");
let second = resolve_before_tool_call_result(
sourced_confirm("Run deploy again?", "jev", "write"),
Some(&handle),
false,
Some(&latch),
)
.await;
assert!(matches!(
second,
crate::extensions::hooks::events::HookResult::Continue
));
let other = resolve_before_tool_call_result(
sourced_confirm("Run ls?", "other", "bash"),
Some(&handle),
false,
Some(&latch),
)
.await;
assert!(
matches!(other, crate::extensions::hooks::events::HookResult::Block { .. }),
"another extension's confirm is not covered by jev's latch"
);
drop(handle);
task.await.unwrap();
}
#[tokio::test]
async fn latched_plus_unlatched_source_prompts_and_allow_all_latches_both() {
let latch = crate::tools::SessionConfirmLatch::new();
latch.allow(&key(&["a"]));
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let handle = crate::tools::SecretPromptHandle::new(tx);
let task = tokio::spawn(async move {
let req = rx.recv().await.expect("must prompt: b is not latched");
assert_eq!(
req.confirm_scope.as_ref().map(|s| s.extensions.clone()),
Some(vec!["a".to_string(), "b".to_string()])
);
assert!(req.prompt.starts_with("Extension: a, b\n"), "{}", req.prompt);
let _ = req.response_tx.send(Some(CONFIRM_ANSWER_ALLOW_ALL.to_string()));
});
let merged = || crate::extensions::hooks::events::HookResult::Confirm {
message: "[a] x\n\n[b] y".into(),
sources: ext(&["a", "b"]),
generations: vec![],
tool: Some("bash".into()),
modified_input: None,
modified_by: vec![],
};
let first = resolve_before_tool_call_result(merged(), Some(&handle), false, Some(&latch)).await;
task.await.unwrap();
assert!(matches!(first, crate::extensions::hooks::events::HookResult::Continue));
assert!(latch.allows_all(&key(&["a", "b"])));
drop(handle);
let second = resolve_before_tool_call_result(merged(), None, false, Some(&latch)).await;
assert!(matches!(second, crate::extensions::hooks::events::HookResult::Continue));
}
#[tokio::test]
async fn no_latch_means_no_allow_all_scope() {
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let handle = crate::tools::SecretPromptHandle::new(tx);
let task = tokio::spawn(async move {
let req = rx.recv().await.unwrap();
assert!(req.confirm_scope.is_none());
let _ = req.response_tx.send(Some("y".into()));
});
let r = resolve_before_tool_call_result(
sourced_confirm("Run?", "jev", "bash"),
Some(&handle),
false,
None,
)
.await;
task.await.unwrap();
assert!(matches!(r, crate::extensions::hooks::events::HookResult::Continue));
}
#[tokio::test]
async fn rewritten_input_is_shown_in_the_confirm_prompt() {
let latch = crate::tools::SessionConfirmLatch::new();
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let handle = crate::tools::SecretPromptHandle::new(tx);
let task = tokio::spawn(async move {
let req = rx.recv().await.unwrap();
let body = req.prompt.clone();
let scope = req.confirm_scope.clone().unwrap();
let _ = req.response_tx.send(Some("y".into()));
(body, scope)
});
let result = resolve_before_tool_call_result(
crate::extensions::hooks::events::HookResult::Confirm {
message: "Run this bash command?".into(),
sources: ext(&["jev"]),
generations: vec![],
tool: Some("bash".into()),
modified_input: Some(serde_json::json!({"command": "rm -rf build/"})),
modified_by: ext(&["rewriter"]),
},
Some(&handle),
false,
Some(&latch),
)
.await;
let (body, scope) = task.await.unwrap();
let header_end = body.find("Message from jev:\nRun this bash command?").unwrap();
let header = &body[..header_end];
assert!(header.contains("Input rewritten by: rewriter"), "{body}");
assert!(header.contains("Input that will run:\n rm -rf build/"), "{body}");
assert_eq!(scope.rewritten_by, ext(&["rewriter"]));
assert!(matches!(
result,
crate::extensions::hooks::events::HookResult::Modify { input }
if input == serde_json::json!({"command": "rm -rf build/"})
));
let plain = confirm_prompt_body("m", &ext(&["jev"]), Some("bash"), None);
assert!(!plain.contains("rewritten"), "{plain}");
}
#[test]
fn final_input_preview_redacts_credentials() {
let secrets = [
"SECRET-BEARER-123",
"SECRET-KEY-456",
"SECRET-FIELD-789",
"QmFzaWNTRUNSRVQ=",
"SECRET-TOK-000",
"SECRET-FLAG-111",
"sk-ant-api03-abcdefghijklmnop",
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2lnbmF0dXJl",
"ghp_abcdefghijklmnopqrstuvwxyz0123",
];
let command = serde_json::json!({
"command": "curl -H 'Authorization: Bearer SECRET-BEARER-123' \
'https://api.example/v1?api_key=SECRET-KEY-456' \
-H \"authorization: Basic QmFzaWNTRUNSRVQ=\" \
&& TOKEN=SECRET-TOK-000 deploy --password SECRET-FLAG-111 \
sk-ant-api03-abcdefghijklmnop \
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.c2lnbmF0dXJl \
ghp_abcdefghijklmnopqrstuvwxyz0123"
});
let json = serde_json::json!({
"url": "https://x/y",
"api_key": "SECRET-FIELD-789",
"headers": {"X-Note": "Bearer SECRET-BEARER-123"},
"args": ["--token=SECRET-TOK-000"],
});
for input in [command, json] {
let p = final_input_preview(&input);
for secret in secrets {
assert!(!p.contains(secret), "{secret} leaked in:\n{p}");
}
assert!(p.contains("[REDACTED]"), "{p}");
}
let plain = final_input_preview(&serde_json::json!({"command": "rm -rf build/ && ls -la"}));
assert!(plain.contains("rm -rf build/ && ls -la"), "{plain}");
}
#[test]
fn redact_secret_text_covers_round4_shapes() {
let rows: &[(&str, &str)] = &[
("curl -H 'x-api-key: XAPIKEY-SECRET-1' https://a", "XAPIKEY-SECRET-1"),
("FOO_TOKEN=TOK-SECRET-2 run", "TOK-SECRET-2"),
("MY_SECRET=SEC-SECRET-3 ./x", "SEC-SECRET-3"),
("DB_PASSWORD=PW-SECRET-4 ./x", "PW-SECRET-4"),
("OPENAI_API_KEY=APIKEY-SECRET-5 ./x", "APIKEY-SECRET-5"),
("curl -H 'Authorization: Basic QkFTSUMtU0VDUkVULTY='", "QkFTSUMtU0VDUkVULTY="),
("aws_secret_access_key=AWSSEC-SECRET-7", "AWSSEC-SECRET-7"),
("git clone https://alice:USERINFO-SECRET-8@github.com/o/r", "USERINFO-SECRET-8"),
("psql postgres://app:PGPW-SECRET-9@db:5432/prod", "PGPW-SECRET-9"),
("DATABASE_URL=postgres://app:DBURL-SECRET-10@db/prod ./run", "DBURL-SECRET-10"),
("curl -H 'Cookie: sid=COOKIE-SECRET-11; theme=dark' https://a", "COOKIE-SECRET-11"),
("printf 'Set-Cookie: sid=SETCOOKIE-SECRET-12'", "SETCOOKIE-SECRET-12"),
("curl -u alice:CURLU-SECRET-13 https://a", "CURLU-SECRET-13"),
("mysql -u root -pMYSQL-SECRET-14 prod", "MYSQL-SECRET-14"),
("sshpass -p SSHPASS-SECRET-15 ssh host", "SSHPASS-SECRET-15"),
("aws configure set aws_secret_access_key AWSCFG-SECRET-16", "AWSCFG-SECRET-16"),
("aws x --secret-access-key FLAG-SECRET-17 --region eu", "FLAG-SECRET-17"),
("aws x --access-key FLAG-SECRET-18", "FLAG-SECRET-18"),
("STRIPE_KEY=rk_live_STRIPE-SECRET-19 node app", "STRIPE-SECRET-19"),
("OPENAI_KEY=OAIKEY-SECRET-20 node app", "OAIKEY-SECRET-20"),
];
let precise = [
"QkFTSUMtU0VDUkVULTY=",
"USERINFO-SECRET-8",
"PGPW-SECRET-9",
"DBURL-SECRET-10",
"COOKIE-SECRET-11",
"SETCOOKIE-SECRET-12",
"CURLU-SECRET-13",
"MYSQL-SECRET-14",
"SSHPASS-SECRET-15",
];
for (text, secret) in rows {
let out = redact_secret_text(text);
assert!(!out.contains(secret), "{secret} leaked: {out}");
assert!(out.contains("[REDACTED]"), "{out}");
let preview = final_input_preview(&serde_json::json!({ "command": text }));
assert!(!preview.contains(secret), "{secret} leaked in preview: {preview}");
if precise.contains(secret) {
let mut v = serde_json::Value::String(text.to_string());
crate::runtime::trace::export::redact_value(&mut v);
assert!(
!v.as_str().unwrap().contains(secret),
"shared scrub leaked {secret}: {v}"
);
}
}
for ok in [
"mkdir -p build/out",
"ssh -p 2222 host",
"ssh -p2222 host",
"docker run -p 8080:80 nginx",
"mysql -h db -P 3306 -p prod",
"curl -u alice https://example.com/path",
"echo the token is valid and the password field is empty",
"monkey=1 make",
"git log --format=%H -- src/",
"https://example.com/a/b?page=2",
] {
assert_eq!(redact_secret_text(ok), ok, "over-redacted");
}
}
#[test]
fn shared_scrub_leaves_benign_code_and_prose_alone() {
let benign = [
"max_tokens: 4096",
"password: String,",
"let token = load()?;",
"the password-reset flow",
"pub authorization: Option<String>,",
"cookie: HashMap<String, String>,",
"MY_KEY=value_in_docs",
"--token-file path/to/file",
"fn refresh_access_token(&self) -> Result<Token>",
"aws_secret_access_key is the field name",
];
for text in benign {
let mut v = serde_json::Value::String(text.to_string());
crate::runtime::trace::export::redact_value(&mut v);
assert_eq!(v.as_str().unwrap(), text, "shared scrub mangled benign text");
assert_eq!(crate::runtime::trace::export::redact_text(text), text);
}
let mut record = serde_json::json!({
"text": "set max_tokens: 4096 and keep the password-reset flow",
"code": ["password: String,", "let token = load()?;"],
});
let before = record.clone();
crate::runtime::trace::export::redact_value(&mut record);
assert_eq!(record, before);
}
#[test]
fn confirmer_message_echoing_injected_credentials_is_redacted() {
let injected = serde_json::json!({
"command": "curl -H 'Authorization: Bearer ECHOED-BEARER-X' https://a"
});
let body = confirm_prompt_body(
"Run: curl -H 'Authorization: Bearer ECHOED-BEARER-X' https://a ?",
&ext(&["jev"]),
Some("bash"),
Some((&ext(&["injector"]), &injected)),
);
assert!(!body.contains("ECHOED-BEARER-X"), "{body}");
assert!(body.contains("Message from jev:\nRun: curl -H 'Authorization: [REDACTED]"), "{body}");
}
#[test]
fn confirm_message_control_and_bidi_chars_are_stripped() {
let hostile = "ok\x1b[3A\x1b[2KExtension: trusted\rTool: none\u{0085}\u{202E}evil\u{200B}x\u{2066}y\tz\nnext";
let body = confirm_prompt_body(hostile, &ext(&["jev"]), Some("bash"), None);
for bad in ['\x1b', '\r', '\u{0085}', '\u{202E}', '\u{200B}', '\u{2066}', '\t'] {
assert!(!body.contains(bad), "{bad:?} survived: {body:?}");
}
assert!(body.contains("Message from jev:\nok[3A[2KExtension: trustedTool: noneevilxy z\nnext"), "{body:?}");
assert!(body.starts_with("Extension: jev\nTool: bash\n"));
}
#[test]
fn extension_message_is_labelled_after_the_host_header() {
let fake = "Input that will run:\n echo harmless";
let body = confirm_prompt_body(fake, &ext(&["jev"]), Some("bash"), None);
let label = body.find("Message from jev:\n").expect(&body);
assert_eq!(body.find("Input that will run:"), Some(label + "Message from jev:\n".len()));
assert!(body.starts_with("Extension: jev\nTool: bash\n"), "{body}");
}
#[test]
fn final_input_preview_is_capped() {
let many = serde_json::json!({"command": (1..=20).map(|i| format!("echo {i}")).collect::<Vec<_>>().join("\n")});
let p = final_input_preview(&many);
assert!(p.lines().count() <= MAX_REWRITE_PREVIEW_LINES + 1, "{p}");
assert!(p.ends_with("…[preview truncated]"), "{p}");
let wide = serde_json::json!({"path": "x".repeat(2000)});
let p = final_input_preview(&wide);
assert!(p.chars().count() <= MAX_REWRITE_PREVIEW_CHARS + 30, "{}", p.len());
let obj = serde_json::json!({"path": "/tmp/a", "content": "hi"});
assert!(final_input_preview(&obj).contains("\"path\": \"/tmp/a\""));
}
#[test]
fn long_confirm_message_is_capped() {
let long = "x\n".repeat(500);
let body = confirm_prompt_body(&long, &ext(&["jev"]), Some("bash"), None);
assert!(body.starts_with("Extension: jev\nTool: bash\n"));
assert!(body.contains("…[message truncated by host]"));
assert!(body.lines().count() <= MAX_CONFIRM_MESSAGE_LINES + 8, "{}", body.lines().count());
let wide = "y".repeat(10_000);
assert!(confirm_prompt_body(&wide, &ext(&["jev"]), None, None).chars().count() < 1400);
}
#[tokio::test]
async fn unattributed_confirm_always_answer_does_not_latch() {
let latch = crate::tools::SessionConfirmLatch::new();
let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
let handle = crate::tools::SecretPromptHandle::new(tx);
let task = tokio::spawn(async move {
let req = rx.recv().await.expect("prompt");
assert!(req.confirm_scope.is_none());
let _ = req.response_tx.send(Some(CONFIRM_ANSWER_ALLOW_ALL.to_string()));
});
let result = resolve_before_tool_call_result(
crate::extensions::hooks::events::HookResult::confirm("Run deploy?"),
Some(&handle),
false,
Some(&latch),
)
.await;
task.await.unwrap();
assert!(matches!(
result,
crate::extensions::hooks::events::HookResult::Continue
));
assert!(latch.is_empty());
}
#[tokio::test]
async fn session_allow_all_does_not_override_block() {
let latch = crate::tools::SessionConfirmLatch::new();
latch.allow(&key(&["jev"]));
let blocked = resolve_before_tool_call_result(
crate::extensions::hooks::events::HookResult::Block {
reason: "nope".into(),
},
None,
false,
Some(&latch),
)
.await;
assert!(matches!(
blocked,
crate::extensions::hooks::events::HookResult::Block { reason } if reason == "nope"
));
let headless = resolve_before_tool_call_result(
sourced_confirm("Run deploy?", "jev", "bash"),
None,
false,
Some(&latch),
)
.await;
assert!(matches!(
headless,
crate::extensions::hooks::events::HookResult::Continue
));
let other = resolve_before_tool_call_result(
sourced_confirm("Run deploy?", "other", "bash"),
None,
false,
Some(&latch),
)
.await;
assert!(matches!(
other,
crate::extensions::hooks::events::HookResult::Block { .. }
));
}
#[test]
fn test_max_tokens_for_model() {
assert_eq!(
HelperMethods::max_tokens_for_model("claude-opus-4-6"),
128000
);
assert_eq!(
HelperMethods::max_tokens_for_model("opus-something"),
128000
);
assert_eq!(
HelperMethods::max_tokens_for_model("claude-sonnet-4-20250514"),
64000
);
assert_eq!(HelperMethods::max_tokens_for_model("haiku"), 64000);
assert_eq!(HelperMethods::max_tokens_for_model("claude-3-haiku"), 64000);
assert_eq!(
HelperMethods::max_tokens_for_model("some-other-model"),
64000
);
assert_eq!(HelperMethods::max_tokens_for_model(""), 64000);
assert_eq!(HelperMethods::max_tokens_for_model("OPUS"), 64000); assert_eq!(
HelperMethods::max_tokens_for_model("model-opus-end"),
128000
); }
#[test]
fn test_truncate_tool_result() {
let default_max = 30000;
let short = "This is a short string.";
assert_eq!(
HelperMethods::truncate_tool_result(short, default_max),
short
);
let exact = "x".repeat(30000);
assert_eq!(
HelperMethods::truncate_tool_result(&exact, default_max),
exact
);
let too_long = "x".repeat(30001);
let truncated = HelperMethods::truncate_tool_result(&too_long, default_max);
assert!(truncated.starts_with(&"x".repeat(30000)));
assert!(truncated.contains("[truncated — 30001 total bytes, showing first 30000]"));
assert!(truncated.len() > 30000);
let very_long = "a".repeat(50000);
let truncated_very_long = HelperMethods::truncate_tool_result(&very_long, default_max);
assert!(
truncated_very_long.contains("[truncated — 50000 total bytes, showing first 30000]")
);
assert!(truncated_very_long.starts_with(&"a".repeat(30000)));
let custom_truncated = HelperMethods::truncate_tool_result(&very_long, 100);
assert!(custom_truncated.starts_with(&"a".repeat(100)));
assert!(custom_truncated.contains("[truncated — 50000 total bytes, showing first 100]"));
}
#[test]
fn truncate_tool_result_enforces_byte_budget() {
let multibyte = "é".repeat(100); let out = HelperMethods::truncate_tool_result(&multibyte, 50);
let body = out.split("\n\n[truncated").next().unwrap();
assert!(
body.len() <= 50,
"retained body must fit the byte budget (got {} bytes)",
body.len()
);
assert!(std::str::from_utf8(body.as_bytes()).is_ok());
assert!(
out.contains("total bytes"),
"marker must report byte counts, got: {out}"
);
}
#[test]
fn test_thinking_level_ranges() {
use crate::core::models::thinking_level_for_budget;
assert_eq!(thinking_level_for_budget(0), "adaptive");
assert_eq!(thinking_level_for_budget(1), "low");
assert_eq!(thinking_level_for_budget(1024), "low");
assert_eq!(thinking_level_for_budget(2048), "low");
assert_eq!(thinking_level_for_budget(2049), "medium");
assert_eq!(thinking_level_for_budget(3000), "medium");
assert_eq!(thinking_level_for_budget(4096), "medium");
assert_eq!(thinking_level_for_budget(4097), "high");
assert_eq!(thinking_level_for_budget(8192), "high");
assert_eq!(thinking_level_for_budget(16384), "high");
assert_eq!(thinking_level_for_budget(16385), "xhigh");
assert_eq!(thinking_level_for_budget(32768), "xhigh");
assert_eq!(thinking_level_for_budget(100000), "xhigh");
}
}
#[cfg(test)]
mod effective_prompt_tests {
use super::*;
struct LateTool;
#[async_trait::async_trait]
impl crate::Tool for LateTool {
fn name(&self) -> &str {
"late_tool"
}
fn description(&self) -> &str {
"Registered after the catalog froze."
}
fn parameters(&self) -> Value {
json!({"type": "object", "properties": {}})
}
async fn execute(&self, _p: Value, _c: crate::ToolContext) -> crate::Result<String> {
Ok(String::new())
}
}
#[tokio::test]
async fn tool_catalog_lists_activatable_tools_outside_the_core() {
let mut runtime = headless("anthropic/claude-sonnet-5-5", "BASE.");
assert!(
runtime.frozen_tool_catalog().await.is_none(),
"flag off: no catalog"
);
runtime.progressive_tool_disclosure = true;
let catalog = runtime.frozen_tool_catalog().await.expect("catalog");
assert!(catalog.starts_with("## Activatable tools\n"));
assert!(
catalog.contains("not instructions"),
"third-party text is framed as data"
);
assert!(catalog.contains("- builtin:subagent_start"), "{catalog}");
for core in [
"builtin:bash",
"builtin:read",
"builtin:activate_tools",
"builtin:search_tools",
] {
assert!(
!catalog.contains(&format!("- {core}")),
"core {core} listed"
);
}
let prompt = runtime.effective_system_prompt().await.unwrap();
assert!(prompt.starts_with("BASE."), "base stays first");
assert!(prompt.ends_with(&catalog), "catalog appended last");
}
#[tokio::test]
async fn tool_catalog_is_frozen_per_session() {
let mut runtime = headless("anthropic/claude-sonnet-5-5", "BASE.");
runtime.progressive_tool_disclosure = true;
let first = runtime.effective_system_prompt().await.unwrap();
runtime.tools.write().await.register(Arc::new(LateTool));
let clone = runtime.clone();
assert_eq!(runtime.effective_system_prompt().await.unwrap(), first);
assert_eq!(clone.effective_system_prompt().await.unwrap(), first);
assert!(
!first.contains("late_tool"),
"late registration does not rewrite the prefix"
);
}
#[tokio::test]
async fn tool_catalog_absent_when_activation_is_denied_or_impossible() {
let mut denied = headless("anthropic/claude-sonnet-5-5", "BASE.");
denied.progressive_tool_disclosure = true;
denied.set_activation_confirm(agent_core::config::ActivationConfirm::Deny);
assert!(denied.frozen_tool_catalog().await.is_none());
assert_eq!(
denied.effective_system_prompt().await.as_deref(),
Some("BASE.")
);
let mut worker = headless("anthropic/claude-sonnet-5-5", "BASE.");
worker.progressive_tool_disclosure = true;
worker.set_tools(crate::ToolRegistry::without_subagent());
assert!(
worker.frozen_tool_catalog().await.is_none(),
"no activate_tools, no catalog"
);
}
fn headless(model: &str, base: &str) -> Runtime {
let mut runtime = Runtime::new_headless();
runtime.set_model(model.to_string());
runtime.set_system_prompt(base.to_string());
runtime
}
#[tokio::test]
async fn anthropic_foreground_ultracode_composes_standing_doctrine_once() {
let mut runtime = headless("anthropic/claude-fable-5", "BASE.");
runtime.named_level = Some(agent_core::reasoning::ReasoningLevel::UltraCode);
let prompt = runtime.effective_system_prompt().await.expect("doctrine");
assert_eq!(prompt.matches("<anthropic-ultracode-workflow>").count(), 1);
assert!(prompt.contains("subagent_start"));
assert!(prompt.contains("subagent_resume"));
}
#[tokio::test]
async fn codex_foreground_with_subagent_tools_composes_doctrine() {
let runtime = headless("openai-codex/gpt-5.6-sol", "BASE.");
let prompt = runtime
.effective_system_prompt()
.await
.expect("prompt must compose");
assert!(prompt.starts_with("BASE."), "base must lead");
assert!(prompt.contains("## Subagent supervision"));
assert!(prompt.contains("NEVER end your turn"));
}
#[tokio::test]
async fn worker_runtime_without_subagent_tools_stays_clean() {
let mut runtime = headless("openai-codex/gpt-5.6-sol", "WORKER.");
runtime.set_tools(ToolRegistry::without_subagent());
assert_eq!(
runtime.effective_system_prompt().await.as_deref(),
Some("WORKER."),
"workers have no subagent tools; doctrine would be unactionable noise"
);
}
#[tokio::test]
async fn non_codex_foreground_stays_clean() {
let runtime = headless("xai-auth/grok-4.5-latest", "BASE.");
assert_eq!(
runtime.effective_system_prompt().await.as_deref(),
Some("BASE.")
);
}
#[tokio::test]
async fn manifest_prompt_is_returned_verbatim() {
let mut runtime = headless("openai-codex/gpt-5.6-sol", "IGNORED.");
let module = agent_core::prompt::PromptModule::new(
agent_core::prompt::PromptModuleId::parse("kernel.test").unwrap(),
"1.0.0",
agent_core::prompt::PromptModuleSource::User,
0,
agent_core::prompt::PromptSelectors::default(),
agent_core::prompt::ModuleMutability::MutableGuidance,
"KERNEL.",
)
.unwrap();
let context = agent_core::prompt::SelectionContext::new(
agent_core::prompt::QualifiedModelId::parse("openai-codex/gpt-5.6-sol").unwrap(),
None,
)
.unwrap();
let stack = agent_core::prompt::PromptStack::new(vec![module], context).unwrap();
runtime.apply_prompt_stack(stack).unwrap();
assert_eq!(
runtime.effective_system_prompt().await.as_deref(),
Some("KERNEL."),
"manifest authors own the full stack; no builtin injection on top"
);
}
#[tokio::test]
async fn unresolvable_model_identity_fails_closed_to_base() {
let runtime = headless("definitely-not-a-model", "BASE.");
assert_eq!(
runtime.effective_system_prompt().await.as_deref(),
Some("BASE.")
);
}
}
#[cfg(test)]
mod set_reasoning_level_checked_tests {
use super::*;
use agent_core::reasoning::ReasoningLevel;
fn codex_runtime(model_id: &str) -> Runtime {
let mut rt = Runtime::new_headless();
rt.set_model(format!("openai-codex/{model_id}"));
rt.set_reasoning_level(ReasoningLevel::Low);
rt
}
fn provisioned_fable_runtime() -> Runtime {
let mut rt = Runtime::new_headless();
rt.set_model("anthropic/claude-fable-5".into());
rt.set_reasoning_level(ReasoningLevel::Low);
let foreground =
agent_core::prompt::QualifiedModelId::parse("anthropic/claude-fable-5").unwrap();
let orchestration =
crate::orchestration::OrchestrationRuntime::baseline(foreground, 3, 8).unwrap();
rt.install_orchestration(Arc::new(orchestration));
rt
}
fn install_typed_manifest(rt: &mut Runtime) {
let module = agent_core::prompt::PromptModule::new(
agent_core::prompt::PromptModuleId::parse("kernel.test").unwrap(),
"1.0.0",
agent_core::prompt::PromptModuleSource::User,
0,
agent_core::prompt::PromptSelectors::default(),
agent_core::prompt::ModuleMutability::MutableGuidance,
"KERNEL.",
)
.unwrap();
let context = agent_core::prompt::SelectionContext::new(
agent_core::prompt::QualifiedModelId::parse("anthropic/claude-fable-5").unwrap(),
None,
)
.unwrap();
rt.apply_prompt_stack(agent_core::prompt::PromptStack::new(vec![module], context).unwrap())
.unwrap();
}
#[test]
fn provisioned_fable_accepts_max_and_ultracode() {
for level in [ReasoningLevel::Max, ReasoningLevel::UltraCode] {
let mut rt = provisioned_fable_runtime();
rt.set_reasoning_level_checked(level).unwrap();
assert_eq!(rt.reasoning_level(), level);
}
}
#[test]
fn fable_ultracode_prerequisites_fail_without_mutation() {
let mut missing_orchestration = Runtime::new_headless();
missing_orchestration.set_model("anthropic/claude-fable-5".into());
missing_orchestration.set_reasoning_level(ReasoningLevel::Low);
assert!(missing_orchestration
.set_reasoning_level_checked(ReasoningLevel::UltraCode)
.is_err());
assert_eq!(missing_orchestration.reasoning_level(), ReasoningLevel::Low);
let mut missing_tools = provisioned_fable_runtime();
missing_tools.set_tools(ToolRegistry::without_subagent());
assert!(missing_tools
.set_reasoning_level_checked(ReasoningLevel::UltraCode)
.is_err());
assert_eq!(missing_tools.reasoning_level(), ReasoningLevel::Low);
let mut worker = provisioned_fable_runtime();
worker.set_codex_request_role(crate::runtime::openai::catalog::ExecutionRole::Worker);
assert!(worker
.set_reasoning_level_checked(ReasoningLevel::UltraCode)
.is_err());
assert_eq!(worker.reasoning_level(), ReasoningLevel::Low);
let mut internal = provisioned_fable_runtime();
internal.set_codex_request_role(crate::runtime::openai::catalog::ExecutionRole::Internal);
assert!(internal
.set_reasoning_level_checked(ReasoningLevel::UltraCode)
.is_err());
assert_eq!(internal.reasoning_level(), ReasoningLevel::Low);
let mut manifest = provisioned_fable_runtime();
install_typed_manifest(&mut manifest);
assert!(manifest
.set_reasoning_level_checked(ReasoningLevel::UltraCode)
.is_err());
assert_eq!(manifest.reasoning_level(), ReasoningLevel::Low);
}
#[test]
fn checked_accepts_client_omission_modes_for_codex() {
for level in [ReasoningLevel::Off, ReasoningLevel::Adaptive] {
let mut rt = codex_runtime("gpt-5.6-sol");
rt.set_reasoning_level_checked(level).unwrap();
assert_eq!(rt.reasoning_level(), level);
}
}
#[test]
fn checked_rejects_ultra_for_luna_no_mutation() {
let mut rt = codex_runtime("gpt-5.6-luna");
let before = rt.reasoning_level();
let err = rt
.set_reasoning_level_checked(ReasoningLevel::Ultra)
.unwrap_err();
assert!(
err.contains("ultra"),
"error must name the rejected level; got: {err}"
);
assert_eq!(
rt.reasoning_level(),
before,
"runtime must not be mutated when validation fails"
);
}
#[test]
fn checked_rejects_max_for_gpt55_no_mutation() {
let mut rt = codex_runtime("gpt-5.5");
rt.set_reasoning_level(ReasoningLevel::Medium);
let err = rt
.set_reasoning_level_checked(ReasoningLevel::Max)
.unwrap_err();
assert!(
err.contains("max"),
"error must name the rejected level; got: {err}"
);
assert_eq!(rt.reasoning_level(), ReasoningLevel::Medium);
}
#[test]
fn checked_accepts_ultra_for_sol_and_mutates() {
let mut rt = codex_runtime("gpt-5.6-sol");
rt.set_reasoning_level_checked(ReasoningLevel::Ultra)
.expect("sol must accept ultra");
assert_eq!(rt.reasoning_level(), ReasoningLevel::Ultra);
}
#[test]
fn checked_rejects_extended_levels_without_metadata() {
let mut rt = Runtime::new_headless();
rt.set_model("claude-opus-4-7".to_string());
rt.set_reasoning_level(ReasoningLevel::Low);
for level in [ReasoningLevel::Max, ReasoningLevel::Ultra] {
assert!(rt.set_reasoning_level_checked(level).is_err());
assert_eq!(rt.reasoning_level(), ReasoningLevel::Low);
}
}
#[test]
fn checked_rejects_max_ultra_for_unknown_codex_model() {
let mut rt = codex_runtime("gpt-unknown-future");
rt.set_reasoning_level(ReasoningLevel::Low);
for level in [ReasoningLevel::Max, ReasoningLevel::Ultra] {
let err = rt.set_reasoning_level_checked(level).unwrap_err();
assert!(err.contains("no capability metadata"), "{err}");
assert_eq!(rt.reasoning_level(), ReasoningLevel::Low);
}
}
#[test]
fn checked_enforces_xai_capability_matrix_no_mutation_on_err() {
let mut rt = Runtime::new_headless();
rt.set_model("xai-auth/grok-4.5".to_string());
assert_eq!(rt.reasoning_level(), ReasoningLevel::High);
for level in [
ReasoningLevel::Off,
ReasoningLevel::XHigh,
ReasoningLevel::Ultra,
] {
let before = rt.reasoning_level();
assert!(rt.set_reasoning_level_checked(level).is_err(), "{level}");
assert_eq!(rt.reasoning_level(), before);
}
rt.set_reasoning_level_checked(ReasoningLevel::Low)
.expect("grok-4.5 supports low");
assert_eq!(rt.reasoning_level(), ReasoningLevel::Low);
}
#[test]
fn checked_rejects_named_on_intrinsic_xai_model_and_defaults_adaptive() {
let mut rt = Runtime::new_headless();
rt.set_model("xai-auth/grok-4.3".to_string());
assert_eq!(rt.reasoning_level(), ReasoningLevel::Adaptive);
assert!(rt
.set_reasoning_level_checked(ReasoningLevel::Medium)
.is_err());
assert_eq!(rt.reasoning_level(), ReasoningLevel::Adaptive);
rt.set_reasoning_level_checked(ReasoningLevel::Adaptive)
.unwrap();
}
#[test]
fn explicit_level_survives_xai_model_switch() {
let mut rt = Runtime::new_headless();
rt.set_model("xai-auth/grok-4.5".to_string());
rt.set_reasoning_level_checked(ReasoningLevel::Low).unwrap();
rt.set_model("xai-auth/grok-4.5-latest".to_string());
assert_eq!(
rt.reasoning_level(),
ReasoningLevel::Low,
"explicit level must not be overwritten by the model default"
);
}
#[test]
fn explicit_unsupported_level_is_clamped_to_model_default_on_switch() {
let mut rt = Runtime::new_headless();
rt.set_reasoning_level_explicit(ReasoningLevel::XHigh);
let clamp = rt
.try_set_model("xai-auth/grok-4.6".to_string())
.expect("model switch succeeds");
assert_eq!(
clamp,
Some(ReasoningClamp {
from: ReasoningLevel::XHigh,
to: ReasoningLevel::High,
})
);
assert_eq!(rt.reasoning_level(), ReasoningLevel::High);
assert!(
rt.is_reasoning_explicit(),
"clamp keeps explicit provenance"
);
assert_eq!(rt.model(), "xai-auth/grok-4.6");
}
#[test]
fn explicit_supported_level_is_not_clamped_on_switch() {
let mut rt = Runtime::new_headless();
rt.set_reasoning_level_explicit(ReasoningLevel::High);
let clamp = rt.try_set_model("xai-auth/grok-4.6".to_string()).unwrap();
assert_eq!(clamp, None);
assert_eq!(rt.reasoning_level(), ReasoningLevel::High);
assert!(rt.is_reasoning_explicit());
}
#[test]
fn non_explicit_default_application_reports_no_clamp() {
let mut rt = Runtime::new_headless();
rt.set_reasoning_level(ReasoningLevel::XHigh);
let clamp = rt.try_set_model("xai-auth/grok-4.6".to_string()).unwrap();
assert_eq!(clamp, None);
assert_eq!(rt.reasoning_level(), ReasoningLevel::High);
assert!(!rt.is_reasoning_explicit());
}
#[test]
fn explicit_max_clamps_to_validating_option_on_multi_agent_model() {
let model = "xai-auth/grok-4.20-multi-agent-0309";
let mut rt = Runtime::new_headless();
rt.set_reasoning_level_explicit(ReasoningLevel::Max);
let clamp = rt
.try_set_model(model.to_string())
.unwrap()
.expect("Max is not supported: must clamp");
assert_eq!(clamp.from, ReasoningLevel::Max);
assert!(
crate::runtime::openai::catalog::validation::validate_reasoning_mutation(
model, clamp.to
)
.is_ok(),
"clamped level {} must validate",
clamp.to
);
assert_eq!(rt.reasoning_level(), clamp.to);
assert!(rt.is_reasoning_explicit());
}
#[test]
fn set_model_returns_clamp() {
let mut rt = Runtime::new_headless();
rt.set_reasoning_level_explicit(ReasoningLevel::XHigh);
let clamp = rt.set_model("xai-auth/grok-4.6".to_string());
assert_eq!(clamp.map(|c| c.to), Some(ReasoningLevel::High));
}
#[test]
fn restore_session_reasoning_clamps_after_reapply() {
let mut rt = Runtime::new_headless();
rt.set_model("xai-auth/grok-4.6".to_string());
let clamp = rt.restore_session_reasoning("xhigh");
assert_eq!(
clamp,
Some(ReasoningClamp {
from: ReasoningLevel::XHigh,
to: ReasoningLevel::High,
})
);
assert_eq!(rt.reasoning_level(), ReasoningLevel::High);
assert!(rt.is_reasoning_explicit());
assert!(rt.set_reasoning_level_checked(rt.reasoning_level()).is_ok());
}
#[test]
fn restore_session_reasoning_keeps_supported_level() {
let mut rt = Runtime::new_headless();
rt.set_model("xai-auth/grok-4.6".to_string());
assert_eq!(rt.restore_session_reasoning("low"), None);
assert_eq!(rt.reasoning_level(), ReasoningLevel::Low);
assert!(rt.is_reasoning_explicit());
}
#[test]
fn apply_config_clamps_unsupported_explicit_level_against_config_model() {
let mut rt = Runtime::new_headless();
rt.apply_config(&crate::config::SynapsConfig {
model: Some("xai-auth/grok-4.6".to_string()),
thinking_level: Some(ReasoningLevel::XHigh),
..Default::default()
});
assert_eq!(rt.model(), "xai-auth/grok-4.6");
assert_eq!(rt.reasoning_level(), ReasoningLevel::High);
assert!(rt.is_reasoning_explicit());
assert!(rt.set_reasoning_level_checked(rt.reasoning_level()).is_ok());
}
}
#[cfg(test)]
mod codex_execution_preflight_tests {
use super::*;
use crate::runtime::openai::catalog::CodexRequestRole;
use agent_core::prompt::QualifiedModelId;
use agent_core::reasoning::ReasoningLevel;
fn ultra_runtime() -> Runtime {
let mut runtime = Runtime::new_headless();
runtime.set_model("openai-codex/gpt-5.6-sol".to_string());
runtime.set_reasoning_level_explicit(ReasoningLevel::Ultra);
runtime
}
fn install_baseline(runtime: &mut Runtime) {
let foreground =
QualifiedModelId::parse("openai-codex/gpt-5.6-sol").expect("qualified model");
let orchestration = crate::orchestration::OrchestrationRuntime::baseline(foreground, 3, 8)
.expect("baseline orchestration");
runtime.install_orchestration(Arc::new(orchestration));
}
#[tokio::test]
async fn foreground_ultra_preflight_requires_orchestration() {
let runtime = ultra_runtime();
let error = runtime
.validate_request_preflight()
.await
.expect_err("Ultra without orchestration must fail closed");
assert!(error.to_string().contains("orchestration"), "{error}");
}
#[tokio::test]
async fn foreground_ultra_preflight_requires_actionable_subagent_tools() {
let mut runtime = ultra_runtime();
install_baseline(&mut runtime);
runtime.set_tools(ToolRegistry::without_subagent());
let error = runtime
.validate_request_preflight()
.await
.expect_err("Ultra without subagent tools must fail closed");
assert!(error.to_string().contains("subagent"), "{error}");
}
#[tokio::test]
async fn foreground_ultra_preflight_accepts_exact_model_policy_and_tools() {
let mut runtime = ultra_runtime();
install_baseline(&mut runtime);
runtime
.validate_request_preflight()
.await
.expect("fully provisioned Ultra must pass preflight");
}
#[tokio::test]
async fn worker_ultra_preflight_never_requires_or_enables_orchestration() {
let mut runtime = ultra_runtime();
runtime.set_codex_request_role(CodexRequestRole::Worker);
runtime.set_tools(ToolRegistry::without_subagent());
runtime
.validate_request_preflight()
.await
.expect("worker Ultra is underlying max without proactive orchestration");
assert_eq!(runtime.codex_request_role(), CodexRequestRole::Worker);
}
#[tokio::test]
async fn non_codex_ultra_preflight_fails_before_provider_work() {
let mut runtime = Runtime::new_headless();
runtime.set_model("anthropic/claude-opus-4-7".to_string());
runtime.set_reasoning_level_explicit(ReasoningLevel::Ultra);
let error = runtime
.validate_request_preflight()
.await
.expect_err("non-Codex Ultra must fail closed");
assert!(
error.to_string().contains("unsupported_reasoning_level"),
"{error}"
);
}
#[tokio::test]
async fn compaction_preflight_validates_target_before_credentials() {
let mut runtime = ultra_runtime();
runtime.set_compaction_model(Some("claude-sonnet-4-6".to_string()));
{
let mut auth = runtime.auth.write().await;
auth.auth_token.clear();
auth.auth_type = "none".to_string();
auth.refresh_token = None;
auth.token_expires = None;
}
let error = runtime
.compact_call(Vec::new())
.await
.expect_err("Codex Ultra cannot silently cross into Anthropic compaction");
assert!(
error.to_string().contains("authoritative exact-model"),
"preflight must reject the target before auth access: {error}"
);
}
}
#[cfg(test)]
mod http_client_timeout_tests {
use futures::StreamExt;
use std::time::Duration;
use tokio::io::AsyncWriteExt;
const CHUNKED_HEAD: &[u8] =
b"HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\ntransfer-encoding: chunked\r\n\r\n";
fn chunk(data: &str) -> Vec<u8> {
format!("{:x}\r\n{}\r\n", data.len(), data).into_bytes()
}
async fn spawn_chunked_server(n_chunks: usize, gap: Duration, terminate: bool) -> String {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (mut sock, _) = listener.accept().await.unwrap();
let mut buf = [0u8; 4096];
use tokio::io::AsyncReadExt;
let _ = sock.read(&mut buf).await;
sock.write_all(CHUNKED_HEAD).await.unwrap();
for i in 0..n_chunks {
sock.write_all(&chunk(&format!("data {i}\n")))
.await
.unwrap();
sock.flush().await.unwrap();
tokio::time::sleep(gap).await;
}
if terminate {
sock.write_all(b"0\r\n\r\n").await.unwrap();
} else {
tokio::time::sleep(Duration::from_secs(60)).await;
}
});
format!("http://{addr}/stream")
}
async fn drain(
client: &reqwest::Client,
url: &str,
) -> Result<usize, Box<dyn std::error::Error + Send + Sync>> {
let resp = client.get(url).send().await?;
let mut stream = resp.bytes_stream();
let mut total = 0usize;
while let Some(c) = stream.next().await {
total += c?.len();
}
Ok(total)
}
#[tokio::test]
async fn active_stream_outliving_read_timeout_survives() {
let url = spawn_chunked_server(15, Duration::from_millis(100), true).await;
let client = super::build_http_client(Duration::from_millis(400)).expect("client builds");
let total = drain(&client, &url)
.await
.expect("active stream must never be killed by the idle timeout");
assert!(total > 0);
}
#[tokio::test]
async fn stalled_stream_is_killed_by_read_timeout() {
let url = spawn_chunked_server(2, Duration::from_millis(10), false).await;
let client = super::build_http_client(Duration::from_millis(300)).expect("client builds");
let start = std::time::Instant::now();
let err = drain(&client, &url)
.await
.expect_err("stalled stream must be detected");
assert!(
start.elapsed() < Duration::from_secs(5),
"stall must be detected promptly, took {:?}",
start.elapsed()
);
let msg = crate::core::error::error_chain_string(err.as_ref());
assert!(
msg.contains("timed out") || msg.contains("timeout"),
"must be a timeout error: {msg}"
);
}
#[tokio::test]
async fn hung_headers_are_killed_by_read_timeout() {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move {
let (_sock, _) = listener.accept().await.unwrap();
tokio::time::sleep(Duration::from_secs(60)).await;
});
let client = super::build_http_client(Duration::from_millis(300)).expect("client builds");
let start = std::time::Instant::now();
let err = client
.get(format!("http://{addr}/codex/responses"))
.send()
.await
.expect_err("hung request must be detected");
assert!(
start.elapsed() < Duration::from_secs(5),
"hung headers must be detected promptly, took {:?}",
start.elapsed()
);
assert!(err.is_timeout(), "must be a timeout: {err:?}");
}
}
#[cfg(test)]
mod memory_context_provider_tests {
use super::{memory_context, Runtime};
use std::sync::Arc;
async fn memory_runtime_with_providers(
plugins: &[(&str, &str)],
) -> (
Runtime,
std::sync::Arc<crate::extensions::lease::ExtensionRuntimeManager>,
) {
let mut manager = crate::extensions::manager::ExtensionManager::new(Arc::new(
crate::extensions::hooks::HookBus::new(),
));
manager.set_progressive_deferral(true);
for (plugin, provider) in plugins {
let manifest: crate::extensions::manifest::ExtensionManifest =
serde_json::from_value(serde_json::json!({
"runtime": "process",
"command": "/bin/false",
"permissions": ["context_providers.register"],
"deferred": {
"context_providers": [{
"id": provider,
"capability": "project-memory",
"description": "test context provider",
"schema_version": 1
}]
}
}))
.expect("manifest parses");
manager
.load(plugin, &manifest)
.await
.expect("deferred context-provider load succeeds without spawning");
}
let extension_runtime = manager.extension_runtime();
let mut runtime = Runtime::new_headless();
runtime.install_extension_runtime(std::sync::Arc::clone(&extension_runtime));
(runtime, extension_runtime)
}
fn assert_memory_off_no_lease(runtime: &Runtime) {
let status = runtime.memory_context_status();
assert_eq!(
status.durable,
memory_context::DurableStatus::Off,
"state must stay Off"
);
assert_eq!(
status.one_shot,
memory_context::OneShotStatus::Idle,
"no one-shot lease may exist"
);
assert!(
runtime.memory_bound_providers_for_test().is_empty(),
"no provider may be bound"
);
}
#[tokio::test]
async fn memory_backend_exclusive_rejects_extension_recall_capture_and_history() {
for selector in ["axel", "invalid-selector"] {
let (mut runtime, _manager) =
memory_runtime_with_providers(&[("memory-test", "notes")]).await;
let provider = runtime.resolve_memory_provider(None).unwrap();
runtime.apply_config(&agent_core::config::load_config_from_str(&format!(
"memory.backend = {selector}\n"
)));
assert!(runtime.memory_backend.exclusive());
assert!(matches!(
runtime.resolve_memory_provider(None),
Err(memory_context::MemoryContextError::ProviderNotRegistered)
));
assert!(matches!(
runtime.resolve_memory_provider(Some(provider.as_str())),
Err(memory_context::MemoryContextError::ProviderNotRegistered)
));
assert!(matches!(
runtime.memory_history_confirm(),
Err(
super::memory_history::HistoryImportError::CaptureProviderUnavailable
| super::memory_history::HistoryImportError::ConsentRequired
)
));
assert_memory_off_no_lease(&runtime);
let mut messages = vec![Arc::new(
serde_json::json!({"role": "user", "content": "test"}),
)];
let original = messages.clone();
runtime.apply_turn_memory_recall(&mut messages).await;
assert_eq!(messages, original);
let clone = runtime.clone();
assert!(clone.memory_backend.exclusive());
assert_eq!(clone.memory_backend.base(), runtime.memory_backend.base());
}
}
#[tokio::test]
async fn memory_backend_first_config_is_immutable_and_changes_require_restart() {
for (first, different) in [("legacy", "axel"), ("axel", "legacy")] {
let mut runtime = Runtime::new_headless();
let config =
agent_core::config::load_config_from_str(&format!("memory.backend = {first}\n"));
runtime.apply_config(&config);
let binding = runtime.memory_backend.clone();
runtime.apply_config(&config);
assert!(!runtime
.memory_backend_reconfigure_denied
.load(std::sync::atomic::Ordering::SeqCst));
if let (Ok(a), Ok(b)) = (binding.scope(), runtime.memory_backend.scope()) {
assert!(std::ptr::eq(a, b));
}
let clone = runtime.clone();
runtime.apply_config(&agent_core::config::load_config_from_str(&format!(
"memory.backend = {different}\n"
)));
assert_eq!(runtime.memory_backend.exclusive(), binding.exclusive());
assert_eq!(
runtime.memory_backend_config.as_ref(),
Some(&config.memory_backend)
);
for denied in [&runtime, &clone] {
let error = denied
.validate_request_preflight()
.await
.unwrap_err()
.to_string();
assert!(error.contains("restart required"), "{error}");
assert!(denied.resolve_memory_provider(None).is_err());
}
runtime.apply_config(&config);
assert!(runtime.validate_request_preflight().await.is_err());
}
}
#[tokio::test]
async fn memory_backend_path_changes_also_require_restart() {
for field in ["executable", "brain"] {
let mut runtime = Runtime::new_headless();
let config = crate::config::MemoryBackendConfig::default();
runtime.apply_memory_backend_config(&config);
let mut different = config.clone();
let path = std::env::temp_dir().join("synaps-memory-config-test");
if field == "executable" {
different.executable = Some(path);
} else {
different.brain = Some(path);
}
runtime.apply_memory_backend_config(&different);
assert_eq!(runtime.memory_backend_config.as_ref(), Some(&config));
assert!(!runtime.memory_backend.exclusive());
let error = runtime
.validate_request_preflight()
.await
.unwrap_err()
.to_string();
assert!(error.contains("restart required"), "{error}");
assert!(runtime.resolve_memory_provider(None).is_err());
}
}
fn default_mode_config(mode: &str, confirmed: bool) -> agent_core::config::MemoryConfig {
agent_core::config::MemoryConfig {
default_mode: mode.to_string(),
default_mode_confirmed: confirmed,
..Default::default()
}
}
#[test]
fn memory_default_mode_confirmed_grants_session_lease() {
for (name, mode) in [
(
"recall_each_prompt",
memory_context::MemoryContextMode::RecallEachPrompt,
),
(
"capture_only",
memory_context::MemoryContextMode::CaptureOnly,
),
(
"capture_and_recall",
memory_context::MemoryContextMode::CaptureAndRecall,
),
] {
let runtime = Runtime::new_headless();
let status = runtime
.apply_memory_default_mode(&default_mode_config(name, true))
.expect("a confirmed non-off default applies")
.expect("legacy backend grants");
assert!(
matches!(status.durable, memory_context::DurableStatus::Active { mode: m, .. } if m == mode),
"{name}: got {:?}",
status.durable
);
assert_eq!(
runtime.memory_durable_proof_for_test(),
Some(memory_context::UserIntentProof::OperatorConfig),
"{name}"
);
}
let runtime = Runtime::new_headless();
runtime
.apply_memory_default_mode(&default_mode_config("recall_once", true))
.expect("applies")
.expect("grants");
assert_eq!(
runtime.memory_context_status().durable,
memory_context::DurableStatus::Off
);
assert_eq!(
runtime.memory_one_shot_proof_for_test(),
Some(memory_context::UserIntentProof::OperatorConfig)
);
}
#[test]
fn memory_default_mode_off_unknown_or_unconfirmed_grants_nothing() {
for (mode, confirmed) in [
("off", true),
("off", false),
("capture_and_recall", false),
("recall_once", false),
("recall_everything_forever", true),
] {
let runtime = Runtime::new_headless();
assert!(
runtime
.apply_memory_default_mode(&default_mode_config(mode, confirmed))
.is_none(),
"{mode}/{confirmed} must not apply"
);
assert_memory_off_no_lease(&runtime);
}
}
#[test]
fn memory_default_mode_keeps_existing_state_and_off_revokes() {
let runtime = Runtime::new_headless();
runtime
.memory_context_enable(
memory_context::MemoryContextMode::RecallEachPrompt,
memory_context::mint_explicit_command_proof(),
)
.unwrap();
assert!(runtime
.apply_memory_default_mode(&default_mode_config("capture_and_recall", true))
.is_none());
assert!(matches!(
runtime.memory_context_status().durable,
memory_context::DurableStatus::Active {
mode: memory_context::MemoryContextMode::RecallEachPrompt,
..
}
));
assert!(matches!(
runtime.memory_durable_proof_for_test(),
Some(memory_context::UserIntentProof::ExplicitCommand { .. })
));
let runtime = Runtime::new_headless();
runtime
.apply_memory_default_mode(&default_mode_config("capture_and_recall", true))
.unwrap()
.unwrap();
runtime.memory_context_disable();
assert_memory_off_no_lease(&runtime);
}
#[test]
fn memory_default_mode_unusable_backend_fails_closed() {
let mut runtime = Runtime::new_headless();
runtime.apply_config(&agent_core::config::load_config_from_str(
"memory.backend = invalid-not-axel-not-legacy\n",
));
assert!(runtime
.apply_memory_default_mode(&default_mode_config("capture_and_recall", true))
.expect("a confirmed default is attempted")
.is_err());
assert_memory_off_no_lease(&runtime);
}
#[test]
fn memory_backend_apply_config_revokes_preexisting_legacy_lease() {
let mut runtime = Runtime::new_headless();
runtime
.memory_context_enable(
memory_context::MemoryContextMode::CaptureAndRecall,
memory_context::UserIntentProof::ExplicitCommand {
command_id: memory_context::mint_explicit_command_id(),
},
)
.unwrap();
let lease = runtime
.memory_context_lock()
.capture_lease_at(std::time::SystemTime::now())
.unwrap();
runtime.apply_config(&agent_core::config::load_config_from_str(
"memory.backend = axel\n",
));
assert_memory_off_no_lease(&runtime);
assert!(runtime.extension_capture_provider(&lease).is_none());
}
#[test]
fn axel_invalid_config_cannot_grant_consent_or_fallback() {
let mut runtime = Runtime::new_headless();
runtime.apply_config(&agent_core::config::load_config_from_str(
"memory.backend = invalid-not-axel-not-legacy\n",
));
assert!(runtime.memory_backend.exclusive());
assert!(runtime.resolve_memory_provider(None).is_err());
assert_memory_off_no_lease(&runtime);
}
#[tokio::test]
async fn memory_enable_unregistered_provider_fails_closed_grants_nothing() {
let (runtime, _ext) =
memory_runtime_with_providers(&[("axel-memory-manager", "project-memory")]).await;
for requested in ["no-such-provider", "bad:id"] {
let err = runtime
.memory_context_enable_resolved(
memory_context::MemoryContextMode::CaptureAndRecall,
memory_context::mint_explicit_command_proof(),
Some(requested),
)
.expect_err("unregistered provider must fail closed");
assert_eq!(
err,
memory_context::MemoryContextError::ProviderNotRegistered
);
assert_memory_off_no_lease(&runtime);
}
let (runtime, _ext) = memory_runtime_with_providers(&[]).await;
let err = runtime
.memory_context_enable(
memory_context::MemoryContextMode::CaptureAndRecall,
memory_context::mint_explicit_command_proof(),
)
.expect_err("empty catalog must fail closed");
assert_eq!(
err,
memory_context::MemoryContextError::ProviderNotRegistered
);
assert_memory_off_no_lease(&runtime);
let err = runtime
.memory_context_recall_once(memory_context::mint_explicit_command_proof())
.expect_err("one-shot grant must validate the provider too");
assert_eq!(
err,
memory_context::MemoryContextError::ProviderNotRegistered
);
assert_memory_off_no_lease(&runtime);
}
#[tokio::test]
async fn memory_enable_exactly_one_declared_provider_records_exact_identity() {
const ADDRESS: &str = "extension:axel-memory-manager:project-memory";
let (runtime, _ext) =
memory_runtime_with_providers(&[("axel-memory-manager", "project-memory")]).await;
let status = runtime
.memory_context_enable(
memory_context::MemoryContextMode::CaptureAndRecall,
memory_context::mint_explicit_command_proof(),
)
.expect("unique declared provider must resolve");
assert!(matches!(
status.durable,
memory_context::DurableStatus::Active { .. }
));
let bound = runtime.memory_bound_providers_for_test();
assert_eq!(bound.len(), 1);
assert_eq!(bound[0].as_str(), ADDRESS, "exact runtime address recorded");
let (runtime, _ext) =
memory_runtime_with_providers(&[("axel-memory-manager", "project-memory")]).await;
runtime
.memory_context_enable_resolved(
memory_context::MemoryContextMode::RecallEachPrompt,
memory_context::mint_explicit_command_proof(),
Some("project-memory"),
)
.expect("explicit exact id must resolve");
assert_eq!(
runtime.memory_bound_providers_for_test()[0].as_str(),
ADDRESS
);
}
#[tokio::test]
async fn memory_enable_ambiguous_overlapping_declarations_fails_closed() {
let (runtime, _ext) = memory_runtime_with_providers(&[
("mem-a", "project-memory"),
("mem-b", "project-memory"),
])
.await;
let err = runtime
.memory_context_enable(
memory_context::MemoryContextMode::CaptureAndRecall,
memory_context::mint_explicit_command_proof(),
)
.expect_err("overlapping declarations must fail closed");
assert_eq!(err, memory_context::MemoryContextError::ProviderAmbiguous);
assert_memory_off_no_lease(&runtime);
let err = runtime
.memory_context_enable_resolved(
memory_context::MemoryContextMode::CaptureOnly,
memory_context::mint_explicit_command_proof(),
Some("project-memory"),
)
.expect_err("same id declared by two plugins is ambiguous");
assert_eq!(err, memory_context::MemoryContextError::ProviderAmbiguous);
assert_memory_off_no_lease(&runtime);
}
#[tokio::test]
async fn memory_disable_revokes_runtime_lease_without_spawn_no_panic() {
let (runtime, ext) =
memory_runtime_with_providers(&[("axel-memory-manager", "project-memory")]).await;
runtime
.memory_context_enable(
memory_context::MemoryContextMode::CaptureAndRecall,
memory_context::mint_explicit_command_proof(),
)
.expect("enable succeeds");
assert_eq!(
ext.lease_count(),
0,
"granting a memory lease spawns nothing"
);
let status = runtime.memory_context_disable();
assert_eq!(status.durable, memory_context::DurableStatus::Off);
assert_memory_off_no_lease(&runtime);
assert_eq!(
ext.lease_count(),
0,
"revocation of a never-spawned lease is a no-op"
);
let status = runtime.memory_context_disable();
assert_eq!(status.durable, memory_context::DurableStatus::Off);
assert_eq!(ext.lease_count(), 0);
}
#[test]
fn memory_context_enable_disable_emit_metadata_only_events() {
memory_context::drain_captured_memory_events_for_test();
let runtime = Runtime::new_headless();
runtime
.memory_context_enable(
memory_context::MemoryContextMode::Off,
memory_context::mint_explicit_command_proof(),
)
.expect_err("Off is not an enable mode");
assert!(
memory_context::drain_captured_memory_events_for_test().is_empty(),
"failed enable must emit no event"
);
runtime
.memory_context_enable(
memory_context::MemoryContextMode::CaptureAndRecall,
memory_context::mint_explicit_command_proof(),
)
.expect("enable succeeds");
runtime.memory_context_disable();
let events = memory_context::drain_captured_memory_events_for_test();
assert_eq!(
events
.iter()
.map(|event| (event.event, event.outcome))
.collect::<Vec<_>>(),
vec![
("memory_context.enabled", "enabled"),
("memory_context.disabled", "disabled"),
],
);
assert_eq!(events[0].mode, Some("capture_and_recall"));
assert_eq!(events[1].mode, Some("off"));
let cwd = std::env::current_dir()
.expect("cwd")
.to_string_lossy()
.into_owned();
for event in &events {
let json = serde_json::to_string(event).expect("event serializes");
assert!(
json.contains("\"project_digest\":\"p") || json.contains("project-cwd-"),
"digest identity expected; got: {json}"
);
assert!(
!json.contains(&cwd),
"raw project path leaked into event JSON: {json}"
);
}
}
#[tokio::test]
async fn memory_session_end_guard_drop_fresh_construction_is_off_no_lease() {
let (runtime, ext) =
memory_runtime_with_providers(&[("axel-memory-manager", "project-memory")]).await;
runtime
.memory_context_enable(
memory_context::MemoryContextMode::CaptureAndRecall,
memory_context::mint_explicit_command_proof(),
)
.expect("enable succeeds");
drop(runtime);
assert_eq!(
ext.lease_count(),
0,
"session end must leave no runtime lease (never-spawned reap is a no-op)"
);
let mut fresh = Runtime::new_headless();
fresh.install_extension_runtime(std::sync::Arc::clone(&ext));
assert_memory_off_no_lease(&fresh);
assert_eq!(ext.lease_count(), 0);
}
}