use super::*;
impl AcpServer {
pub fn new(config: AcpServerConfig) -> Self {
Self::new_with_output(config, AcpOutput::stdout())
}
pub fn new_with_output(config: AcpServerConfig, output: AcpOutput) -> Self {
let notifier_output = output.clone();
let known_sessions = super::session_watch::KnownSessions::default();
let notifier_sessions = known_sessions.clone();
let llm_config_overrides = config.llm_config_overrides.clone();
let runtime_provider_endpoint_overrides = config
.runtime_configurator
.runtime_provider_endpoint_overrides();
let llm_capability_overrides = config.llm_capability_overrides.clone();
let concurrent_controls = ConcurrentSessionControls::new(
config.auth_policy.methods.is_empty() || config.authenticated_principal.is_some(),
serde_json::json!({
"authMethods": config.auth_policy.acp_auth_methods(),
}),
);
Self {
descriptor: AdapterDescriptor {
id: "acp".to_string(),
caller_shape: "agent-session".to_string(),
supports_streaming: true,
supports_cancel: true,
},
pipeline: config.pipeline,
auth_policy: config.auth_policy,
authenticated_principal: config.authenticated_principal,
runtime_configurator: config.runtime_configurator,
sessions: HashMap::new(),
concurrent_controls,
timeline_subscriptions: HashMap::new(),
next_id: AtomicU64::new(1),
pending: Arc::new(Mutex::new(HashMap::new())),
session_cancellations: Arc::new(std::sync::Mutex::new(HashMap::new())),
output,
compile_cache: None,
vm_baseline_cache: None,
profile: config.profile,
llm_config_overrides,
runtime_provider_endpoint_overrides,
llm_capability_overrides,
default_budget: config.budget,
sandbox: config.sandbox,
active_bulk_auth: std::sync::Mutex::new(None),
known_sessions,
_session_change_subscription: harn_vm::subscribe_session_changes(Arc::new(
super::session_watch::SessionInfoNotifier::new(notifier_output, notifier_sessions),
)),
}
}
pub(super) fn track_known_session(&self, session_id: &str) {
let mut known = self
.known_sessions
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
known.insert(session_id.to_string());
}
pub async fn handle_incoming_message(&mut self, msg: serde_json::Value) {
let provider_overrides = self.llm_config_overrides.clone();
let runtime_provider_endpoint_overrides = self.runtime_provider_endpoint_overrides.clone();
let capability_overrides = self.llm_capability_overrides.clone();
let dispatch: std::pin::Pin<Box<dyn std::future::Future<Output = ()> + '_>> =
Box::pin(self.handle_incoming_message_scoped(msg));
harn_vm::orchestration::scope_llm_runtime_overrides_with_provider_endpoints(
provider_overrides,
capability_overrides,
runtime_provider_endpoint_overrides,
dispatch,
)
.await;
}
pub(super) fn compile_pipeline_cached(
&mut self,
source: &str,
source_path: Option<&Path>,
target_pipeline: Option<&str>,
) -> Result<(harn_vm::Chunk, bool), String> {
let target_owned = target_pipeline.map(|s| s.to_string());
let cache_key = source_path.and_then(|path| {
std::fs::metadata(path)
.and_then(|m| m.modified())
.ok()
.map(|mtime| (path.to_path_buf(), mtime))
});
if let Some((ref path, mtime)) = cache_key {
if let Some(entry) = self.compile_cache.as_ref() {
if entry.path == *path
&& entry.mtime == mtime
&& entry.target_pipeline == target_owned
&& entry.source == source
{
return Ok((entry.chunk.clone(), true));
}
}
}
let chunk = match target_pipeline {
Some(name) => harn_vm::compile_source_named(source, name),
None => harn_vm::compile_source(source),
}
.map_err(|e| format!("Compilation error: {e}"))?;
if let Some((path, mtime)) = cache_key {
self.compile_cache = Some(CompileCacheEntry {
path,
mtime,
target_pipeline: target_owned,
source: source.to_string(),
chunk: chunk.clone(),
});
}
Ok((chunk, false))
}
pub(super) async fn prepare_vm_baseline_cached(
&mut self,
source: &str,
source_path: Option<&Path>,
target_pipeline: Option<&str>,
cwd: &Path,
project_root: &Path,
mode_id: &str,
) -> Result<(Option<harn_vm::VmBaseline>, Option<bool>, u64), String> {
let Some(source_path) = source_path else {
return Ok((None, None, 0));
};
let prepare_started = Instant::now();
let target_owned = target_pipeline.map(str::to_string);
let cache_key = std::fs::metadata(source_path)
.and_then(|m| m.modified())
.ok()
.map(|mtime| (source_path.to_path_buf(), mtime));
let project_root = Some(project_root.to_path_buf());
if let Some((ref path, mtime)) = cache_key {
if let Some(entry) = self.vm_baseline_cache.as_ref() {
if entry.path == *path
&& entry.mtime == mtime
&& entry.target_pipeline == target_owned
&& entry.source == source
&& entry.cwd == cwd
&& entry.project_root == project_root
&& entry.mode_id == mode_id
{
return Ok((
Some(entry.baseline.clone()),
Some(true),
prepare_started.elapsed().as_millis() as u64,
));
}
}
}
let baseline = execute::prepare_vm_baseline(
source,
source_path,
cwd,
project_root.as_deref(),
self.runtime_configurator.clone(),
)
.await?;
if let Some((path, mtime)) = cache_key {
self.vm_baseline_cache = Some(VmBaselineCacheEntry {
path,
mtime,
target_pipeline: target_owned,
source: source.to_string(),
cwd: cwd.to_path_buf(),
project_root,
mode_id: mode_id.to_string(),
baseline: baseline.clone(),
});
} else {
self.vm_baseline_cache = None;
}
Ok((
Some(baseline),
Some(false),
prepare_started.elapsed().as_millis() as u64,
))
}
pub(super) fn write_line(&self, line: &str) {
self.output.write_line(line);
}
pub(super) fn send_response(&self, id: &serde_json::Value, result: serde_json::Value) {
let response = harn_vm::jsonrpc::response(id.clone(), result);
if let Ok(line) = serde_json::to_string(&response) {
self.write_line(&line);
}
}
pub(super) fn send_error(&self, id: &serde_json::Value, code: i64, message: &str) {
let response = harn_vm::jsonrpc::error_response(id.clone(), code, message);
if let Ok(line) = serde_json::to_string(&response) {
self.write_line(&line);
}
}
pub(super) fn send_error_with_data(
&self,
id: &serde_json::Value,
code: i64,
message: &str,
data: serde_json::Value,
) {
let response = harn_vm::jsonrpc::error_response_with_data(id.clone(), code, message, data);
if let Ok(line) = serde_json::to_string(&response) {
self.write_line(&line);
}
}
pub(super) fn emit_control_outcome(
&self,
session_id: &str,
method: &str,
outcome: &str,
status: &str,
actor: serde_json::Value,
target: serde_json::Value,
reason: Option<&str>,
) {
harn_vm::agent_events::emit_event(&harn_vm::agent_events::AgentEvent::ControlOutcome {
session_id: session_id.to_string(),
control_id: control_id(),
method: method.to_string(),
outcome: outcome.to_string(),
status: status.to_string(),
actor,
target,
reason: reason.map(str::to_string),
metadata: serde_json::Value::Null,
});
}
pub(super) fn send_notification(&self, method: &str, params: serde_json::Value) {
let notification = harn_vm::jsonrpc::notification(method, params);
if let Ok(line) = serde_json::to_string(¬ification) {
self.write_line(&line);
}
}
pub(super) fn send_prompt_error(&self, id: &serde_json::Value, message: &str) {
self.send_prompt_failure(
id,
message,
harn_vm::llm::AgentTerminalClass::GenericThrow,
super::types::AcpPromptFailureFacts::default(),
);
}
pub(super) fn send_prompt_failure(
&self,
id: &serde_json::Value,
message: &str,
terminal_class: harn_vm::llm::AgentTerminalClass,
facts: super::types::AcpPromptFailureFacts,
) {
let data = super::types::AcpPromptErrorData::with_facts(terminal_class, facts);
self.send_error_with_data(
id,
-32000,
message,
serde_json::to_value(data).expect("ACP prompt error data must serialize"),
);
eprintln!("{message}");
}
pub(super) fn send_prompt_protocol_error(&self, id: &serde_json::Value, message: &str) {
let data = super::types::AcpPromptErrorData::new(
harn_vm::llm::AgentTerminalClass::AgentLoopProtocolFailure,
);
self.send_error_with_data(
id,
-32602,
message,
serde_json::to_value(data).expect("ACP prompt error data must serialize"),
);
eprintln!("{message}");
}
pub(super) fn next_session_id(&mut self) -> String {
uuid::Uuid::new_v4().to_string()
}
pub(super) fn register_session_cancellation(
&mut self,
session_id: &str,
) -> SessionCancellation {
let cancellation = SessionCancellation::default();
self.session_cancellations
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(session_id.to_string(), cancellation.clone());
cancellation
}
}