use agent_client_protocol_schema::ProtocolVersion;
use agent_client_protocol_schema::v1::{
CancelNotification, PermissionOption, RequestPermissionOutcome, RequestPermissionRequest,
SessionId, SessionUpdate, StopReason, ToolCall, ToolCallStatus, ToolCallUpdate,
};
use aion_core::{
ActivityEvent, ActivityEventKind, ActivityId, InterventionCapabilities, InterventionKind,
InterventionPrimitive, RunId, StopKind, WorkflowId,
};
use aion_integrations::HarnessError;
use aion_integrations::jsonrpc::JsonRpcResponse;
use chrono::Utc;
use serde::Serialize;
use serde_json::{Value, json};
use uuid::Uuid;
use crate::messages::MessageAccumulator;
use crate::permission::PermissionPolicy;
use crate::protocol;
pub type EventKind = (ActivityEventKind, bool);
pub const UPDATE_SOURCE_PREFIX: &str = "session/update";
#[derive(Clone, Debug)]
pub struct EventIdentity {
pub workflow_id: WorkflowId,
pub run_id: RunId,
pub activity_id: ActivityId,
pub attempt: u32,
}
impl EventIdentity {
#[must_use]
pub fn stamp(&self, worker_seq: u64, kind: EventKind) -> ActivityEvent {
let (kind, ephemeral) = kind;
ActivityEvent {
workflow_id: self.workflow_id.clone(),
run_id: self.run_id.clone(),
activity_id: self.activity_id.clone(),
attempt: self.attempt,
agent_id: Uuid::nil(),
agent_role: protocol::AGENT_ROLE.to_owned(),
emitted_at: Utc::now(),
worker_seq,
store_seq: None,
ephemeral,
kind,
}
}
}
pub fn gate_protocol_version(received: ProtocolVersion) -> Result<(), HarnessError> {
if received == protocol::PROTOCOL_VERSION {
return Ok(());
}
Err(HarnessError::protocol(format!(
"ACP protocol version mismatch: this adapter speaks version {}, the agent answered with \
version {} — the agent is incompatible with this client and the connection is refused",
protocol::PROTOCOL_VERSION.as_u16(),
received.as_u16(),
)))
}
#[must_use]
pub fn negotiated_capabilities() -> InterventionCapabilities {
InterventionCapabilities::from_primitives([InterventionPrimitive::Cancel])
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AcpNotification {
pub method: &'static str,
pub params: Value,
}
pub fn intervention_to_notification(
session: &SessionId,
kind: &InterventionKind,
) -> Result<AcpNotification, HarnessError> {
match kind {
InterventionKind::Cancel { .. } => {
let params = serde_json::to_value(CancelNotification::new(session.clone())).map_err(
|source| {
HarnessError::protocol(format!(
"session/cancel params are not encodable: {source}"
))
},
)?;
Ok(AcpNotification {
method: protocol::METHOD_SESSION_CANCEL,
params,
})
}
InterventionKind::InjectMessage { .. }
| InterventionKind::PauseResume { .. }
| InterventionKind::UpdateBudget { .. }
| InterventionKind::RespondToApproval { .. } => Err(
HarnessError::capability_not_supported(primitive_label(kind.primitive())),
),
}
}
pub(crate) fn primitive_label(primitive: InterventionPrimitive) -> &'static str {
match primitive {
InterventionPrimitive::InjectMessage => "inject_message",
InterventionPrimitive::Cancel => "cancel",
InterventionPrimitive::PauseResume => "pause_resume",
InterventionPrimitive::UpdateBudget => "update_budget",
InterventionPrimitive::RespondToApproval => "respond_to_approval",
}
}
#[must_use]
pub fn is_success(reason: StopReason) -> bool {
matches!(reason, StopReason::EndTurn)
}
#[must_use]
pub fn stop_reason_label(reason: StopReason) -> &'static str {
match reason {
StopReason::EndTurn => "end_turn",
StopReason::MaxTokens => "max_tokens",
StopReason::MaxTurnRequests => "max_turn_requests",
StopReason::Refusal => "refusal",
StopReason::Cancelled => "cancelled",
_ => "other",
}
}
#[must_use]
pub fn stop_kind(reason: StopReason) -> StopKind {
match reason {
StopReason::EndTurn => StopKind::EndTurn,
StopReason::MaxTokens | StopReason::MaxTurnRequests => StopKind::LimitReached,
StopReason::Cancelled => StopKind::Cancelled,
other => StopKind::Other {
reason: stop_reason_label(other).to_owned(),
},
}
}
pub fn update_to_kinds(
update: &SessionUpdate,
accumulator: &mut MessageAccumulator,
) -> Vec<EventKind> {
match update {
SessionUpdate::AgentMessageChunk(chunk) => accumulator.push_agent_chunk(chunk),
SessionUpdate::AgentThoughtChunk(chunk) => accumulator.push_thought_chunk(chunk),
SessionUpdate::UserMessageChunk(chunk) => accumulator.push_user_chunk(chunk),
SessionUpdate::ToolCall(call) => tool_call_kinds(call),
SessionUpdate::ToolCallUpdate(update) => tool_call_update_kinds(update),
SessionUpdate::Plan(plan) => vec![update_raw("plan", plan)],
SessionUpdate::AvailableCommandsUpdate(commands) => {
vec![update_raw("available_commands_update", commands)]
}
SessionUpdate::CurrentModeUpdate(mode) => vec![update_raw("current_mode_update", mode)],
SessionUpdate::ConfigOptionUpdate(config) => {
vec![update_raw("config_option_update", config)]
}
SessionUpdate::SessionInfoUpdate(info) => vec![update_raw("session_info_update", info)],
SessionUpdate::UsageUpdate(usage) => vec![update_raw("usage_update", usage)],
other => vec![update_raw("unrecognised", other)],
}
}
fn tool_call_kinds(call: &ToolCall) -> Vec<EventKind> {
let mut events = vec![(
ActivityEventKind::ToolCall {
tool: call.title.clone(),
call_id: call.tool_call_id.to_string(),
input: call.raw_input.clone().unwrap_or(Value::Null),
},
false,
)];
if let Some(is_error) = terminal_status(call.status) {
events.push(tool_result_kind(
&call.tool_call_id.to_string(),
call.raw_output.as_ref(),
&call.content,
is_error,
));
}
events.push(update_raw("tool_call", call));
events
}
fn tool_call_update_kinds(update: &ToolCallUpdate) -> Vec<EventKind> {
let mut events = Vec::new();
if let Some(is_error) = update.fields.status.and_then(terminal_status) {
events.push(tool_result_kind(
&update.tool_call_id.to_string(),
update.fields.raw_output.as_ref(),
update.fields.content.as_deref().unwrap_or_default(),
is_error,
));
}
events.push(update_raw("tool_call_update", update));
events
}
fn terminal_status(status: ToolCallStatus) -> Option<bool> {
match status {
ToolCallStatus::Completed => Some(false),
ToolCallStatus::Failed => Some(true),
_ => None,
}
}
fn tool_result_kind<T: Serialize + std::fmt::Debug>(
call_id: &str,
raw_output: Option<&Value>,
content: &[T],
is_error: bool,
) -> EventKind {
let output = match raw_output {
Some(output) => output.clone(),
None if content.is_empty() => Value::Null,
None => to_value_or_debug(&content),
};
(
ActivityEventKind::ToolResult {
call_id: call_id.to_owned(),
output,
is_error,
},
false,
)
}
#[must_use]
pub fn permission_request_kind(request: &RequestPermissionRequest) -> EventKind {
raw_kind(protocol::METHOD_SESSION_REQUEST_PERMISSION, request)
}
#[must_use]
pub fn permission_decision_kind(
policy: PermissionPolicy,
tool_call_id: &str,
options: &[PermissionOption],
outcome: &RequestPermissionOutcome,
) -> EventKind {
let value = json!({
"policy": policy.label(),
"toolCallId": tool_call_id,
"offered": to_value_or_debug(&options),
"outcome": to_value_or_debug(outcome),
});
(
ActivityEventKind::Raw {
source: format!("{}/decision", protocol::METHOD_SESSION_REQUEST_PERMISSION),
value,
},
false,
)
}
#[must_use]
pub fn prompt_response_kind(response: &JsonRpcResponse) -> EventKind {
raw_kind(protocol::METHOD_SESSION_PROMPT, response)
}
#[must_use]
pub fn refused_request_kind(method: &str, params: Option<&Value>) -> EventKind {
(
ActivityEventKind::Raw {
source: format!("refused/{method}"),
value: json!({
"method": method,
"params": params.cloned().unwrap_or(Value::Null),
"code": protocol::method_not_found_code(),
"reason": "this client does not advertise the capability serving this method",
}),
},
false,
)
}
#[must_use]
pub fn raw_kind<T: Serialize + std::fmt::Debug>(source: impl Into<String>, value: &T) -> EventKind {
(
ActivityEventKind::Raw {
source: source.into(),
value: to_value_or_debug(value),
},
false,
)
}
#[must_use]
pub fn update_raw<T: Serialize + std::fmt::Debug>(suffix: &str, value: &T) -> EventKind {
raw_kind(format!("{UPDATE_SOURCE_PREFIX}/{suffix}"), value)
}
fn to_value_or_debug<T: Serialize + std::fmt::Debug>(value: &T) -> Value {
match serde_json::to_value(value) {
Ok(encoded) => encoded,
Err(source) => json!({
"acpEncodeError": source.to_string(),
"debug": format!("{value:?}"),
}),
}
}
#[cfg(test)]
mod tests {
use agent_client_protocol_schema::ProtocolVersion;
use agent_client_protocol_schema::v1::{
AvailableCommand, AvailableCommandsUpdate, ConfigOptionUpdate, ContentBlock, ContentChunk,
CurrentModeUpdate, PermissionOption, PermissionOptionKind, Plan, PlanEntry,
PlanEntryPriority, PlanEntryStatus, RequestPermissionOutcome, RequestPermissionRequest,
SessionInfoUpdate, SessionUpdate, StopReason, TextContent, ToolCall, ToolCallStatus,
ToolCallUpdate, ToolCallUpdateFields, ToolKind, UsageUpdate,
};
use aion_core::{
ActivityEventKind, ActivityId, InterventionPrimitive, MessageRole, ProgressDetail, RunId,
StopKind, WorkflowId,
};
use aion_integrations::HarnessError;
use aion_integrations::jsonrpc::{JsonRpcId, JsonRpcResponse};
use serde_json::json;
use uuid::Uuid;
use super::{
EventIdentity, EventKind, gate_protocol_version, is_success, negotiated_capabilities,
permission_decision_kind, permission_request_kind, prompt_response_kind,
refused_request_kind, stop_kind, stop_reason_label, update_to_kinds,
};
use crate::messages::MessageAccumulator;
use crate::permission::PermissionPolicy;
fn raw_source(event: &EventKind) -> Option<&str> {
match &event.0 {
ActivityEventKind::Raw { source, .. } => Some(source.as_str()),
_ => None,
}
}
fn has_raw(events: &[EventKind], source: &str) -> bool {
events.iter().any(|event| raw_source(event) == Some(source))
}
fn text_chunk(text: &str) -> ContentChunk {
ContentChunk::new(ContentBlock::Text(TextContent::new(text)))
}
fn kinds(update: &SessionUpdate) -> Vec<EventKind> {
let mut accumulator = MessageAccumulator::new();
update_to_kinds(update, &mut accumulator)
}
#[test]
fn the_matching_protocol_version_passes_the_gate() -> Result<(), HarnessError> {
gate_protocol_version(ProtocolVersion::V1)
}
#[test]
fn a_mismatched_protocol_version_is_refused_naming_both_versions() -> Result<(), String> {
let error = gate_protocol_version(ProtocolVersion::V0)
.err()
.ok_or("a version mismatch must be refused")?;
assert!(
matches!(error, HarnessError::Protocol { .. }),
"a version mismatch is a protocol error, got {error:?}"
);
let message = error.to_string();
assert!(
message.contains("version 1") && message.contains("version 0"),
"the refusal names the expected AND received versions: {message}"
);
Ok(())
}
#[test]
fn only_cancel_is_advertised() {
let capabilities = negotiated_capabilities();
assert!(capabilities.supports_primitive(InterventionPrimitive::Cancel));
for unsupported in [
InterventionPrimitive::InjectMessage,
InterventionPrimitive::PauseResume,
InterventionPrimitive::UpdateBudget,
InterventionPrimitive::RespondToApproval,
] {
assert!(
!capabilities.supports_primitive(unsupported),
"{unsupported:?} has no ACP mechanism and must not be advertised"
);
}
assert_eq!(capabilities.supported.len(), 1);
}
#[test]
fn only_end_turn_is_a_completed_run() {
assert!(is_success(StopReason::EndTurn));
for failure in [
StopReason::MaxTokens,
StopReason::MaxTurnRequests,
StopReason::Refusal,
StopReason::Cancelled,
] {
assert!(
!is_success(failure),
"{failure:?} is not a completed run and must never be reported as success"
);
}
}
#[test]
fn every_stop_reason_maps_to_a_kind_and_a_wire_label() {
let cases = [
(StopReason::EndTurn, StopKind::EndTurn, "end_turn"),
(StopReason::MaxTokens, StopKind::LimitReached, "max_tokens"),
(
StopReason::MaxTurnRequests,
StopKind::LimitReached,
"max_turn_requests",
),
(
StopReason::Refusal,
StopKind::Other {
reason: "refusal".to_owned(),
},
"refusal",
),
(StopReason::Cancelled, StopKind::Cancelled, "cancelled"),
];
for (reason, expected_kind, expected_label) in cases {
assert_eq!(stop_kind(reason), expected_kind, "kind for {reason:?}");
assert_eq!(stop_reason_label(reason), expected_label);
}
}
#[test]
fn an_agent_message_chunk_streams_an_ephemeral_delta() -> Result<(), String> {
let events = kinds(&SessionUpdate::AgentMessageChunk(text_chunk("hi")));
let [only] = events.as_slice() else {
return Err(format!("one delta per chunk, got {events:?}"));
};
assert!(only.1, "a per-token chunk is ephemeral");
assert!(matches!(only.0, ActivityEventKind::Delta { .. }));
Ok(())
}
#[test]
fn a_user_message_chunk_accumulates_into_a_durable_user_message() -> Result<(), String> {
let mut accumulator = MessageAccumulator::new();
let streamed = update_to_kinds(
&SessionUpdate::UserMessageChunk(text_chunk("do the thing")),
&mut accumulator,
);
assert!(
streamed.is_empty(),
"a user chunk emits nothing until its boundary, got {streamed:?}"
);
let closed = accumulator.finish();
let [only] = closed.as_slice() else {
return Err(format!("one message per user turn, got {closed:?}"));
};
assert!(!only.1, "a user turn is durable");
match &only.0 {
ActivityEventKind::Message { role, text } => {
assert_eq!(*role, MessageRole::User);
assert_eq!(text, "do the thing");
}
other => return Err(format!("expected Message, got {other:?}")),
}
Ok(())
}
#[test]
fn an_agent_thought_chunk_streams_a_note_and_survives_durably() -> Result<(), String> {
let mut accumulator = MessageAccumulator::new();
let streamed = update_to_kinds(
&SessionUpdate::AgentThoughtChunk(text_chunk("weighing it")),
&mut accumulator,
);
let [only] = streamed.as_slice() else {
return Err(format!("one note per thought chunk, got {streamed:?}"));
};
assert!(only.1, "the live reasoning stream is ephemeral");
match &only.0 {
ActivityEventKind::Progress {
detail: ProgressDetail::Thinking { text, .. },
} => assert_eq!(text, "weighing it"),
other => return Err(format!("expected Progress::Thinking, got {other:?}")),
}
let closed = accumulator.finish();
let [durable] = closed.as_slice() else {
return Err(format!("one durable reasoning message, got {closed:?}"));
};
assert!(!durable.1, "the accumulated reasoning is durable");
match &durable.0 {
ActivityEventKind::Message { role, text } => {
assert_eq!(*role, MessageRole::Assistant);
assert_eq!(text, "weighing it");
}
other => return Err(format!("expected Message, got {other:?}")),
}
assert_eq!(
accumulator.completed_text(),
"",
"reasoning is never the activity's result"
);
Ok(())
}
#[test]
fn the_terminal_response_is_recorded_verbatim_with_its_meta() -> Result<(), String> {
let result = json!({ "stopReason": "end_turn", "_meta": { "costUsd": 0.04 } });
let event = prompt_response_kind(&JsonRpcResponse::success(JsonRpcId::number(9), result));
assert_eq!(raw_source(&event), Some("session/prompt"));
match &event.0 {
ActivityEventKind::Raw { value, .. } => {
assert_eq!(value["result"]["_meta"]["costUsd"], json!(0.04));
assert_eq!(value["id"], json!(9));
}
other => return Err(format!("expected Raw, got {other:?}")),
}
assert!(!event.1, "the terminal frame is durable");
Ok(())
}
#[test]
fn a_tool_call_yields_the_classified_event_and_the_verbatim_frame() -> Result<(), String> {
let call = ToolCall::new("t1", "Read src/main.rs")
.kind(ToolKind::Read)
.raw_input(json!({ "path": "src/main.rs" }));
let events = kinds(&SessionUpdate::ToolCall(call));
let [classified, verbatim] = events.as_slice() else {
return Err(format!("a classified event plus the frame, got {events:?}"));
};
match &classified.0 {
ActivityEventKind::ToolCall {
tool,
call_id,
input,
} => {
assert_eq!(tool, "Read src/main.rs");
assert_eq!(call_id, "t1");
assert_eq!(input, &json!({ "path": "src/main.rs" }));
}
other => return Err(format!("expected ToolCall, got {other:?}")),
}
assert_eq!(raw_source(verbatim), Some("session/update/tool_call"));
match &verbatim.0 {
ActivityEventKind::Raw { value, .. } => {
assert_eq!(value["kind"], json!("read"));
assert_eq!(value["toolCallId"], json!("t1"));
assert_eq!(value["title"], json!("Read src/main.rs"));
}
other => return Err(format!("expected Raw, got {other:?}")),
}
Ok(())
}
#[test]
fn a_completed_tool_call_update_yields_a_tool_result() -> Result<(), String> {
let update = ToolCallUpdate::new(
"t1",
ToolCallUpdateFields::new()
.status(ToolCallStatus::Completed)
.raw_output(json!({ "bytes": 42 })),
);
let events = kinds(&SessionUpdate::ToolCallUpdate(update));
let [result, verbatim] = events.as_slice() else {
return Err(format!("a result plus the frame, got {events:?}"));
};
match &result.0 {
ActivityEventKind::ToolResult {
call_id,
output,
is_error,
} => {
assert_eq!(call_id, "t1");
assert_eq!(output, &json!({ "bytes": 42 }));
assert!(!is_error);
}
other => return Err(format!("expected ToolResult, got {other:?}")),
}
assert_eq!(
raw_source(verbatim),
Some("session/update/tool_call_update")
);
Ok(())
}
#[test]
fn a_failed_tool_call_update_marks_the_result_as_an_error() -> Result<(), String> {
let update = ToolCallUpdate::new(
"t2",
ToolCallUpdateFields::new().status(ToolCallStatus::Failed),
);
let events = kinds(&SessionUpdate::ToolCallUpdate(update));
let Some(first) = events.first() else {
return Err("a failed update yields events".to_owned());
};
match &first.0 {
ActivityEventKind::ToolResult {
output, is_error, ..
} => {
assert!(is_error, "a failed status is an error result");
assert_eq!(
output,
&serde_json::Value::Null,
"no output reported is a null output, not a fabricated one"
);
}
other => return Err(format!("expected ToolResult, got {other:?}")),
}
Ok(())
}
#[test]
fn a_non_terminal_tool_call_update_is_the_verbatim_frame_alone() -> Result<(), String> {
let update = ToolCallUpdate::new(
"t3",
ToolCallUpdateFields::new().status(ToolCallStatus::InProgress),
);
let events = kinds(&SessionUpdate::ToolCallUpdate(update));
let [only] = events.as_slice() else {
return Err(format!("only the frame, got {events:?}"));
};
assert_eq!(raw_source(only), Some("session/update/tool_call_update"));
Ok(())
}
#[test]
fn the_remaining_variants_each_reach_the_transcript_verbatim() {
let cases: Vec<(SessionUpdate, &str)> = vec![
(
SessionUpdate::Plan(Plan::new(vec![PlanEntry::new(
"read the code",
PlanEntryPriority::High,
PlanEntryStatus::Pending,
)])),
"session/update/plan",
),
(
SessionUpdate::AvailableCommandsUpdate(AvailableCommandsUpdate::new(vec![
AvailableCommand::new("create_plan", "draft a plan"),
])),
"session/update/available_commands_update",
),
(
SessionUpdate::CurrentModeUpdate(CurrentModeUpdate::new("architect")),
"session/update/current_mode_update",
),
(
SessionUpdate::ConfigOptionUpdate(ConfigOptionUpdate::new(vec![])),
"session/update/config_option_update",
),
(
SessionUpdate::SessionInfoUpdate(SessionInfoUpdate::new().title("a title")),
"session/update/session_info_update",
),
(
SessionUpdate::UsageUpdate(UsageUpdate::new(1_024, 200_000)),
"session/update/usage_update",
),
];
for (update, source) in cases {
let events = kinds(&update);
assert!(
has_raw(&events, source),
"{update:?} must reach the transcript under `{source}`, got {events:?}"
);
}
}
#[test]
fn a_plan_reaches_the_transcript_with_all_its_entries() -> Result<(), String> {
let plan = Plan::new(vec![
PlanEntry::new("read", PlanEntryPriority::High, PlanEntryStatus::Completed),
PlanEntry::new("write", PlanEntryPriority::Low, PlanEntryStatus::Pending),
]);
let events = kinds(&SessionUpdate::Plan(plan));
let [only] = events.as_slice() else {
return Err(format!("one event for a plan, got {events:?}"));
};
match &only.0 {
ActivityEventKind::Raw { value, .. } => {
let entries = value["entries"]
.as_array()
.ok_or("the plan entries survive as an array")?;
assert_eq!(entries.len(), 2);
assert_eq!(entries[0]["status"], json!("completed"));
}
other => return Err(format!("expected Raw, got {other:?}")),
}
Ok(())
}
#[test]
fn a_permission_request_and_its_decision_are_both_recorded() -> Result<(), String> {
let options = vec![
PermissionOption::new("a1", "Allow once", PermissionOptionKind::AllowOnce),
PermissionOption::new("r1", "Reject", PermissionOptionKind::RejectOnce),
];
let request = RequestPermissionRequest::new(
"sess-1",
ToolCallUpdate::new("t1", ToolCallUpdateFields::new().title("rm -rf".to_owned())),
options.clone(),
);
let asked = permission_request_kind(&request);
assert_eq!(raw_source(&asked), Some("session/request_permission"));
let outcome = PermissionPolicy::Deny.decide(&options);
let decided = permission_decision_kind(PermissionPolicy::Deny, "t1", &options, &outcome);
assert_eq!(
raw_source(&decided),
Some("session/request_permission/decision")
);
match &decided.0 {
ActivityEventKind::Raw { value, .. } => {
assert_eq!(value["policy"], json!("deny"));
assert_eq!(value["toolCallId"], json!("t1"));
assert_eq!(value["outcome"]["outcome"], json!("selected"));
assert_eq!(value["outcome"]["optionId"], json!("r1"));
assert_eq!(
value["offered"]
.as_array()
.map(Vec::len)
.ok_or("the offered options are recorded")?,
2
);
}
other => return Err(format!("expected Raw, got {other:?}")),
}
assert!(!asked.1 && !decided.1, "an audit record is never ephemeral");
Ok(())
}
#[test]
fn a_cancelled_decision_is_recorded_as_cancelled() -> Result<(), String> {
let decided = permission_decision_kind(
PermissionPolicy::AllowOnce,
"t9",
&[],
&RequestPermissionOutcome::Cancelled,
);
match &decided.0 {
ActivityEventKind::Raw { value, .. } => {
assert_eq!(value["outcome"]["outcome"], json!("cancelled"));
assert_eq!(value["policy"], json!("allow_once"));
}
other => return Err(format!("expected Raw, got {other:?}")),
}
Ok(())
}
#[test]
fn a_refused_peer_request_is_recorded_with_its_code() -> Result<(), String> {
let params = json!({ "sessionId": "s1", "path": "/etc/passwd" });
let event = refused_request_kind("fs/read_text_file", Some(¶ms));
assert_eq!(raw_source(&event), Some("refused/fs/read_text_file"));
match &event.0 {
ActivityEventKind::Raw { value, .. } => {
assert_eq!(value["method"], json!("fs/read_text_file"));
assert_eq!(value["code"], json!(-32601));
assert_eq!(value["params"]["path"], json!("/etc/passwd"));
}
other => return Err(format!("expected Raw, got {other:?}")),
}
Ok(())
}
#[test]
fn stamping_fills_the_run_identity_and_the_acp_attribution() {
let identity = EventIdentity {
workflow_id: WorkflowId::new(Uuid::nil()),
run_id: RunId::new(Uuid::from_u128(0x11)),
activity_id: ActivityId::from_sequence_position(4),
attempt: 3,
};
let event = identity.stamp(
7,
(
ActivityEventKind::Stop {
reason: StopKind::EndTurn,
},
false,
),
);
assert_eq!(event.attempt, 3);
assert_eq!(event.worker_seq, 7);
assert_eq!(event.store_seq, None);
assert!(!event.ephemeral);
assert_eq!(event.agent_id, Uuid::nil());
assert_eq!(event.agent_role, "agent");
}
}