use serde::de::{self, DeserializeOwned};
use serde::{Deserialize, Serialize};
use super::identifiers::InputId;
use crate::connection::AuthBindingRef;
use crate::provider::Provider;
use crate::service::TurnToolOverlay;
use crate::skills::SkillKey;
use crate::types::{HandlingMode, RenderMetadata, SystemNoticeBlock, SystemNoticeKind};
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RunApplyBoundary {
Immediate,
RunStart,
RunCheckpoint,
}
#[non_exhaustive]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum CoreRenderable {
Text { text: String },
Blocks {
blocks: Vec<crate::types::ContentBlock>,
},
Json { value: serde_json::Value },
SystemNotice {
kind: SystemNoticeKind,
#[serde(default, skip_serializing_if = "Option::is_none")]
body: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
blocks: Vec<SystemNoticeBlock>,
},
Reference { uri: String, label: Option<String> },
}
impl CoreRenderable {
#[must_use]
pub fn text(text: impl Into<String>) -> Self {
Self::Text { text: text.into() }
}
#[must_use]
pub fn render_text(&self) -> String {
match self {
Self::Text { text } => text.clone(),
Self::Blocks { blocks } => crate::types::text_content(blocks),
Self::Json { value } => {
serde_json::to_string_pretty(value).unwrap_or_else(|_| value.to_string())
}
Self::Reference { uri, label } => match label {
Some(label) if !label.trim().is_empty() => format!("[Reference] {label} ({uri})"),
_ => format!("[Reference] {uri}"),
},
Self::SystemNotice { kind, body, blocks } => {
crate::types::SystemNoticeMessage::with_blocks(*kind, body.clone(), blocks.clone())
.model_projection_text()
}
}
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ConversationAppendRole {
User,
Assistant,
SystemNotice,
Tool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConversationAppend {
pub role: ConversationAppendRole,
pub content: CoreRenderable,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConversationContextAppend {
pub key: String,
pub content: CoreRenderable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeExecutionKind {
ContentTurn,
ResumePending,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PeerResponseTerminalApplyIntent {
AppendContextAndRun,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ModelId(String);
impl ModelId {
pub fn new(s: impl Into<String>) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl std::fmt::Display for ModelId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct KeepAlivePolicy {
#[serde(with = "duration_seconds")]
pub ttl: std::time::Duration,
pub policy: KeepAliveMode,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum KeepAliveMode {
Pinned,
PolicyDriven,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "directive", rename_all = "snake_case")]
pub enum KeepAliveDirective {
Enable(KeepAlivePolicy),
Disable,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TurnInstruction {
pub kind: TurnInstructionKind,
pub body: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TurnInstructionKind {
User,
System,
Host,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct StructuredProviderExtension {
pub namespace: String,
pub key: String,
#[cfg_attr(feature = "schema", schemars(with = "String"))]
#[serde(default)]
pub body: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "provider", rename_all = "snake_case")]
pub enum ProviderTag {
Anthropic(AnthropicProviderTag),
OpenAi(OpenAiProviderTag),
Gemini(GeminiProviderTag),
Unknown {
bag: StructuredProviderExtension,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct OpaqueProviderBody {
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub body: String,
}
impl OpaqueProviderBody {
pub fn from_value(v: &serde_json::Value) -> Self {
Self {
body: v.to_string(),
}
}
pub fn as_value(&self) -> serde_json::Value {
serde_json::from_str(&self.body).unwrap_or(serde_json::Value::Null)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum AnthropicThinkingConfig {
Adaptive,
Enabled { budget_tokens: u32 },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum AnthropicEffort {
Low,
Medium,
High,
Max,
XHigh,
}
impl AnthropicEffort {
pub fn as_legacy_str(self) -> &'static str {
match self {
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::Max => "max",
Self::XHigh => "xhigh",
}
}
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AnthropicInferenceGeo {
Us,
Global,
Other {
region: String,
},
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnthropicContextWindow {
OneMegabyte,
}
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AnthropicCompactionConfig {
Auto,
Custom { edit: OpaqueProviderBody },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct AnthropicProviderTag {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking: Option<AnthropicThinkingConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_budget_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web_search: Option<OpaqueProviderBody>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub top_k: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub effort: Option<AnthropicEffort>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub structured_output: Option<crate::OutputSchema>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inference_geo: Option<AnthropicInferenceGeo>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compaction: Option<AnthropicCompactionConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context: Option<AnthropicContextWindow>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supports_temperature_override: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct OpenAiProviderTag {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning_effort: Option<ReasoningEffort>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seed: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub frequency_penalty: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub presence_penalty: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub web_search: Option<OpaqueProviderBody>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub structured_output: Option<crate::OutputSchema>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning: Option<OpaqueProviderBody>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chat_template_kwargs: Option<OpaqueProviderBody>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking: Option<OpaqueProviderBody>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supports_temperature_override: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supports_reasoning_override: Option<bool>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum GeminiThinkingLevel {
Minimal,
Low,
Medium,
High,
}
impl GeminiThinkingLevel {
pub fn as_str(self) -> &'static str {
match self {
Self::Minimal => "minimal",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GeminiThinkingConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub include_thoughts: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_level: Option<GeminiThinkingLevel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_budget: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct GeminiProviderTag {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking: Option<GeminiThinkingConfig>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_budget: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_level: Option<GeminiThinkingLevel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub top_k: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub structured_output: Option<crate::OutputSchema>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub google_search: Option<OpaqueProviderBody>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub candidate_count: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ReasoningEffort {
None,
Low,
#[default]
Medium,
High,
#[serde(rename = "xhigh")]
XHigh,
}
impl ReasoningEffort {
pub fn as_legacy_str(self) -> &'static str {
match self {
Self::None => "none",
Self::Low => "low",
Self::Medium => "medium",
Self::High => "high",
Self::XHigh => "xhigh",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(rename_all = "snake_case")]
pub enum ReasoningMode {
Emit,
Silent,
Off,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(deny_unknown_fields)]
pub struct ProviderParamsOverride {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reasoning: Option<ReasoningMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub thinking_budget_tokens: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_tag: Option<ProviderTag>,
}
impl ProviderParamsOverride {
pub fn is_empty(&self) -> bool {
self.temperature.is_none()
&& self.top_p.is_none()
&& self.max_output_tokens.is_none()
&& self.reasoning.is_none()
&& self.thinking_budget_tokens.is_none()
&& self.provider_tag.is_none()
}
pub fn clear_web_search(&mut self) {
match self.provider_tag.as_mut() {
Some(ProviderTag::Anthropic(t)) => t.web_search = None,
Some(ProviderTag::OpenAi(t)) => t.web_search = None,
Some(ProviderTag::Gemini(t)) => t.google_search = None,
_ => {}
}
}
pub fn set_structured_output(
&mut self,
provider: Provider,
schema: crate::OutputSchema,
) -> Result<StructuredOutputInjection, ProviderParamsMergeError> {
match (provider, self.provider_tag.as_mut()) {
(Provider::Anthropic, Some(ProviderTag::Anthropic(tag))) => {
tag.structured_output = Some(schema);
}
(Provider::Anthropic, None) => {
self.provider_tag = Some(ProviderTag::Anthropic(AnthropicProviderTag {
structured_output: Some(schema),
..Default::default()
}));
}
(Provider::OpenAI | Provider::SelfHosted, Some(ProviderTag::OpenAi(tag))) => {
tag.structured_output = Some(schema);
}
(Provider::OpenAI | Provider::SelfHosted, None) => {
self.provider_tag = Some(ProviderTag::OpenAi(OpenAiProviderTag {
structured_output: Some(schema),
..Default::default()
}));
}
(Provider::Gemini, Some(ProviderTag::Gemini(tag))) => {
tag.structured_output = Some(schema);
}
(Provider::Gemini, None) => {
self.provider_tag = Some(ProviderTag::Gemini(GeminiProviderTag {
structured_output: Some(schema),
..Default::default()
}));
}
(Provider::Other, _) => {
return Ok(StructuredOutputInjection::NoProviderSlot);
}
(_, Some(tag)) => {
return Err(ProviderParamsMergeError::ProviderTagMismatch {
explicit: tag.provider_label(),
defaults: provider.as_str(),
});
}
}
Ok(StructuredOutputInjection::Injected)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StructuredOutputInjection {
Injected,
NoProviderSlot,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum ProviderParamsMergeError {
#[error(
"provider params carry a `{explicit}` provider tag but the merge target is `{defaults}`"
)]
ProviderTagMismatch {
explicit: &'static str,
defaults: &'static str,
},
}
impl ProviderTag {
pub fn provider_label(&self) -> &'static str {
match self {
Self::Anthropic(_) => "anthropic",
Self::OpenAi(_) => "openai",
Self::Gemini(_) => "gemini",
Self::Unknown { .. } => "unknown",
}
}
pub fn merge_missing_from(
&mut self,
defaults: &ProviderTag,
) -> Result<(), ProviderParamsMergeError> {
fn fill<T: Clone>(target: &mut Option<T>, default: &Option<T>) {
if target.is_none()
&& let Some(value) = default
{
*target = Some(value.clone());
}
}
match (self, defaults) {
(Self::Anthropic(target), Self::Anthropic(default)) => {
fill(&mut target.thinking, &default.thinking);
fill(
&mut target.thinking_budget_tokens,
&default.thinking_budget_tokens,
);
fill(&mut target.web_search, &default.web_search);
fill(&mut target.top_k, &default.top_k);
fill(&mut target.effort, &default.effort);
fill(&mut target.structured_output, &default.structured_output);
fill(&mut target.inference_geo, &default.inference_geo);
fill(&mut target.compaction, &default.compaction);
fill(&mut target.context, &default.context);
fill(
&mut target.supports_temperature_override,
&default.supports_temperature_override,
);
Ok(())
}
(Self::OpenAi(target), Self::OpenAi(default)) => {
fill(&mut target.reasoning_effort, &default.reasoning_effort);
fill(&mut target.seed, &default.seed);
fill(&mut target.frequency_penalty, &default.frequency_penalty);
fill(&mut target.presence_penalty, &default.presence_penalty);
fill(&mut target.web_search, &default.web_search);
fill(&mut target.structured_output, &default.structured_output);
fill(&mut target.reasoning, &default.reasoning);
fill(
&mut target.chat_template_kwargs,
&default.chat_template_kwargs,
);
fill(&mut target.thinking, &default.thinking);
fill(
&mut target.supports_temperature_override,
&default.supports_temperature_override,
);
fill(
&mut target.supports_reasoning_override,
&default.supports_reasoning_override,
);
Ok(())
}
(Self::Gemini(target), Self::Gemini(default)) => {
fill(&mut target.thinking, &default.thinking);
fill(&mut target.thinking_budget, &default.thinking_budget);
fill(&mut target.thinking_level, &default.thinking_level);
fill(&mut target.top_k, &default.top_k);
fill(&mut target.top_p, &default.top_p);
fill(&mut target.structured_output, &default.structured_output);
fill(&mut target.google_search, &default.google_search);
fill(&mut target.candidate_count, &default.candidate_count);
Ok(())
}
(Self::Unknown { .. }, Self::Unknown { .. }) => Ok(()),
(explicit, defaults) => Err(ProviderParamsMergeError::ProviderTagMismatch {
explicit: explicit.provider_label(),
defaults: defaults.provider_label(),
}),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(transparent)]
pub struct ProviderParamsCarrier {
pub params: ProviderParamsOverride,
#[serde(skip)]
pub tool_defaults: Option<ProviderTag>,
}
impl ProviderParamsCarrier {
pub fn from_params(params: ProviderParamsOverride) -> Self {
Self {
params,
tool_defaults: None,
}
}
pub fn is_empty(&self) -> bool {
self.params.is_empty() && self.tool_defaults.is_none()
}
pub fn serializes_empty(&self) -> bool {
self.params.is_empty()
}
pub fn effective_params(&self) -> Result<ProviderParamsOverride, ProviderParamsMergeError> {
let mut effective = self.params.clone();
if let Some(defaults) = self.tool_defaults.as_ref() {
match effective.provider_tag.as_mut() {
None => effective.provider_tag = Some(defaults.clone()),
Some(tag) => tag.merge_missing_from(defaults)?,
}
}
Ok(effective)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TurnMetadataMergeConflict {
pub field: &'static str,
pub reason: &'static str,
}
impl std::fmt::Display for TurnMetadataMergeConflict {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"batch turn-metadata scalar conflict on field `{}`: {}",
self.field, self.reason
)
}
}
impl std::error::Error for TurnMetadataMergeConflict {}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(tag = "action", content = "value", rename_all = "snake_case")]
pub enum TurnMetadataOverride<T> {
Set(T),
Clear,
}
impl<T> TurnMetadataOverride<T> {
pub fn set(value: T) -> Self {
Self::Set(value)
}
pub const fn clear() -> Self {
Self::Clear
}
pub fn as_set(&self) -> Option<&T> {
match self {
Self::Set(value) => Some(value),
Self::Clear => None,
}
}
pub fn into_set(self) -> Option<T> {
match self {
Self::Set(value) => Some(value),
Self::Clear => None,
}
}
pub const fn is_clear(&self) -> bool {
matches!(self, Self::Clear)
}
pub fn as_ref(&self) -> TurnMetadataOverride<&T> {
match self {
Self::Set(value) => TurnMetadataOverride::Set(value),
Self::Clear => TurnMetadataOverride::Clear,
}
}
}
impl<'de, T> Deserialize<'de> for TurnMetadataOverride<T>
where
T: DeserializeOwned,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = serde_json::Value::deserialize(deserializer)?;
if let Some(object) = raw.as_object() {
let Some(action_value) = object.get("action") else {
return serde_json::from_value(raw)
.map(Self::Set)
.map_err(de::Error::custom);
};
let action = action_value.as_str().ok_or_else(|| {
de::Error::custom("turn metadata override action must be a string")
})?;
return match action {
"clear" => {
if object.contains_key("value") {
return Err(de::Error::custom("clear override cannot include value"));
}
Ok(Self::Clear)
}
"set" => {
let value = object
.get("value")
.ok_or_else(|| de::Error::custom("set override is missing value"))?;
serde_json::from_value(value.clone())
.map(Self::Set)
.map_err(de::Error::custom)
}
other => Err(de::Error::custom(format!(
"unknown turn metadata override action `{other}`"
))),
};
}
serde_json::from_value(raw)
.map(Self::Set)
.map_err(de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct RuntimeTurnMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub handling_mode: Option<HandlingMode>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skill_references: Option<Vec<SkillKey>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub flow_tool_overlay: Option<TurnToolOverlay>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub additional_instructions: Option<Vec<TurnInstruction>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<ModelId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<Provider>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_params: Option<TurnMetadataOverride<ProviderParamsOverride>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth_binding: Option<TurnMetadataOverride<AuthBindingRef>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub keep_alive: Option<KeepAliveDirective>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub render_metadata: Option<RenderMetadata>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub execution_kind: Option<RuntimeExecutionKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub peer_response_terminal_apply_intent: Option<PeerResponseTerminalApplyIntent>,
}
impl RuntimeTurnMetadata {
pub fn is_empty(&self) -> bool {
self.handling_mode.is_none()
&& self.skill_references.is_none()
&& self.flow_tool_overlay.is_none()
&& self.additional_instructions.is_none()
&& self.model.is_none()
&& self.provider.is_none()
&& self.provider_params.is_none()
&& self.auth_binding.is_none()
&& self.keep_alive.is_none()
&& self.render_metadata.is_none()
&& self.execution_kind.is_none()
&& self.peer_response_terminal_apply_intent.is_none()
}
pub fn merge(&mut self, other: Self) -> Result<(), TurnMetadataMergeConflict> {
merge_scalar(
&mut self.handling_mode,
other.handling_mode,
"handling_mode",
)?;
merge_scalar(
&mut self.flow_tool_overlay,
other.flow_tool_overlay,
"flow_tool_overlay",
)?;
merge_scalar(&mut self.model, other.model, "model")?;
merge_scalar(&mut self.provider, other.provider, "provider")?;
merge_override(
&mut self.provider_params,
other.provider_params,
"provider_params",
)?;
merge_override(&mut self.auth_binding, other.auth_binding, "auth_binding")?;
merge_scalar(&mut self.keep_alive, other.keep_alive, "keep_alive")?;
merge_scalar(
&mut self.render_metadata,
other.render_metadata,
"render_metadata",
)?;
merge_scalar(
&mut self.execution_kind,
other.execution_kind,
"execution_kind",
)?;
merge_scalar(
&mut self.peer_response_terminal_apply_intent,
other.peer_response_terminal_apply_intent,
"peer_response_terminal_apply_intent",
)?;
if let Some(extra) = other.skill_references {
self.skill_references
.get_or_insert_with(Vec::new)
.extend(extra);
}
if let Some(extra) = other.additional_instructions {
self.additional_instructions
.get_or_insert_with(Vec::new)
.extend(extra);
}
Ok(())
}
}
fn merge_scalar<T: PartialEq>(
lhs: &mut Option<T>,
rhs: Option<T>,
field: &'static str,
) -> Result<(), TurnMetadataMergeConflict> {
match (lhs.as_ref(), rhs) {
(_, None) => Ok(()),
(None, Some(v)) => {
*lhs = Some(v);
Ok(())
}
(Some(existing), Some(new)) => {
if *existing == new {
Ok(())
} else {
Err(TurnMetadataMergeConflict {
field,
reason: "two inputs in one batch set distinct scalar overrides",
})
}
}
}
}
fn merge_override<T: PartialEq>(
lhs: &mut Option<TurnMetadataOverride<T>>,
rhs: Option<TurnMetadataOverride<T>>,
field: &'static str,
) -> Result<(), TurnMetadataMergeConflict> {
match (lhs.as_ref(), rhs) {
(_, None) => Ok(()),
(None, Some(override_fact)) => {
*lhs = Some(override_fact);
Ok(())
}
(Some(existing), Some(new)) if *existing == new => Ok(()),
(Some(TurnMetadataOverride::Set(_)), Some(TurnMetadataOverride::Set(_))) => {
Err(TurnMetadataMergeConflict {
field,
reason: "two inputs in one batch set distinct scalar overrides",
})
}
(Some(_), Some(_)) => Err(TurnMetadataMergeConflict {
field,
reason: "one input sets the field while another clears it",
}),
}
}
mod duration_seconds {
use serde::{Deserialize, Deserializer, Serializer};
use std::time::Duration;
pub fn serialize<S: Serializer>(value: &Duration, ser: S) -> Result<S::Ok, S::Error> {
ser.serialize_u64(value.as_secs())
}
pub fn deserialize<'de, D: Deserializer<'de>>(de: D) -> Result<Duration, D::Error> {
let secs = u64::deserialize(de)?;
Ok(Duration::from_secs(secs))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StagedRunInput {
pub boundary: RunApplyBoundary,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub appends: Vec<ConversationAppend>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub context_appends: Vec<ConversationContextAppend>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub contributing_input_ids: Vec<InputId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_metadata: Option<RuntimeTurnMetadata>,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "primitive_type", rename_all = "snake_case")]
#[allow(clippy::large_enum_variant)]
pub enum RunPrimitive {
StagedInput(StagedRunInput),
ImmediateAppend(ConversationAppend),
ImmediateContextAppend(ConversationContextAppend),
}
impl RunPrimitive {
pub fn contributing_input_ids(&self) -> &[InputId] {
match self {
RunPrimitive::StagedInput(staged) => &staged.contributing_input_ids,
RunPrimitive::ImmediateAppend(_) | RunPrimitive::ImmediateContextAppend(_) => &[],
}
}
pub fn turn_metadata(&self) -> Option<&RuntimeTurnMetadata> {
match self {
RunPrimitive::StagedInput(staged) => staged.turn_metadata.as_ref(),
RunPrimitive::ImmediateAppend(_) | RunPrimitive::ImmediateContextAppend(_) => None,
}
}
pub fn extract_content_input(&self) -> crate::types::ContentInput {
match self {
RunPrimitive::StagedInput(staged) => {
content_input_from_conversation_appends(&staged.appends)
}
RunPrimitive::ImmediateAppend(append) => {
content_input_from_core_renderable(&append.content)
}
RunPrimitive::ImmediateContextAppend(ctx) => {
content_input_from_core_renderable(&ctx.content)
}
}
}
pub fn model_projection_content_input(&self) -> crate::types::ContentInput {
match self {
RunPrimitive::StagedInput(staged) => {
model_projection_content_input_from_conversation_appends(&staged.appends)
}
RunPrimitive::ImmediateAppend(append) => {
model_projection_content_input_from_core_renderable(&append.content)
}
RunPrimitive::ImmediateContextAppend(ctx) => {
model_projection_content_input_from_core_renderable(&ctx.content)
}
}
}
pub fn typed_turn_appends(&self) -> Vec<ConversationAppend> {
match self {
RunPrimitive::StagedInput(staged) => staged.appends.clone(),
RunPrimitive::ImmediateAppend(append) => vec![append.clone()],
RunPrimitive::ImmediateContextAppend(_) => Vec::new(),
}
}
pub fn apply_boundary(&self) -> RunApplyBoundary {
match self {
RunPrimitive::StagedInput(staged) => staged.boundary,
RunPrimitive::ImmediateAppend(_) | RunPrimitive::ImmediateContextAppend(_) => {
RunApplyBoundary::Immediate
}
}
}
pub fn peer_response_terminal_apply_intent(&self) -> Option<PeerResponseTerminalApplyIntent> {
self.turn_metadata()
.and_then(|metadata| metadata.peer_response_terminal_apply_intent)
}
pub fn is_peer_response_terminal_context_and_run(&self) -> bool {
matches!(
self.peer_response_terminal_apply_intent(),
Some(PeerResponseTerminalApplyIntent::AppendContextAndRun)
)
}
pub fn peer_response_terminal_apply_intent_violation(&self) -> Option<&'static str> {
if !self.is_peer_response_terminal_context_and_run() {
return None;
}
let RunPrimitive::StagedInput(staged) = self else {
return Some("terminal peer-response apply intent requires a staged primitive");
};
if staged.boundary != RunApplyBoundary::RunStart {
return Some("terminal peer-response apply intent requires RunStart boundary");
}
if staged.context_appends.is_empty() {
return Some("terminal peer-response apply intent requires a staged context append");
}
if staged
.turn_metadata
.as_ref()
.and_then(|metadata| metadata.execution_kind)
!= Some(RuntimeExecutionKind::ContentTurn)
{
return Some("terminal peer-response apply intent requires ContentTurn execution kind");
}
None
}
pub fn is_context_only_apply_without_turn(&self) -> bool {
matches!(
self,
RunPrimitive::StagedInput(staged)
if staged.appends.is_empty()
&& !staged.context_appends.is_empty()
&& !self.is_peer_response_terminal_context_and_run()
)
}
pub fn is_context_only_immediate(&self) -> bool {
matches!(
self,
RunPrimitive::StagedInput(staged)
if staged.appends.is_empty()
&& !staged.context_appends.is_empty()
&& staged.boundary == RunApplyBoundary::Immediate
)
}
}
pub fn content_input_from_conversation_appends(
appends: &[ConversationAppend],
) -> crate::types::ContentInput {
let mut all_blocks = Vec::new();
for append in appends {
append_content_blocks(&append.content, &mut all_blocks);
}
content_input_from_blocks(all_blocks)
}
pub fn model_projection_content_input_from_conversation_appends(
appends: &[ConversationAppend],
) -> crate::types::ContentInput {
let mut all_blocks = Vec::new();
for append in appends {
append_model_projection_blocks(&append.content, &mut all_blocks);
}
content_input_from_blocks(all_blocks)
}
fn content_input_from_core_renderable(content: &CoreRenderable) -> crate::types::ContentInput {
let mut all_blocks = Vec::new();
append_content_blocks(content, &mut all_blocks);
content_input_from_blocks(all_blocks)
}
fn model_projection_content_input_from_core_renderable(
content: &CoreRenderable,
) -> crate::types::ContentInput {
let mut all_blocks = Vec::new();
append_model_projection_blocks(content, &mut all_blocks);
content_input_from_blocks(all_blocks)
}
fn append_content_blocks(
content: &CoreRenderable,
all_blocks: &mut Vec<crate::types::ContentBlock>,
) {
use crate::types::ContentBlock;
match content {
CoreRenderable::Text { text } => {
all_blocks.push(ContentBlock::Text { text: text.clone() });
}
CoreRenderable::Blocks { blocks } => {
all_blocks.extend(blocks.iter().cloned());
}
CoreRenderable::SystemNotice { .. } => {}
_ => {}
}
}
fn append_model_projection_blocks(
content: &CoreRenderable,
all_blocks: &mut Vec<crate::types::ContentBlock>,
) {
use crate::types::{ContentBlock, SystemNoticeMessage};
match content {
CoreRenderable::SystemNotice { kind, body, blocks } => {
let projection = SystemNoticeMessage::with_blocks(*kind, body.clone(), blocks.clone())
.model_projection_text();
if !projection.trim().is_empty() {
all_blocks.push(ContentBlock::Text { text: projection });
}
append_system_notice_media_blocks(blocks, all_blocks);
}
_ => append_content_blocks(content, all_blocks),
}
}
fn append_system_notice_media_blocks(
blocks: &[SystemNoticeBlock],
all_blocks: &mut Vec<crate::types::ContentBlock>,
) {
for block in blocks {
let content = match block {
SystemNoticeBlock::Comms { content, .. }
| SystemNoticeBlock::ExternalEvent { content, .. } => content,
_ => continue,
};
all_blocks.extend(
content
.iter()
.filter(|block| !matches!(block, crate::types::ContentBlock::Text { .. }))
.cloned(),
);
}
}
fn content_input_from_blocks(
all_blocks: Vec<crate::types::ContentBlock>,
) -> crate::types::ContentInput {
use crate::types::{ContentBlock, ContentInput};
if all_blocks.is_empty() {
ContentInput::Text(String::new())
} else if all_blocks.len() == 1 {
if let ContentBlock::Text { text } = &all_blocks[0] {
ContentInput::Text(text.clone())
} else {
ContentInput::Blocks(all_blocks)
}
} else {
ContentInput::Blocks(all_blocks)
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn run_apply_boundary_serde_roundtrip() {
for boundary in [
RunApplyBoundary::Immediate,
RunApplyBoundary::RunStart,
RunApplyBoundary::RunCheckpoint,
] {
let json = serde_json::to_value(boundary).unwrap();
let parsed: RunApplyBoundary = serde_json::from_value(json).unwrap();
assert_eq!(boundary, parsed);
}
}
#[test]
fn core_renderable_text_serde() {
let r = CoreRenderable::Text {
text: "hello".into(),
};
let json = serde_json::to_value(&r).unwrap();
assert_eq!(json["type"], "text");
assert_eq!(json["text"], "hello");
let parsed: CoreRenderable = serde_json::from_value(json).unwrap();
assert_eq!(r, parsed);
}
#[test]
fn core_renderable_json_serde() {
let r = CoreRenderable::Json {
value: serde_json::json!({"key": "val"}),
};
let json = serde_json::to_value(&r).unwrap();
assert_eq!(json["type"], "json");
let parsed: CoreRenderable = serde_json::from_value(json).unwrap();
assert_eq!(r, parsed);
}
fn make_staged(appends: Vec<ConversationAppend>) -> RunPrimitive {
RunPrimitive::StagedInput(StagedRunInput {
boundary: RunApplyBoundary::RunStart,
appends,
context_appends: vec![],
contributing_input_ids: vec![],
turn_metadata: None,
})
}
#[test]
fn extract_content_from_staged_text() {
let p = make_staged(vec![ConversationAppend {
role: ConversationAppendRole::User,
content: CoreRenderable::Text {
text: "hello".into(),
},
}]);
assert_eq!(
p.extract_content_input(),
crate::types::ContentInput::Text("hello".into())
);
}
#[test]
fn extract_content_from_staged_blocks() {
let p = make_staged(vec![ConversationAppend {
role: ConversationAppendRole::User,
content: CoreRenderable::Blocks {
blocks: vec![
crate::types::ContentBlock::Text { text: "a".into() },
crate::types::ContentBlock::Text { text: "b".into() },
],
},
}]);
let result = p.extract_content_input();
assert!(
matches!(&result, crate::types::ContentInput::Blocks(blocks) if blocks.len() == 2),
"expected Blocks with 2 elements, got {result:?}"
);
}
#[test]
fn extract_content_from_staged_empty() {
let p = make_staged(vec![]);
assert_eq!(
p.extract_content_input(),
crate::types::ContentInput::Text(String::new())
);
}
#[test]
fn extract_content_single_text_block_collapses() {
let p = make_staged(vec![ConversationAppend {
role: ConversationAppendRole::User,
content: CoreRenderable::Blocks {
blocks: vec![crate::types::ContentBlock::Text {
text: "single".into(),
}],
},
}]);
assert_eq!(
p.extract_content_input(),
crate::types::ContentInput::Text("single".into())
);
}
#[test]
fn system_notice_append_does_not_leak_projection_into_operator_prompt() {
let append = ConversationAppend {
role: ConversationAppendRole::SystemNotice,
content: CoreRenderable::SystemNotice {
kind: SystemNoticeKind::Comms,
body: Some("Peer request: checksum_token".to_string()),
blocks: vec![SystemNoticeBlock::Comms {
kind: crate::types::CommsNoticeKind::Request,
direction: crate::types::SystemNoticeDirection::Incoming,
peer: Some(crate::types::SystemNoticePeer {
id: crate::comms::PeerId::new(),
display_name: Some("worker-1".to_string()),
}),
request_id: Some(crate::time_compat::new_uuid_v7().to_string()),
intent: Some("checksum_token".to_string()),
status: None,
summary: Some("Peer request: checksum_token".to_string()),
payload: None,
content: vec![crate::types::ContentBlock::Text {
text: "What is the token?".to_string(),
}],
}],
},
};
let p = make_staged(vec![append.clone()]);
assert_eq!(p.typed_turn_appends(), vec![append]);
assert_eq!(
p.extract_content_input(),
crate::types::ContentInput::Text(String::new())
);
let projection = p.model_projection_content_input().text_content();
assert!(projection.contains("Peer request"));
assert!(projection.contains("checksum_token"));
assert!(projection.contains("What is the token?"));
}
#[test]
fn system_notice_media_remains_typed_notice_not_operator_prompt() {
let image = crate::types::ContentBlock::Image {
media_type: "image/png".to_string(),
data: crate::types::ImageData::Inline {
data: "aW1hZ2U=".to_string(),
},
};
let p = make_staged(vec![ConversationAppend {
role: ConversationAppendRole::SystemNotice,
content: CoreRenderable::SystemNotice {
kind: SystemNoticeKind::ExternalEvent,
body: Some("External event".to_string()),
blocks: vec![SystemNoticeBlock::ExternalEvent {
source: "webhook".to_string(),
event_type: "image".to_string(),
summary: Some("Webhook image".to_string()),
body: Some("Do not inject this prose".to_string()),
payload: None,
content: vec![
crate::types::ContentBlock::Text {
text: "Do not inject this text".to_string(),
},
image,
],
}],
},
}]);
assert_eq!(
p.extract_content_input(),
crate::types::ContentInput::Text(String::new())
);
match p.model_projection_content_input() {
crate::types::ContentInput::Blocks(blocks) => {
assert!(blocks.iter().any(|block| matches!(
block,
crate::types::ContentBlock::Text { text }
if text.contains("Do not inject this prose")
&& text.contains("Do not inject this text")
)));
assert!(
blocks
.iter()
.any(|block| matches!(block, crate::types::ContentBlock::Image { .. }))
);
}
other => panic!("expected typed notice projection with media blocks, got {other:?}"),
}
}
#[test]
fn typed_turn_appends_excludes_context_only_appends() {
let p = RunPrimitive::ImmediateContextAppend(ConversationContextAppend {
key: "ctx".to_string(),
content: CoreRenderable::SystemNotice {
kind: SystemNoticeKind::Comms,
body: Some("context".to_string()),
blocks: Vec::new(),
},
});
assert!(p.typed_turn_appends().is_empty());
assert_eq!(
p.extract_content_input(),
crate::types::ContentInput::Text(String::new())
);
}
#[test]
fn context_only_immediate_true() {
let p = RunPrimitive::StagedInput(StagedRunInput {
boundary: RunApplyBoundary::Immediate,
appends: vec![],
context_appends: vec![ConversationContextAppend {
key: "k".into(),
content: CoreRenderable::Text { text: "ctx".into() },
}],
contributing_input_ids: vec![],
turn_metadata: None,
});
assert!(p.is_context_only_immediate());
}
#[test]
fn context_only_immediate_false_with_appends() {
let p = RunPrimitive::StagedInput(StagedRunInput {
boundary: RunApplyBoundary::Immediate,
appends: vec![ConversationAppend {
role: ConversationAppendRole::User,
content: CoreRenderable::Text { text: "hi".into() },
}],
context_appends: vec![ConversationContextAppend {
key: "k".into(),
content: CoreRenderable::Text { text: "ctx".into() },
}],
contributing_input_ids: vec![],
turn_metadata: None,
});
assert!(!p.is_context_only_immediate());
}
#[test]
fn context_only_immediate_false_wrong_boundary() {
let p = RunPrimitive::StagedInput(StagedRunInput {
boundary: RunApplyBoundary::RunCheckpoint,
appends: vec![],
context_appends: vec![ConversationContextAppend {
key: "k".into(),
content: CoreRenderable::Text { text: "ctx".into() },
}],
contributing_input_ids: vec![],
turn_metadata: None,
});
assert!(!p.is_context_only_immediate());
}
#[test]
fn context_only_apply_without_turn_true_for_plain_context() {
let p = RunPrimitive::StagedInput(StagedRunInput {
boundary: RunApplyBoundary::RunCheckpoint,
appends: vec![],
context_appends: vec![ConversationContextAppend {
key: "k".into(),
content: CoreRenderable::Text { text: "ctx".into() },
}],
contributing_input_ids: vec![],
turn_metadata: Some(RuntimeTurnMetadata {
execution_kind: Some(RuntimeExecutionKind::ContentTurn),
..Default::default()
}),
});
assert!(p.is_context_only_apply_without_turn());
}
#[test]
fn terminal_peer_response_context_and_run_bypasses_context_only_shortcut() {
let p = RunPrimitive::StagedInput(StagedRunInput {
boundary: RunApplyBoundary::RunStart,
appends: vec![],
context_appends: vec![ConversationContextAppend {
key: "peer_response_terminal:550e8400-e29b-41d4-a716-446655440000:req-123".into(),
content: CoreRenderable::Text {
text: "Peer terminal response: done".into(),
},
}],
contributing_input_ids: vec![InputId::new()],
turn_metadata: Some(RuntimeTurnMetadata {
execution_kind: Some(RuntimeExecutionKind::ContentTurn),
peer_response_terminal_apply_intent: Some(
PeerResponseTerminalApplyIntent::AppendContextAndRun,
),
..Default::default()
}),
});
assert!(p.is_peer_response_terminal_context_and_run());
assert_eq!(p.peer_response_terminal_apply_intent_violation(), None);
assert!(!p.is_context_only_apply_without_turn());
}
#[test]
fn terminal_peer_response_with_conversation_append_keeps_context_and_run_intent() {
let p = RunPrimitive::StagedInput(StagedRunInput {
boundary: RunApplyBoundary::RunStart,
appends: vec![ConversationAppend {
role: ConversationAppendRole::User,
content: CoreRenderable::Blocks {
blocks: vec![crate::types::ContentBlock::Text {
text: "Peer terminal response: done".into(),
}],
},
}],
context_appends: vec![ConversationContextAppend {
key: "peer_response_terminal:550e8400-e29b-41d4-a716-446655440000:req-123".into(),
content: CoreRenderable::Text {
text: "Peer terminal response: done".into(),
},
}],
contributing_input_ids: vec![InputId::new()],
turn_metadata: Some(RuntimeTurnMetadata {
execution_kind: Some(RuntimeExecutionKind::ContentTurn),
peer_response_terminal_apply_intent: Some(
PeerResponseTerminalApplyIntent::AppendContextAndRun,
),
..Default::default()
}),
});
assert!(p.is_peer_response_terminal_context_and_run());
assert_eq!(p.peer_response_terminal_apply_intent_violation(), None);
assert!(!p.is_context_only_apply_without_turn());
}
#[test]
fn non_staged_is_not_context_only() {
let p = RunPrimitive::ImmediateAppend(ConversationAppend {
role: ConversationAppendRole::User,
content: CoreRenderable::Text { text: "hi".into() },
});
assert!(!p.is_context_only_immediate());
}
#[test]
fn core_renderable_reference_serde() {
let r = CoreRenderable::Reference {
uri: "file:///tmp/a.txt".into(),
label: Some("a file".into()),
};
let json = serde_json::to_value(&r).unwrap();
assert_eq!(json["type"], "reference");
let parsed: CoreRenderable = serde_json::from_value(json).unwrap();
assert_eq!(r, parsed);
}
#[test]
fn execution_kind_serde_round_trip() {
for kind in [
RuntimeExecutionKind::ContentTurn,
RuntimeExecutionKind::ResumePending,
] {
let json = serde_json::to_value(kind).unwrap();
let parsed: RuntimeExecutionKind = serde_json::from_value(json.clone()).unwrap();
assert_eq!(kind, parsed);
}
assert_eq!(
serde_json::to_value(RuntimeExecutionKind::ContentTurn).unwrap(),
serde_json::Value::String("content_turn".into())
);
assert_eq!(
serde_json::to_value(RuntimeExecutionKind::ResumePending).unwrap(),
serde_json::Value::String("resume_pending".into())
);
}
#[test]
fn turn_metadata_execution_kind_defaults_to_none() {
let meta = RuntimeTurnMetadata::default();
assert_eq!(meta.execution_kind, None);
}
#[test]
fn turn_metadata_execution_kind_round_trips() {
let meta = RuntimeTurnMetadata {
execution_kind: Some(RuntimeExecutionKind::ContentTurn),
..Default::default()
};
let json = serde_json::to_value(&meta).unwrap();
assert_eq!(json["execution_kind"], "content_turn");
let parsed: RuntimeTurnMetadata = serde_json::from_value(json).unwrap();
assert_eq!(
parsed.execution_kind,
Some(RuntimeExecutionKind::ContentTurn)
);
}
#[test]
fn turn_metadata_without_execution_kind_deserializes() {
let json = serde_json::json!({});
let parsed: RuntimeTurnMetadata = serde_json::from_value(json).unwrap();
assert_eq!(parsed.execution_kind, None);
}
#[test]
fn conversation_append_role_serde() {
for role in [
ConversationAppendRole::User,
ConversationAppendRole::Assistant,
ConversationAppendRole::SystemNotice,
ConversationAppendRole::Tool,
] {
let json = serde_json::to_value(role).unwrap();
let parsed: ConversationAppendRole = serde_json::from_value(json).unwrap();
assert_eq!(role, parsed);
}
}
#[test]
fn conversation_append_serde() {
let append = ConversationAppend {
role: ConversationAppendRole::User,
content: CoreRenderable::Text {
text: "hello".into(),
},
};
let json = serde_json::to_value(&append).unwrap();
let parsed: ConversationAppend = serde_json::from_value(json).unwrap();
assert_eq!(append, parsed);
}
#[test]
fn staged_run_input_serde() {
let staged = StagedRunInput {
boundary: RunApplyBoundary::RunStart,
appends: vec![ConversationAppend {
role: ConversationAppendRole::User,
content: CoreRenderable::Text {
text: "prompt".into(),
},
}],
context_appends: vec![],
contributing_input_ids: vec![InputId::new()],
turn_metadata: Some(RuntimeTurnMetadata {
keep_alive: Some(KeepAliveDirective::Enable(KeepAlivePolicy {
ttl: std::time::Duration::from_secs(30),
policy: KeepAliveMode::Pinned,
})),
..Default::default()
}),
};
let json = serde_json::to_value(&staged).unwrap();
let parsed: StagedRunInput = serde_json::from_value(json).unwrap();
assert_eq!(staged, parsed);
}
#[test]
fn keep_alive_directive_tri_state_round_trip() {
let disable = RuntimeTurnMetadata {
keep_alive: Some(KeepAliveDirective::Disable),
..Default::default()
};
let json = serde_json::to_value(&disable).unwrap();
let parsed: RuntimeTurnMetadata = serde_json::from_value(json).unwrap();
assert_eq!(parsed.keep_alive, Some(KeepAliveDirective::Disable));
assert!(!parsed.is_empty(), "explicit Disable is not empty metadata");
let enable = RuntimeTurnMetadata {
keep_alive: Some(KeepAliveDirective::Enable(KeepAlivePolicy {
ttl: std::time::Duration::from_secs(30),
policy: KeepAliveMode::Pinned,
})),
..Default::default()
};
let json = serde_json::to_value(&enable).unwrap();
let parsed: RuntimeTurnMetadata = serde_json::from_value(json).unwrap();
assert_eq!(parsed.keep_alive, enable.keep_alive);
assert_ne!(parsed.keep_alive, Some(KeepAliveDirective::Disable));
}
#[test]
fn run_primitive_staged_input_serde() {
let primitive = RunPrimitive::StagedInput(StagedRunInput {
boundary: RunApplyBoundary::RunStart,
appends: vec![],
context_appends: vec![],
contributing_input_ids: vec![InputId::new(), InputId::new()],
turn_metadata: None,
});
let json = serde_json::to_value(&primitive).unwrap();
assert_eq!(json["primitive_type"], "staged_input");
let parsed: RunPrimitive = serde_json::from_value(json).unwrap();
assert_eq!(primitive, parsed);
}
#[test]
fn run_primitive_immediate_append_serde() {
let primitive = RunPrimitive::ImmediateAppend(ConversationAppend {
role: ConversationAppendRole::SystemNotice,
content: CoreRenderable::Text {
text: "notice".into(),
},
});
let json = serde_json::to_value(&primitive).unwrap();
assert_eq!(json["primitive_type"], "immediate_append");
let parsed: RunPrimitive = serde_json::from_value(json).unwrap();
assert_eq!(primitive, parsed);
}
#[test]
fn run_primitive_contributing_input_ids() {
let ids = vec![InputId::new(), InputId::new()];
let primitive = RunPrimitive::StagedInput(StagedRunInput {
boundary: RunApplyBoundary::RunStart,
appends: vec![],
context_appends: vec![],
contributing_input_ids: ids.clone(),
turn_metadata: None,
});
assert_eq!(primitive.contributing_input_ids(), &ids);
let immediate = RunPrimitive::ImmediateAppend(ConversationAppend {
role: ConversationAppendRole::User,
content: CoreRenderable::Text { text: "hi".into() },
});
assert!(immediate.contributing_input_ids().is_empty());
}
#[test]
fn conversation_context_append_serde() {
let ctx = ConversationContextAppend {
key: "peers".into(),
content: CoreRenderable::Json {
value: serde_json::json!(["peer1", "peer2"]),
},
};
let json = serde_json::to_value(&ctx).unwrap();
let parsed: ConversationContextAppend = serde_json::from_value(json).unwrap();
assert_eq!(ctx, parsed);
}
#[test]
fn carrier_effective_params_typed_fieldwise_merge() {
let carrier = ProviderParamsCarrier {
params: ProviderParamsOverride {
temperature: Some(0.3),
provider_tag: Some(ProviderTag::Anthropic(AnthropicProviderTag {
effort: Some(AnthropicEffort::High),
..Default::default()
})),
..Default::default()
},
tool_defaults: Some(ProviderTag::Anthropic(AnthropicProviderTag {
effort: Some(AnthropicEffort::Low),
web_search: Some(OpaqueProviderBody::from_value(
&serde_json::json!({"type": "web_search_20250305"}),
)),
..Default::default()
})),
};
let effective = carrier.effective_params().expect("merge succeeds");
assert_eq!(effective.temperature, Some(0.3));
let Some(ProviderTag::Anthropic(tag)) = effective.provider_tag else {
panic!("anthropic tag expected");
};
assert_eq!(tag.effort, Some(AnthropicEffort::High));
assert_eq!(
tag.web_search,
Some(OpaqueProviderBody::from_value(
&serde_json::json!({"type": "web_search_20250305"})
))
);
}
#[test]
fn carrier_effective_params_provider_mismatch_fails_typed() {
let carrier = ProviderParamsCarrier {
params: ProviderParamsOverride {
provider_tag: Some(ProviderTag::Gemini(GeminiProviderTag::default())),
..Default::default()
},
tool_defaults: Some(ProviderTag::OpenAi(OpenAiProviderTag::default())),
};
let err = carrier.effective_params().expect_err("mismatch is a fault");
assert_eq!(
err,
ProviderParamsMergeError::ProviderTagMismatch {
explicit: "gemini",
defaults: "openai",
}
);
}
#[test]
fn carrier_serde_is_transparent_and_fail_closed() {
let parsed: ProviderParamsCarrier =
serde_json::from_value(serde_json::json!({ "temperature": 0.7 }))
.expect("typed shape parses");
assert_eq!(parsed.params.temperature, Some(0.7));
assert!(parsed.tool_defaults.is_none());
let unknown =
serde_json::from_value::<ProviderParamsCarrier>(serde_json::json!({ "thinking": {} }));
assert!(
unknown.is_err(),
"legacy/unknown provider-params keys must be rejected at ingress"
);
let round = serde_json::to_value(&parsed).expect("serialize");
let keys: Vec<&str> = round
.as_object()
.expect("carrier serializes as an object")
.keys()
.map(String::as_str)
.collect();
assert_eq!(keys, ["temperature"]);
let reparsed: ProviderParamsCarrier =
serde_json::from_value(round).expect("round-trip parses");
assert_eq!(reparsed, parsed);
}
#[test]
fn set_structured_output_typed_injection() {
let schema = crate::OutputSchema::from_json_value(
serde_json::json!({"type": "object", "properties": {}}),
)
.expect("schema");
let mut params = ProviderParamsOverride::default();
params
.set_structured_output(Provider::Anthropic, schema.clone())
.expect("inject into empty override");
assert!(matches!(
params.provider_tag,
Some(ProviderTag::Anthropic(AnthropicProviderTag {
structured_output: Some(_),
..
}))
));
let mut mismatched = ProviderParamsOverride {
provider_tag: Some(ProviderTag::Gemini(GeminiProviderTag::default())),
..Default::default()
};
let err = mismatched
.set_structured_output(Provider::Anthropic, schema.clone())
.expect_err("identity conflict is typed");
assert!(matches!(
err,
ProviderParamsMergeError::ProviderTagMismatch { .. }
));
let mut other = ProviderParamsOverride::default();
let outcome = other
.set_structured_output(Provider::Other, schema)
.expect("no-slot providers proceed prompt-based");
assert_eq!(outcome, StructuredOutputInjection::NoProviderSlot);
assert!(other.provider_tag.is_none());
}
#[test]
fn opaque_provider_body_round_trip() {
let v = serde_json::json!({"max_uses": 5, "allowed_domains": ["example.com"]});
let body = OpaqueProviderBody::from_value(&v);
assert_eq!(body.as_value(), v);
}
}