use std::collections::VecDeque;
use std::fmt;
use std::path::Path;
use std::thread::{self, JoinHandle};
use serde::de::DeserializeOwned;
use serde::Serialize;
use serde_json::{json, Value};
use tokio::sync::mpsc;
use crate::adapters::acp::{
run_acp_channel_server_with_existing_handle, AcpChannelHandle, AcpJsonRpcError,
AcpJsonRpcErrorResponse, AcpJsonRpcId, AcpJsonRpcRequest, AcpJsonRpcResponse, AcpServerConfig,
AcpSessionIdParams, AcpSessionInjectParams, AcpSessionNewParams, AcpSessionPromptParams,
AcpSessionPromptResult, AcpSessionRestoreResult, ACP_METHOD_INITIALIZE,
ACP_METHOD_SESSION_CANCEL, ACP_METHOD_SESSION_CLOSE, ACP_METHOD_SESSION_INJECT,
ACP_METHOD_SESSION_LOAD, ACP_METHOD_SESSION_NEW, ACP_METHOD_SESSION_PROMPT,
ACP_METHOD_SESSION_RESUME, HARN_AGENT_EVENT_METHOD,
};
const DEFAULT_THREAD_NAME: &str = "harn-acp-embed";
pub struct EmbeddedAgent {
request_tx: Option<mpsc::UnboundedSender<serde_json::Value>>,
response_rx: Option<mpsc::UnboundedReceiver<String>>,
handle: AcpChannelHandle,
thread: Option<JoinHandle<()>>,
}
impl EmbeddedAgent {
pub fn spawn(config: AcpServerConfig) -> Self {
Self::spawn_named(config, DEFAULT_THREAD_NAME)
}
pub fn spawn_named(config: AcpServerConfig, thread_name: impl Into<String>) -> Self {
let (request_tx, request_rx) = mpsc::unbounded_channel::<serde_json::Value>();
let (response_tx, response_rx) = mpsc::unbounded_channel::<String>();
let handle = AcpChannelHandle::default();
let worker_handle = handle.clone();
let thread = thread::Builder::new()
.name(thread_name.into())
.spawn(move || {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("EmbeddedAgent: build current-thread tokio runtime");
let server_future = run_acp_channel_server_with_existing_handle(
config,
request_rx,
response_tx,
worker_handle,
);
runtime.block_on(server_future);
})
.expect("EmbeddedAgent: spawn ACP worker thread");
Self {
request_tx: Some(request_tx),
response_rx: Some(response_rx),
handle,
thread: Some(thread),
}
}
pub fn requests(&self) -> mpsc::UnboundedSender<serde_json::Value> {
self.request_tx
.as_ref()
.expect("EmbeddedAgent request sender was taken by into_parts")
.clone()
}
pub fn take_responses(&mut self) -> Option<mpsc::UnboundedReceiver<String>> {
self.response_rx.take()
}
pub fn handle(&self) -> &AcpChannelHandle {
&self.handle
}
#[allow(clippy::type_complexity)]
pub fn into_parts(
mut self,
) -> (
mpsc::UnboundedSender<serde_json::Value>,
Option<mpsc::UnboundedReceiver<String>>,
AcpChannelHandle,
) {
let request_tx = self
.request_tx
.take()
.expect("EmbeddedAgent request sender was already taken");
let response_rx = self.response_rx.take();
let handle = self.handle.clone();
self.thread.take();
(request_tx, response_rx, handle)
}
pub fn shutdown(&self) {
self.handle.shutdown();
}
pub fn join(&mut self) -> thread::Result<()> {
self.handle.shutdown();
match self.thread.take() {
Some(thread) => thread.join(),
None => Ok(()),
}
}
}
pub type EmbeddedAgentResult<T> = Result<T, EmbeddedAgentError>;
#[derive(Debug)]
#[non_exhaustive]
pub enum EmbeddedAgentError {
Json(serde_json::Error),
RequestChannelClosed,
ResponseChannelUnavailable,
ResponseChannelClosed,
JsonRpc(AcpJsonRpcError),
View(String),
UnexpectedMessage(Value),
WorkerPanicked,
}
impl fmt::Display for EmbeddedAgentError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Json(error) => write!(f, "embedded ACP JSON error: {error}"),
Self::RequestChannelClosed => write!(f, "embedded ACP request channel closed"),
Self::ResponseChannelUnavailable => {
write!(f, "embedded ACP response receiver is unavailable")
}
Self::ResponseChannelClosed => write!(f, "embedded ACP response channel closed"),
Self::JsonRpc(error) => {
write!(
f,
"embedded ACP JSON-RPC error {}: {}",
error.code, error.message
)
}
Self::View(error) => write!(f, "embedded ACP view error: {error}"),
Self::UnexpectedMessage(value) => {
write!(f, "embedded ACP emitted an unexpected message: {value}")
}
Self::WorkerPanicked => write!(f, "embedded ACP worker thread panicked"),
}
}
}
impl std::error::Error for EmbeddedAgentError {}
impl From<serde_json::Error> for EmbeddedAgentError {
fn from(error: serde_json::Error) -> Self {
Self::Json(error)
}
}
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub enum EmbeddedAgentEvent {
SessionUpdate {
session_id: Option<String>,
update: Value,
raw: Value,
},
TimelineUpdate {
subscription_id: String,
update: Value,
raw: Value,
},
AgentEvent {
kind: Option<String>,
event: Value,
raw: Value,
},
HostRequest {
id: AcpJsonRpcId,
method: String,
params: Value,
raw: Value,
},
RequestCompleted {
id: AcpJsonRpcId,
result: Value,
raw: Value,
},
RequestFailed {
id: AcpJsonRpcId,
error: AcpJsonRpcError,
raw: Value,
},
Notification {
method: String,
params: Value,
raw: Value,
},
}
pub struct EmbeddedAgentClient {
agent: EmbeddedAgent,
request_tx: mpsc::UnboundedSender<Value>,
response_rx: mpsc::UnboundedReceiver<String>,
pending: VecDeque<EmbeddedAgentEvent>,
next_id: u64,
}
impl EmbeddedAgentClient {
pub async fn spawn(config: AcpServerConfig) -> EmbeddedAgentResult<Self> {
Self::spawn_named(config, DEFAULT_THREAD_NAME).await
}
pub async fn spawn_named(
config: AcpServerConfig,
thread_name: impl Into<String>,
) -> EmbeddedAgentResult<Self> {
let mut agent = EmbeddedAgent::spawn_named(config, thread_name);
let request_tx = agent.requests();
let response_rx = agent
.take_responses()
.ok_or(EmbeddedAgentError::ResponseChannelUnavailable)?;
agent.handle().wait_ready().await;
Ok(Self {
agent,
request_tx,
response_rx,
pending: VecDeque::new(),
next_id: 1,
})
}
pub fn handle(&self) -> &AcpChannelHandle {
self.agent.handle()
}
pub fn shutdown(&self) {
self.agent.shutdown();
}
pub fn join(&mut self) -> EmbeddedAgentResult<()> {
self.agent
.join()
.map_err(|_| EmbeddedAgentError::WorkerPanicked)
}
pub fn begin_request<P: Serialize>(
&mut self,
method: impl Into<String>,
params: P,
) -> EmbeddedAgentResult<u64> {
let id = self.next_request_id();
let request = AcpJsonRpcRequest::new(id, method, params).into_json_value()?;
self.request_tx
.send(request)
.map_err(|_| EmbeddedAgentError::RequestChannelClosed)?;
Ok(id)
}
pub async fn next_event(&mut self) -> EmbeddedAgentResult<EmbeddedAgentEvent> {
if let Some(event) = self.pending.pop_front() {
return Ok(event);
}
self.recv_wire_event().await
}
async fn recv_wire_event(&mut self) -> EmbeddedAgentResult<EmbeddedAgentEvent> {
let line = self
.response_rx
.recv()
.await
.ok_or(EmbeddedAgentError::ResponseChannelClosed)?;
let value: Value = serde_json::from_str(&line)?;
classify_embedded_agent_message(value)
}
pub async fn recv_request_result(&mut self, request_id: u64) -> EmbeddedAgentResult<Value> {
let mut buffered = std::mem::take(&mut self.pending);
loop {
let event = match self.recv_wire_event().await {
Ok(event) => event,
Err(error) => {
self.pending = buffered;
return Err(error);
}
};
match event {
EmbeddedAgentEvent::RequestCompleted { id, result, .. }
if acp_id_matches(&id, request_id) =>
{
self.pending = buffered;
return Ok(result);
}
EmbeddedAgentEvent::RequestFailed { id, error, .. }
if acp_id_matches(&id, request_id) =>
{
self.pending = buffered;
return Err(EmbeddedAgentError::JsonRpc(error));
}
other => buffered.push_back(other),
}
}
}
pub async fn request_value<P: Serialize>(
&mut self,
method: impl Into<String>,
params: P,
) -> EmbeddedAgentResult<Value> {
let request_id = self.begin_request(method, params)?;
self.recv_request_result(request_id).await
}
pub fn respond_to_host_request(
&self,
id: AcpJsonRpcId,
result: Value,
) -> EmbeddedAgentResult<()> {
self.request_tx
.send(json!({
"jsonrpc": "2.0",
"id": id,
"result": result,
}))
.map_err(|_| EmbeddedAgentError::RequestChannelClosed)
}
pub fn fail_host_request(
&self,
id: AcpJsonRpcId,
code: i64,
message: impl Into<String>,
data: Option<Value>,
) -> EmbeddedAgentResult<()> {
let mut error = json!({
"code": code,
"message": message.into(),
});
if let Some(data) = data {
error["data"] = data;
}
self.request_tx
.send(json!({
"jsonrpc": "2.0",
"id": id,
"error": error,
}))
.map_err(|_| EmbeddedAgentError::RequestChannelClosed)
}
pub fn answer_approval(&self, id: AcpJsonRpcId, result: Value) -> EmbeddedAgentResult<()> {
self.respond_to_host_request(id, result)
}
pub fn answer_auth_prompt(&self, id: AcpJsonRpcId, result: Value) -> EmbeddedAgentResult<()> {
self.respond_to_host_request(id, result)
}
pub async fn initialize(&mut self) -> EmbeddedAgentResult<Value> {
self.request_value(ACP_METHOD_INITIALIZE, json!({})).await
}
pub async fn start_run(
&mut self,
params: AcpSessionNewParams,
) -> EmbeddedAgentResult<AcpSessionRestoreResult> {
let result = self.request_value(ACP_METHOD_SESSION_NEW, params).await?;
restore_result_from_value(result)
}
pub async fn load_run(
&mut self,
session_id: impl Into<String>,
) -> EmbeddedAgentResult<AcpSessionRestoreResult> {
let result = self
.request_value(
ACP_METHOD_SESSION_LOAD,
AcpSessionIdParams::new(session_id.into()),
)
.await?;
restore_result_from_value(result)
}
pub async fn load_run_with_cwd(
&mut self,
session_id: impl Into<String>,
cwd: impl Into<String>,
) -> EmbeddedAgentResult<AcpSessionRestoreResult> {
let result = self
.request_value(
ACP_METHOD_SESSION_LOAD,
json!({"sessionId": session_id.into(), "cwd": cwd.into()}),
)
.await?;
restore_result_from_value(result)
}
pub async fn resume_run(
&mut self,
session_id: impl Into<String>,
) -> EmbeddedAgentResult<AcpSessionRestoreResult> {
let result = self
.request_value(
ACP_METHOD_SESSION_RESUME,
AcpSessionIdParams::new(session_id.into()),
)
.await?;
restore_result_from_value(result)
}
pub async fn send_user_input(
&mut self,
params: AcpSessionPromptParams,
) -> EmbeddedAgentResult<AcpSessionPromptResult> {
self.request_typed(ACP_METHOD_SESSION_PROMPT, params).await
}
pub fn begin_user_input(&mut self, params: AcpSessionPromptParams) -> EmbeddedAgentResult<u64> {
self.begin_request(ACP_METHOD_SESSION_PROMPT, params)
}
pub async fn inject_user_input(
&mut self,
params: AcpSessionInjectParams,
) -> EmbeddedAgentResult<Value> {
self.request_value(ACP_METHOD_SESSION_INJECT, params).await
}
pub async fn cancel_session(
&mut self,
session_id: impl Into<String>,
) -> EmbeddedAgentResult<Value> {
self.request_value(
ACP_METHOD_SESSION_CANCEL,
AcpSessionIdParams::new(session_id.into()),
)
.await
}
pub async fn close_session(
&mut self,
session_id: impl Into<String>,
) -> EmbeddedAgentResult<Value> {
self.request_value(
ACP_METHOD_SESSION_CLOSE,
AcpSessionIdParams::new(session_id.into()),
)
.await
}
pub async fn session_view(
&mut self,
session_id: impl Into<String>,
) -> EmbeddedAgentResult<harn_vm::orchestration::SessionView> {
self.request_typed(
harn_vm::orchestration::SESSION_VIEW_QUERY_METHOD,
json!({"sessionId": session_id.into()}),
)
.await
}
pub async fn session_view_for_run_path(
&mut self,
run_path: impl Into<String>,
session_id: Option<String>,
) -> EmbeddedAgentResult<harn_vm::orchestration::SessionView> {
let mut params = json!({"runPath": run_path.into()});
if let Some(session_id) = session_id {
params["sessionId"] = Value::String(session_id);
}
self.request_typed(harn_vm::orchestration::SESSION_VIEW_QUERY_METHOD, params)
.await
}
pub fn run_view_from_path(
path: impl AsRef<Path>,
) -> EmbeddedAgentResult<harn_vm::orchestration::RunView> {
let path = path.as_ref();
let run = harn_vm::orchestration::load_run_record(path)
.map_err(|error| EmbeddedAgentError::View(error.to_string()))?;
Ok(harn_vm::orchestration::build_run_view_with_path(
&run,
Some(path.to_string_lossy().to_string()),
))
}
pub async fn subscribe_session_events(
&mut self,
session_id: impl Into<String>,
) -> EmbeddedAgentResult<String> {
let result = self
.request_value(
harn_vm::session_timeline::SESSION_TIMELINE_SUBSCRIBE_METHOD,
json!({"sessionId": session_id.into()}),
)
.await?;
result
.get("subscriptionId")
.and_then(Value::as_str)
.map(str::to_string)
.ok_or(EmbeddedAgentError::UnexpectedMessage(result))
}
pub async fn unsubscribe_session_events(
&mut self,
subscription_id: impl Into<String>,
) -> EmbeddedAgentResult<Value> {
self.request_value(
harn_vm::session_timeline::SESSION_TIMELINE_UNSUBSCRIBE_METHOD,
json!({"subscriptionId": subscription_id.into()}),
)
.await
}
pub async fn pause_workflow(
&mut self,
session_id: impl Into<String>,
workflow_id: impl Into<String>,
) -> EmbeddedAgentResult<Value> {
self.request_value(
"workflow/pause",
json!({"sessionId": session_id.into(), "workflowId": workflow_id.into()}),
)
.await
}
pub async fn resume_workflow(
&mut self,
session_id: impl Into<String>,
workflow_id: impl Into<String>,
) -> EmbeddedAgentResult<Value> {
self.request_value(
"workflow/resume",
json!({"sessionId": session_id.into(), "workflowId": workflow_id.into()}),
)
.await
}
fn next_request_id(&mut self) -> u64 {
let id = self.next_id;
self.next_id = if id == u64::MAX { 1 } else { id + 1 };
id
}
async fn request_typed<P, R>(
&mut self,
method: impl Into<String>,
params: P,
) -> EmbeddedAgentResult<R>
where
P: Serialize,
R: DeserializeOwned,
{
let value = self.request_value(method, params).await?;
Ok(serde_json::from_value(value)?)
}
}
fn acp_id_matches(id: &AcpJsonRpcId, request_id: u64) -> bool {
match id {
AcpJsonRpcId::Number(value) => *value == request_id,
AcpJsonRpcId::String(value) => value == &request_id.to_string(),
AcpJsonRpcId::Null => false,
}
}
fn restore_result_from_value(mut value: Value) -> EmbeddedAgentResult<AcpSessionRestoreResult> {
if value.get("sessionId").is_none() {
if let Some(session_id) = value
.get("session")
.and_then(|session| session.get("sessionId"))
.cloned()
{
value["sessionId"] = session_id;
}
}
Ok(serde_json::from_value(value)?)
}
fn classify_embedded_agent_message(value: Value) -> EmbeddedAgentResult<EmbeddedAgentEvent> {
let raw = value.clone();
if value.get("id").is_some() {
if let Some(method) = value.get("method").and_then(Value::as_str) {
let id: AcpJsonRpcId = serde_json::from_value(value["id"].clone())?;
return Ok(EmbeddedAgentEvent::HostRequest {
id,
method: method.to_string(),
params: value.get("params").cloned().unwrap_or(Value::Null),
raw,
});
}
if value.get("error").is_some() {
let response: AcpJsonRpcErrorResponse = serde_json::from_value(value)?;
return Ok(EmbeddedAgentEvent::RequestFailed {
id: response.id,
error: response.error,
raw,
});
}
if value.get("result").is_some() {
let response: AcpJsonRpcResponse<Value> = serde_json::from_value(value)?;
return Ok(EmbeddedAgentEvent::RequestCompleted {
id: response.id,
result: response.result,
raw,
});
}
}
let Some(method) = value.get("method").and_then(Value::as_str) else {
return Err(EmbeddedAgentError::UnexpectedMessage(raw));
};
let params = value.get("params").cloned().unwrap_or(Value::Null);
match method {
"session/update" => Ok(EmbeddedAgentEvent::SessionUpdate {
session_id: params
.get("sessionId")
.and_then(Value::as_str)
.map(str::to_string),
update: params
.get("update")
.cloned()
.unwrap_or_else(|| params.clone()),
raw,
}),
harn_vm::session_timeline::SESSION_TIMELINE_UPDATE_METHOD => {
Ok(EmbeddedAgentEvent::TimelineUpdate {
subscription_id: params
.get("subscriptionId")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
update: params
.get("update")
.cloned()
.unwrap_or_else(|| params.clone()),
raw,
})
}
HARN_AGENT_EVENT_METHOD => {
let event = params
.get("event")
.cloned()
.unwrap_or_else(|| params.clone());
Ok(EmbeddedAgentEvent::AgentEvent {
kind: params
.get("kind")
.or_else(|| event.get("kind"))
.and_then(Value::as_str)
.map(str::to_string),
event,
raw,
})
}
_ => Ok(EmbeddedAgentEvent::Notification {
method: method.to_string(),
params,
raw,
}),
}
}
impl Drop for EmbeddedAgent {
fn drop(&mut self) {
if let Some(thread) = self.thread.take() {
self.handle.shutdown();
let _ = thread.join();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn block_on<T>(future: impl std::future::Future<Output = T>) -> T {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("test runtime")
.block_on(future)
}
async fn recv_json(rx: &mut mpsc::UnboundedReceiver<String>) -> serde_json::Value {
let line = rx.recv().await.expect("ACP response channel closed");
serde_json::from_str(&line).expect("valid JSON-RPC line")
}
#[test]
fn embedded_agent_round_trips_session_new_and_shuts_down() {
let mut agent = EmbeddedAgent::spawn(AcpServerConfig::new(None));
let requests = agent.requests();
let mut responses = agent.take_responses().expect("responses receiver");
block_on(agent.handle().wait_ready());
requests
.send(serde_json::json!({
"jsonrpc": "2.0",
"id": 1,
"method": "session/new",
"params": {"cwd": "."},
}))
.expect("send session/new");
let created = block_on(recv_json(&mut responses));
assert!(
created["result"]["sessionId"].as_str().is_some(),
"session/new should return a sessionId, got: {created}"
);
agent.shutdown();
assert!(agent.join().is_ok(), "worker thread should join cleanly");
assert!(agent.handle().is_shutdown());
assert!(agent.handle().is_terminated());
}
#[test]
fn shutdown_handle_terminates_idle_agent() {
let agent = EmbeddedAgent::spawn(AcpServerConfig::new(None));
let handle = agent.handle().clone();
block_on(handle.wait_ready());
assert!(!handle.is_terminated());
handle.shutdown();
block_on(handle.wait_terminated());
drop(agent); assert!(handle.is_shutdown());
}
#[test]
fn dropping_request_sender_terminates_agent() {
let agent = EmbeddedAgent::spawn(AcpServerConfig::new(None));
let (requests, _responses, handle) = agent.into_parts();
block_on(handle.wait_ready());
assert!(!handle.is_terminated());
drop(requests);
block_on(handle.wait_terminated());
assert!(
!handle.is_shutdown(),
"EOF teardown must not set the shutdown flag"
);
}
#[test]
fn into_parts_detaches_thread_and_keeps_channels_live() {
let agent = EmbeddedAgent::spawn(AcpServerConfig::new(None));
let (requests, responses, handle) = agent.into_parts();
let mut responses = responses.expect("responses receiver");
block_on(handle.wait_ready());
requests
.send(serde_json::json!({
"jsonrpc": "2.0",
"id": 7,
"method": "session/new",
"params": {"cwd": "."},
}))
.expect("send session/new");
let created = block_on(recv_json(&mut responses));
assert!(created["result"]["sessionId"].as_str().is_some());
handle.shutdown();
block_on(handle.wait_terminated());
}
#[test]
fn embedded_agent_client_wraps_session_lifecycle_and_views() {
let mut client = block_on(EmbeddedAgentClient::spawn(AcpServerConfig::new(None)))
.expect("spawn embedded client");
let created = block_on(client.start_run(AcpSessionNewParams::cwd(".")))
.expect("session/new through client");
assert!(!created.session_id.is_empty());
let resumed = block_on(client.resume_run(created.session_id.clone()))
.expect("session/resume through client");
assert_eq!(resumed.session_id, created.session_id);
let view =
block_on(client.session_view(created.session_id.clone())).expect("session view query");
assert_eq!(view.schema, harn_vm::orchestration::SESSION_VIEW_SCHEMA);
assert_eq!(view.session.session_id, Some(created.session_id));
client.shutdown();
client.join().expect("client worker joins");
}
#[test]
fn embedded_agent_client_can_drive_request_results_as_events() {
let mut client = block_on(EmbeddedAgentClient::spawn(AcpServerConfig::new(None)))
.expect("spawn embedded client");
let request_id = client
.begin_request(ACP_METHOD_SESSION_NEW, AcpSessionNewParams::cwd("."))
.expect("begin session/new");
let event = block_on(client.next_event()).expect("next event");
match event {
EmbeddedAgentEvent::RequestCompleted { id, result, .. } => {
assert!(acp_id_matches(&id, request_id));
assert!(result["sessionId"].as_str().is_some());
}
other => panic!("expected request completion, got {other:?}"),
}
client.shutdown();
client.join().expect("client worker joins");
}
#[test]
fn embedded_agent_client_preserves_buffered_events_while_waiting_for_result() {
let mut client = block_on(EmbeddedAgentClient::spawn(AcpServerConfig::new(None)))
.expect("spawn embedded client");
client.pending.push_back(EmbeddedAgentEvent::Notification {
method: "test/notification".to_string(),
params: serde_json::json!({"ok": true}),
raw: serde_json::json!({"method": "test/notification"}),
});
let created = block_on(client.start_run(AcpSessionNewParams::cwd(".")))
.expect("session/new through client");
assert!(!created.session_id.is_empty());
let buffered = block_on(client.next_event()).expect("buffered event");
match buffered {
EmbeddedAgentEvent::Notification { method, params, .. } => {
assert_eq!(method, "test/notification");
assert_eq!(params["ok"], true);
}
other => panic!("expected buffered notification, got {other:?}"),
}
client.shutdown();
client.join().expect("client worker joins");
}
#[test]
fn embedded_agent_client_classifies_host_requests_and_notifications() {
let host = classify_embedded_agent_message(serde_json::json!({
"jsonrpc": "2.0",
"id": "approval_1",
"method": "session/request_permission",
"params": {"toolCallId": "tool_1"},
}))
.expect("host request");
match host {
EmbeddedAgentEvent::HostRequest {
id, method, params, ..
} => {
assert_eq!(id, AcpJsonRpcId::String("approval_1".to_string()));
assert_eq!(method, "session/request_permission");
assert_eq!(params["toolCallId"], "tool_1");
}
other => panic!("expected host request, got {other:?}"),
}
let update = classify_embedded_agent_message(serde_json::json!({
"jsonrpc": "2.0",
"method": "session/update",
"params": {
"sessionId": "s1",
"update": {"sessionUpdate": "agent_message_chunk", "content": {"type": "text", "text": "hi"}}
},
}))
.expect("session update");
match update {
EmbeddedAgentEvent::SessionUpdate {
session_id, update, ..
} => {
assert_eq!(session_id.as_deref(), Some("s1"));
assert_eq!(update["sessionUpdate"], "agent_message_chunk");
}
other => panic!("expected session update, got {other:?}"),
}
let timeline = classify_embedded_agent_message(serde_json::json!({
"jsonrpc": "2.0",
"method": harn_vm::session_timeline::SESSION_TIMELINE_UPDATE_METHOD,
"params": {"subscriptionId": "sub_1", "update": {"schemaVersion": 1}},
}))
.expect("timeline update");
match timeline {
EmbeddedAgentEvent::TimelineUpdate {
subscription_id,
update,
..
} => {
assert_eq!(subscription_id, "sub_1");
assert_eq!(update["schemaVersion"], 1);
}
other => panic!("expected timeline update, got {other:?}"),
}
}
}