use chrono::{DateTime, Utc};
use meerkat_core::lifecycle::InputId;
use meerkat_core::lifecycle::run_primitive::RuntimeTurnMetadata;
use meerkat_core::ops::{OpEvent, OperationId};
use meerkat_core::types::HandlingMode;
use meerkat_core::{
BlobStore, BlobStoreError, MissingBlobBehavior, externalize_content_blocks,
hydrate_content_blocks,
};
use serde::{Deserialize, Serialize};
use crate::identifiers::{
CorrelationId, IdempotencyKey, KindId, LogicalRuntimeId, SupersessionKey,
};
use meerkat_core::types::RenderMetadata;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InputHeader {
pub id: InputId,
pub timestamp: DateTime<Utc>,
pub source: InputOrigin,
pub durability: InputDurability,
pub visibility: InputVisibility,
#[serde(skip_serializing_if = "Option::is_none")]
pub idempotency_key: Option<IdempotencyKey>,
#[serde(skip_serializing_if = "Option::is_none")]
pub supersession_key: Option<SupersessionKey>,
#[serde(skip_serializing_if = "Option::is_none")]
pub correlation_id: Option<CorrelationId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum InputOrigin {
Operator,
Peer {
peer_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
runtime_id: Option<LogicalRuntimeId>,
},
Flow { flow_id: String, step_index: usize },
System,
External { source_name: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum InputDurability {
Durable,
Ephemeral,
Derived,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct InputVisibility {
pub transcript_eligible: bool,
pub operator_eligible: bool,
}
impl Default for InputVisibility {
fn default() -> Self {
Self {
transcript_eligible: true,
operator_eligible: true,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "input_type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum Input {
Prompt(PromptInput),
Peer(PeerInput),
FlowStep(FlowStepInput),
ExternalEvent(ExternalEventInput),
#[serde(alias = "system_generated")]
Continuation(ContinuationInput),
#[serde(alias = "projected")]
Operation(OperationInput),
}
impl Input {
pub fn header(&self) -> &InputHeader {
match self {
Input::Prompt(i) => &i.header,
Input::Peer(i) => &i.header,
Input::FlowStep(i) => &i.header,
Input::ExternalEvent(i) => &i.header,
Input::Continuation(i) => &i.header,
Input::Operation(i) => &i.header,
}
}
pub fn id(&self) -> &InputId {
&self.header().id
}
pub fn kind_id(&self) -> KindId {
match self {
Input::Prompt(_) => KindId::new("prompt"),
Input::Peer(p) => match &p.convention {
Some(PeerConvention::Message) => KindId::new("peer_message"),
Some(PeerConvention::Request { .. }) => KindId::new("peer_request"),
Some(PeerConvention::ResponseProgress { .. }) => {
KindId::new("peer_response_progress")
}
Some(PeerConvention::ResponseTerminal { .. }) => {
KindId::new("peer_response_terminal")
}
None => KindId::new("peer_message"),
},
Input::FlowStep(_) => KindId::new("flow_step"),
Input::ExternalEvent(_) => KindId::new("external_event"),
Input::Continuation(_) => KindId::new("continuation"),
Input::Operation(_) => KindId::new("operation"),
}
}
pub fn handling_mode(&self) -> Option<HandlingMode> {
match self {
Input::Prompt(prompt) => prompt.turn_metadata.as_ref()?.handling_mode,
Input::FlowStep(flow_step) => flow_step.turn_metadata.as_ref()?.handling_mode,
Input::ExternalEvent(event) => Some(event.handling_mode),
Input::Continuation(continuation) => Some(continuation.handling_mode),
Input::Peer(peer) => peer.handling_mode,
Input::Operation(_) => None,
}
}
}
fn migrate_legacy_payload_blocks(event: &mut ExternalEventInput) -> Result<(), BlobStoreError> {
let Some(obj) = event.payload.as_object_mut() else {
return Ok(());
};
let Some(blocks_value) = obj.remove("blocks") else {
return Ok(());
};
if event.blocks.is_some() {
return Ok(());
}
let blocks = serde_json::from_value::<Vec<meerkat_core::types::ContentBlock>>(blocks_value)
.map_err(|err| {
BlobStoreError::Internal(format!("failed to decode payload blocks: {err}"))
})?;
event.blocks = Some(blocks);
Ok(())
}
pub async fn externalize_input_images(
blob_store: &dyn BlobStore,
input: &mut Input,
) -> Result<(), BlobStoreError> {
match input {
Input::Prompt(prompt) => {
if let Some(blocks) = prompt.blocks.as_mut() {
externalize_content_blocks(blob_store, blocks).await?;
}
}
Input::Peer(peer) => {
if let Some(blocks) = peer.blocks.as_mut() {
externalize_content_blocks(blob_store, blocks).await?;
}
}
Input::FlowStep(flow_step) => {
if let Some(blocks) = flow_step.blocks.as_mut() {
externalize_content_blocks(blob_store, blocks).await?;
}
}
Input::ExternalEvent(event) => {
migrate_legacy_payload_blocks(event)?;
if let Some(blocks) = event.blocks.as_mut() {
externalize_content_blocks(blob_store, blocks).await?;
}
}
Input::Continuation(_) | Input::Operation(_) => {}
}
Ok(())
}
pub async fn hydrate_input_images(
blob_store: &dyn BlobStore,
input: &mut Input,
missing_behavior: MissingBlobBehavior,
) -> Result<(), BlobStoreError> {
match input {
Input::Prompt(prompt) => {
if let Some(blocks) = prompt.blocks.as_mut() {
hydrate_content_blocks(blob_store, blocks, missing_behavior).await?;
}
}
Input::Peer(peer) => {
if let Some(blocks) = peer.blocks.as_mut() {
hydrate_content_blocks(blob_store, blocks, missing_behavior).await?;
}
}
Input::FlowStep(flow_step) => {
if let Some(blocks) = flow_step.blocks.as_mut() {
hydrate_content_blocks(blob_store, blocks, missing_behavior).await?;
}
}
Input::ExternalEvent(event) => {
migrate_legacy_payload_blocks(event)?;
if let Some(blocks) = event.blocks.as_mut() {
hydrate_content_blocks(blob_store, blocks, missing_behavior).await?;
}
}
Input::Continuation(_) | Input::Operation(_) => {}
}
Ok(())
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PromptInput {
pub header: InputHeader,
pub text: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blocks: Option<Vec<meerkat_core::types::ContentBlock>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_metadata: Option<RuntimeTurnMetadata>,
}
impl PromptInput {
pub fn new(text: impl Into<String>, turn_metadata: Option<RuntimeTurnMetadata>) -> Self {
Self {
header: InputHeader {
id: meerkat_core::lifecycle::InputId::new(),
timestamp: chrono::Utc::now(),
source: InputOrigin::Operator,
durability: InputDurability::Durable,
visibility: InputVisibility::default(),
idempotency_key: None,
supersession_key: None,
correlation_id: None,
},
text: text.into(),
blocks: None,
turn_metadata,
}
}
pub fn from_content_input(
input: meerkat_core::types::ContentInput,
turn_metadata: Option<RuntimeTurnMetadata>,
) -> Self {
let text = input.text_content();
let blocks = if input.has_images() {
Some(input.into_blocks())
} else {
None
};
Self {
header: InputHeader {
id: meerkat_core::lifecycle::InputId::new(),
timestamp: chrono::Utc::now(),
source: InputOrigin::Operator,
durability: InputDurability::Durable,
visibility: InputVisibility::default(),
idempotency_key: None,
supersession_key: None,
correlation_id: None,
},
text,
blocks,
turn_metadata,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PeerInput {
pub header: InputHeader,
#[serde(skip_serializing_if = "Option::is_none")]
pub convention: Option<PeerConvention>,
pub body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blocks: Option<Vec<meerkat_core::types::ContentBlock>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub handling_mode: Option<HandlingMode>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "convention_type", rename_all = "snake_case")]
#[non_exhaustive]
pub enum PeerConvention {
Message,
Request { request_id: String, intent: String },
ResponseProgress {
request_id: String,
phase: ResponseProgressPhase,
},
ResponseTerminal {
request_id: String,
status: ResponseTerminalStatus,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseProgressPhase {
Accepted,
InProgress,
PartialResult,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ResponseTerminalStatus {
Completed,
Failed,
Cancelled,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FlowStepInput {
pub header: InputHeader,
pub step_id: String,
pub instructions: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blocks: Option<Vec<meerkat_core::types::ContentBlock>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_metadata: Option<RuntimeTurnMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExternalEventInput {
pub header: InputHeader,
pub event_type: String,
pub payload: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub blocks: Option<Vec<meerkat_core::types::ContentBlock>>,
#[serde(default)]
pub handling_mode: HandlingMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub render_metadata: Option<RenderMetadata>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContinuationInput {
pub header: InputHeader,
pub reason: String,
#[serde(default)]
pub handling_mode: HandlingMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
}
impl ContinuationInput {
pub fn detached_background_op_completed() -> Self {
Self {
header: InputHeader {
id: meerkat_core::lifecycle::InputId::new(),
timestamp: chrono::Utc::now(),
source: InputOrigin::System,
durability: InputDurability::Derived,
visibility: InputVisibility {
transcript_eligible: false,
operator_eligible: false,
},
idempotency_key: None,
supersession_key: None,
correlation_id: None,
},
reason: "detached_background_op_completed".to_string(),
handling_mode: HandlingMode::Steer,
request_id: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationInput {
pub header: InputHeader,
pub operation_id: OperationId,
pub event: OpEvent,
}
pub(crate) fn classify_execution_kind(
input: &Input,
) -> meerkat_core::lifecycle::RuntimeExecutionKind {
match input {
Input::Continuation(_) => meerkat_core::lifecycle::RuntimeExecutionKind::ResumePending,
_ => meerkat_core::lifecycle::RuntimeExecutionKind::ContentTurn,
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
use chrono::Utc;
fn make_header() -> InputHeader {
InputHeader {
id: InputId::new(),
timestamp: Utc::now(),
source: InputOrigin::Operator,
durability: InputDurability::Durable,
visibility: InputVisibility::default(),
idempotency_key: None,
supersession_key: None,
correlation_id: None,
}
}
#[test]
fn prompt_input_serde() {
let input = Input::Prompt(PromptInput {
header: make_header(),
text: "hello".into(),
blocks: None,
turn_metadata: None,
});
let json = serde_json::to_value(&input).unwrap();
assert_eq!(json["input_type"], "prompt");
let parsed: Input = serde_json::from_value(json).unwrap();
assert!(matches!(parsed, Input::Prompt(_)));
}
#[test]
fn peer_input_message_serde() {
let input = Input::Peer(PeerInput {
header: make_header(),
convention: Some(PeerConvention::Message),
body: "hi there".into(),
blocks: None,
handling_mode: None,
});
let json = serde_json::to_value(&input).unwrap();
assert_eq!(json["input_type"], "peer");
let parsed: Input = serde_json::from_value(json).unwrap();
assert!(matches!(parsed, Input::Peer(_)));
}
#[test]
fn peer_input_request_serde() {
let input = Input::Peer(PeerInput {
header: make_header(),
convention: Some(PeerConvention::Request {
request_id: "req-1".into(),
intent: "mob.peer_added".into(),
}),
body: "Agent joined".into(),
blocks: None,
handling_mode: None,
});
let json = serde_json::to_value(&input).unwrap();
let parsed: Input = serde_json::from_value(json).unwrap();
if let Input::Peer(p) = parsed {
assert!(matches!(p.convention, Some(PeerConvention::Request { .. })));
} else {
panic!("Expected PeerInput");
}
}
#[test]
fn peer_input_response_terminal_serde() {
let input = Input::Peer(PeerInput {
header: make_header(),
convention: Some(PeerConvention::ResponseTerminal {
request_id: "req-1".into(),
status: ResponseTerminalStatus::Completed,
}),
body: "Done".into(),
blocks: None,
handling_mode: None,
});
let json = serde_json::to_value(&input).unwrap();
let parsed: Input = serde_json::from_value(json).unwrap();
assert!(matches!(parsed, Input::Peer(_)));
}
#[test]
fn peer_input_response_progress_serde() {
let input = Input::Peer(PeerInput {
header: make_header(),
convention: Some(PeerConvention::ResponseProgress {
request_id: "req-1".into(),
phase: ResponseProgressPhase::InProgress,
}),
body: "Working...".into(),
blocks: None,
handling_mode: None,
});
let json = serde_json::to_value(&input).unwrap();
let parsed: Input = serde_json::from_value(json).unwrap();
assert!(matches!(parsed, Input::Peer(_)));
}
#[test]
fn flow_step_input_serde() {
let input = Input::FlowStep(FlowStepInput {
header: make_header(),
step_id: "step-1".into(),
instructions: "analyze the data".into(),
blocks: Some(vec![
meerkat_core::types::ContentBlock::Text {
text: "analyze the data".into(),
},
meerkat_core::types::ContentBlock::Image {
media_type: "image/png".into(),
data: meerkat_core::types::ImageData::Inline {
data: "abc123".into(),
},
},
]),
turn_metadata: None,
});
let json = serde_json::to_value(&input).unwrap();
assert_eq!(json["input_type"], "flow_step");
let parsed: Input = serde_json::from_value(json).unwrap();
assert!(matches!(parsed, Input::FlowStep(_)));
}
#[test]
fn external_event_input_serde() {
let input = Input::ExternalEvent(ExternalEventInput {
header: make_header(),
event_type: "webhook.received".into(),
payload: serde_json::json!({"url": "https://example.com"}),
blocks: Some(vec![
meerkat_core::types::ContentBlock::Text {
text: "look".into(),
},
meerkat_core::types::ContentBlock::Image {
media_type: "image/png".into(),
data: meerkat_core::types::ImageData::Inline {
data: "abc123".into(),
},
},
]),
handling_mode: HandlingMode::Queue,
render_metadata: None,
});
let json = serde_json::to_value(&input).unwrap();
assert_eq!(json["input_type"], "external_event");
let parsed: Input = serde_json::from_value(json).unwrap();
assert!(matches!(parsed, Input::ExternalEvent(_)));
}
#[test]
fn legacy_external_event_payload_blocks_migrate_to_canonical_blocks_owner() {
let mut input = Input::ExternalEvent(ExternalEventInput {
header: make_header(),
event_type: "webhook.received".into(),
payload: serde_json::json!({
"body": "see image",
"blocks": [
{ "type": "text", "text": "caption text" },
{ "type": "image", "media_type": "image/png", "source": "inline", "data": "abc123" }
]
}),
blocks: None,
handling_mode: HandlingMode::Queue,
render_metadata: None,
});
match &mut input {
Input::ExternalEvent(event) => {
migrate_legacy_payload_blocks(event).unwrap();
assert!(event.payload.get("blocks").is_none());
assert_eq!(event.payload["body"], "see image");
assert_eq!(event.blocks.as_ref().map(Vec::len), Some(2));
}
other => panic!("Expected ExternalEvent, got {other:?}"),
}
}
#[test]
fn continuation_input_serde() {
let input = Input::Continuation(ContinuationInput::detached_background_op_completed());
let json = serde_json::to_value(&input).unwrap();
assert_eq!(json["input_type"], "continuation");
let parsed: Input = serde_json::from_value(json).unwrap();
match parsed {
Input::Continuation(continuation) => {
assert_eq!(continuation.handling_mode, HandlingMode::Steer);
assert_eq!(continuation.reason, "detached_background_op_completed");
}
other => panic!("Expected Continuation, got {other:?}"),
}
}
#[test]
fn continuation_input_accepts_legacy_system_generated_tag() {
let input = Input::Continuation(ContinuationInput::detached_background_op_completed());
let mut json = serde_json::to_value(&input).unwrap();
json["input_type"] = serde_json::Value::String("system_generated".into());
let parsed: Input = serde_json::from_value(json).unwrap();
match parsed {
Input::Continuation(continuation) => {
assert_eq!(continuation.reason, "detached_background_op_completed");
}
other => panic!("Expected Continuation, got {other:?}"),
}
}
#[test]
fn operation_input_serde() {
let input = Input::Operation(OperationInput {
header: InputHeader {
durability: InputDurability::Derived,
..make_header()
},
operation_id: OperationId::new(),
event: OpEvent::Cancelled {
id: OperationId::new(),
},
});
let json = serde_json::to_value(&input).unwrap();
assert_eq!(json["input_type"], "operation");
let parsed: Input = serde_json::from_value(json).unwrap();
assert!(matches!(parsed, Input::Operation(_)));
}
#[test]
fn operation_input_accepts_legacy_projected_tag() {
let input = Input::Operation(OperationInput {
header: InputHeader {
durability: InputDurability::Derived,
..make_header()
},
operation_id: OperationId::new(),
event: OpEvent::Cancelled {
id: OperationId::new(),
},
});
let mut json = serde_json::to_value(&input).unwrap();
json["input_type"] = serde_json::Value::String("projected".into());
let parsed: Input = serde_json::from_value(json).unwrap();
assert!(matches!(parsed, Input::Operation(_)));
}
#[test]
fn input_kind_id() {
let prompt = Input::Prompt(PromptInput {
header: make_header(),
text: "hi".into(),
blocks: None,
turn_metadata: None,
});
assert_eq!(prompt.kind_id().0, "prompt");
let peer_msg = Input::Peer(PeerInput {
header: make_header(),
convention: Some(PeerConvention::Message),
body: "hi".into(),
blocks: None,
handling_mode: None,
});
assert_eq!(peer_msg.kind_id().0, "peer_message");
let peer_req = Input::Peer(PeerInput {
header: make_header(),
convention: Some(PeerConvention::Request {
request_id: "r".into(),
intent: "i".into(),
}),
body: "hi".into(),
blocks: None,
handling_mode: None,
});
assert_eq!(peer_req.kind_id().0, "peer_request");
let continuation = Input::Continuation(ContinuationInput {
header: make_header(),
reason: "continue".into(),
handling_mode: HandlingMode::Steer,
request_id: None,
});
assert_eq!(continuation.kind_id().0, "continuation");
let operation = Input::Operation(OperationInput {
header: make_header(),
operation_id: OperationId::new(),
event: OpEvent::Cancelled {
id: OperationId::new(),
},
});
assert_eq!(operation.kind_id().0, "operation");
}
#[test]
fn input_source_variants() {
let sources = vec![
InputOrigin::Operator,
InputOrigin::Peer {
peer_id: "p1".into(),
runtime_id: None,
},
InputOrigin::Flow {
flow_id: "f1".into(),
step_index: 0,
},
InputOrigin::System,
InputOrigin::External {
source_name: "webhook".into(),
},
];
for source in sources {
let json = serde_json::to_value(&source).unwrap();
let parsed: InputOrigin = serde_json::from_value(json).unwrap();
assert_eq!(source, parsed);
}
}
#[test]
fn input_durability_serde() {
for d in [
InputDurability::Durable,
InputDurability::Ephemeral,
InputDurability::Derived,
] {
let json = serde_json::to_value(d).unwrap();
let parsed: InputDurability = serde_json::from_value(json).unwrap();
assert_eq!(d, parsed);
}
}
#[test]
fn peer_input_without_handling_mode_deserializes_as_none() {
let json = serde_json::json!({
"input_type": "peer",
"header": serde_json::to_value(make_header()).unwrap(),
"convention": { "convention_type": "message" },
"body": "hello"
});
let parsed: Input = serde_json::from_value(json).unwrap();
match parsed {
Input::Peer(p) => assert!(p.handling_mode.is_none()),
other => panic!("Expected Peer, got {other:?}"),
}
}
#[test]
fn peer_input_with_queue_handling_mode_roundtrips() {
let input = Input::Peer(PeerInput {
header: make_header(),
convention: Some(PeerConvention::Message),
body: "hi".into(),
blocks: None,
handling_mode: Some(HandlingMode::Queue),
});
let json = serde_json::to_value(&input).unwrap();
assert_eq!(json["handling_mode"], "queue");
let parsed: Input = serde_json::from_value(json).unwrap();
match parsed {
Input::Peer(p) => assert_eq!(p.handling_mode, Some(HandlingMode::Queue)),
other => panic!("Expected Peer, got {other:?}"),
}
}
#[test]
fn peer_input_with_steer_handling_mode_roundtrips() {
let input = Input::Peer(PeerInput {
header: make_header(),
convention: Some(PeerConvention::Message),
body: "hi".into(),
blocks: None,
handling_mode: Some(HandlingMode::Steer),
});
let json = serde_json::to_value(&input).unwrap();
assert_eq!(json["handling_mode"], "steer");
let parsed: Input = serde_json::from_value(json).unwrap();
match parsed {
Input::Peer(p) => assert_eq!(p.handling_mode, Some(HandlingMode::Steer)),
other => panic!("Expected Peer, got {other:?}"),
}
}
#[test]
fn peer_input_handling_mode_not_serialized_when_none() {
let input = Input::Peer(PeerInput {
header: make_header(),
convention: Some(PeerConvention::Message),
body: "hi".into(),
blocks: None,
handling_mode: None,
});
let json = serde_json::to_value(&input).unwrap();
assert!(json.get("handling_mode").is_none());
}
#[test]
fn classify_prompt_is_content_turn() {
let input = Input::Prompt(PromptInput {
header: make_header(),
text: "hello".into(),
blocks: None,
turn_metadata: None,
});
assert_eq!(
classify_execution_kind(&input),
meerkat_core::lifecycle::RuntimeExecutionKind::ContentTurn
);
}
#[test]
fn classify_peer_terminal_is_content_turn() {
let input = Input::Peer(PeerInput {
header: make_header(),
convention: Some(PeerConvention::ResponseTerminal {
request_id: "r".into(),
status: ResponseTerminalStatus::Completed,
}),
body: "done".into(),
blocks: None,
handling_mode: None,
});
assert_eq!(
classify_execution_kind(&input),
meerkat_core::lifecycle::RuntimeExecutionKind::ContentTurn
);
}
#[test]
fn classify_peer_terminal_with_steer_is_content_turn() {
let input = Input::Peer(PeerInput {
header: make_header(),
convention: Some(PeerConvention::ResponseTerminal {
request_id: "r".into(),
status: ResponseTerminalStatus::Completed,
}),
body: "done".into(),
blocks: None,
handling_mode: Some(HandlingMode::Steer),
});
assert_eq!(
classify_execution_kind(&input),
meerkat_core::lifecycle::RuntimeExecutionKind::ContentTurn
);
}
#[test]
fn classify_continuation_is_resume_pending() {
let input = Input::Continuation(ContinuationInput::detached_background_op_completed());
assert_eq!(
classify_execution_kind(&input),
meerkat_core::lifecycle::RuntimeExecutionKind::ResumePending
);
}
#[test]
fn classify_peer_message_is_content_turn() {
let input = Input::Peer(PeerInput {
header: make_header(),
convention: Some(PeerConvention::Message),
body: "hi".into(),
blocks: None,
handling_mode: None,
});
assert_eq!(
classify_execution_kind(&input),
meerkat_core::lifecycle::RuntimeExecutionKind::ContentTurn
);
}
#[test]
fn classify_operation_is_content_turn() {
let input = Input::Operation(OperationInput {
header: make_header(),
operation_id: OperationId::new(),
event: OpEvent::Started {
id: OperationId::new(),
kind: meerkat_core::ops::WorkKind::ToolCall,
},
});
assert_eq!(
classify_execution_kind(&input),
meerkat_core::lifecycle::RuntimeExecutionKind::ContentTurn
);
}
}