use serde::{Deserialize, Deserializer, Serialize};
use crate::config::ApiProvider;
use super::events::{ChangeReceipt, ObservationSummary, WorkGraphProposal};
use super::ids::{BindingId, WorkEdgeId, WorkNodeId};
pub type Ts = i64;
pub const SCHEMA_VERSION: u32 = 1;
pub const HISTORY_CAP: usize = 256;
pub const ACTIVITY_CAP: usize = 256;
pub const SEEN_KEYS_CAP: usize = 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReasoningEffortTier {
Off,
Low,
Medium,
High,
Auto,
Max,
ThinkingEnabledGranularityUnavailable,
Unavailable,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum WorkActivityEvent {
ReasoningEffortChanged {
requested: ReasoningEffortTier,
effective: ReasoningEffortTier,
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_kind: Option<ApiProvider>,
provider: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
endpoint_identity: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
model: Option<String>,
ts: Ts,
#[serde(default, skip_serializing_if = "Option::is_none")]
operation: Option<WorkNodeId>,
},
}
#[derive(Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
enum WorkActivityEventWire {
ReasoningEffortChanged {
requested: ReasoningEffortTier,
effective: ReasoningEffortTier,
#[serde(default)]
provider_kind: Option<ApiProvider>,
provider: String,
#[serde(default)]
endpoint_identity: Option<String>,
#[serde(default)]
model: Option<String>,
ts: Ts,
#[serde(default)]
operation: Option<WorkNodeId>,
},
}
impl<'de> Deserialize<'de> for WorkActivityEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
match WorkActivityEventWire::deserialize(deserializer)? {
WorkActivityEventWire::ReasoningEffortChanged {
requested,
mut effective,
provider_kind,
provider,
endpoint_identity,
model,
ts,
operation,
} => {
if provider_kind.is_none() || endpoint_identity.is_none() || model.is_none() {
effective = ReasoningEffortTier::Unavailable;
}
Ok(Self::ReasoningEffortChanged {
requested,
effective,
provider_kind,
provider,
endpoint_identity,
model,
ts,
operation,
})
}
}
}
}
#[must_use]
pub(crate) fn constrained_effective_reasoning_for_route(
requested: ReasoningEffortTier,
provider: ApiProvider,
endpoint_identity: &str,
model: &str,
) -> Option<ReasoningEffortTier> {
use ReasoningEffortTier::{
Auto, High, Low, Medium, Off, ThinkingEnabledGranularityUnavailable, Unavailable,
};
if provider == ApiProvider::Zai {
if !crate::config::is_exact_zai_chat_route(provider, endpoint_identity) {
return Some(Unavailable);
}
if crate::config::is_exact_zai_glm_5_2_route(provider, endpoint_identity, model) {
return Some(match requested {
Low | Medium => High,
other => other,
});
}
if crate::config::is_exact_known_zai_reasoning_route(provider, endpoint_identity, model) {
return Some(match requested {
Off | Auto => requested,
_ => ThinkingEnabledGranularityUnavailable,
});
}
return Some(Unavailable);
}
if provider == ApiProvider::Minimax {
if crate::config::is_exact_minimax_m3_route(provider, endpoint_identity, model) {
return Some(match requested {
Off | Auto => requested,
_ => ThinkingEnabledGranularityUnavailable,
});
}
return Some(Unavailable);
}
if provider == ApiProvider::MinimaxAnthropic {
if crate::config::is_exact_minimax_anthropic_m3_route(provider, endpoint_identity, model) {
return Some(match requested {
Off | Auto => requested,
_ => ThinkingEnabledGranularityUnavailable,
});
}
return Some(Unavailable);
}
if provider == ApiProvider::Custom {
return Some(Unavailable);
}
if crate::config::is_exact_kimi_code_k3_route(provider, endpoint_identity, model) {
return Some(match requested {
Off => Low,
other => other,
});
}
if crate::config::is_exact_direct_moonshot_k3_route(provider, endpoint_identity, model) {
return Some(match requested {
Off => Low,
Medium => High,
other => other,
});
}
None
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeKind {
Objective,
PlanStep,
Operation,
Evidence,
Blocker,
Approval,
RuntimeRef,
LaneRef,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EdgeKind {
Contains,
DependsOn,
Blocks,
Produces,
Verifies,
RunsOn,
RequiresApproval,
Supersedes,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeState {
Ready,
Initializing,
Active,
Waiting,
Blocked,
Completed,
Verified,
Stale,
Superseded,
Cancelled,
Failed,
}
impl NodeState {
#[must_use]
pub fn is_terminal(self) -> bool {
matches!(
self,
NodeState::Verified | NodeState::Superseded | NodeState::Cancelled
)
}
#[must_use]
pub fn is_live(self) -> bool {
matches!(
self,
NodeState::Initializing | NodeState::Active | NodeState::Waiting
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceKindTag {
ToolRun,
Artifact,
TestSummary,
Receipt,
Approval,
Route,
WebCitation,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum EvidenceKind {
ToolRun,
Artifact {
digest: String,
},
TestSummary,
Receipt {
owner: String,
},
Approval,
Route,
WebCitation {
ref_id: String,
url: String,
retrieved_at: String,
},
}
impl EvidenceKind {
#[must_use]
pub fn tag(&self) -> EvidenceKindTag {
match self {
EvidenceKind::ToolRun => EvidenceKindTag::ToolRun,
EvidenceKind::Artifact { .. } => EvidenceKindTag::Artifact,
EvidenceKind::TestSummary => EvidenceKindTag::TestSummary,
EvidenceKind::Receipt { .. } => EvidenceKindTag::Receipt,
EvidenceKind::Approval => EvidenceKindTag::Approval,
EvidenceKind::Route => EvidenceKindTag::Route,
EvidenceKind::WebCitation { .. } => EvidenceKindTag::WebCitation,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EvidenceRefError {
EmptyReference,
ReferenceTooLong { len: usize },
AbsolutePath,
HomeRelativePath,
ContainsWhitespaceOrControl,
LooksLikeKeyMaterial,
WebCitationReferenceMismatch,
InvalidWebCitationUrl,
InvalidWebCitationTimestamp,
}
impl std::fmt::Display for EvidenceRefError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EvidenceRefError::EmptyReference => write!(f, "evidence reference is empty"),
EvidenceRefError::ReferenceTooLong { len } => {
write!(f, "evidence reference too long ({len} chars)")
}
EvidenceRefError::AbsolutePath => {
write!(f, "evidence reference must not be an absolute path")
}
EvidenceRefError::HomeRelativePath => {
write!(f, "evidence reference must not be a home-relative path")
}
EvidenceRefError::ContainsWhitespaceOrControl => {
write!(
f,
"evidence reference must not contain whitespace or control chars"
)
}
EvidenceRefError::LooksLikeKeyMaterial => {
write!(f, "evidence reference must not embed key material")
}
EvidenceRefError::WebCitationReferenceMismatch => {
write!(f, "web citation reference must match its ref_id")
}
EvidenceRefError::InvalidWebCitationUrl => {
write!(f, "web citation URL must be HTTP(S) without credentials")
}
EvidenceRefError::InvalidWebCitationTimestamp => {
write!(f, "web citation retrieved_at must be RFC 3339")
}
}
}
}
impl std::error::Error for EvidenceRefError {}
const EVIDENCE_REFERENCE_MAX_LEN: usize = 512;
fn web_citation_url_has_sensitive_query(url: &reqwest::Url) -> bool {
url.query_pairs().any(|(name, _)| {
let name = name.to_ascii_lowercase();
matches!(
name.as_ref(),
"access_token"
| "api_key"
| "authorization"
| "auth"
| "credential"
| "key"
| "session"
| "session_id"
| "sig"
| "signature"
| "token"
| "x-amz-credential"
| "x-amz-signature"
| "x-goog-credential"
| "x-goog-signature"
) || name.ends_with("_token")
|| name.ends_with("_key")
})
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(try_from = "EvidenceRefRaw", into = "EvidenceRefRaw")]
pub struct EvidenceRef {
kind: EvidenceKind,
reference: String,
raw_bytes: Option<u64>,
truncated: bool,
}
impl EvidenceRef {
pub fn new(
kind: EvidenceKind,
reference: impl Into<String>,
raw_bytes: Option<u64>,
truncated: bool,
) -> Result<Self, EvidenceRefError> {
let reference = reference.into();
if reference.is_empty() {
return Err(EvidenceRefError::EmptyReference);
}
if reference.chars().count() > EVIDENCE_REFERENCE_MAX_LEN {
return Err(EvidenceRefError::ReferenceTooLong {
len: reference.chars().count(),
});
}
let mut chars = reference.chars();
let first = chars.next().unwrap_or('\0');
let drive_absolute = {
let bytes = reference.as_bytes();
bytes.len() >= 3
&& bytes[0].is_ascii_alphabetic()
&& bytes[1] == b':'
&& (bytes[2] == b'/' || bytes[2] == b'\\')
};
if first == '/' || first == '\\' || drive_absolute {
return Err(EvidenceRefError::AbsolutePath);
}
if first == '~' {
return Err(EvidenceRefError::HomeRelativePath);
}
if reference
.chars()
.any(|c| c.is_whitespace() || c.is_control())
{
return Err(EvidenceRefError::ContainsWhitespaceOrControl);
}
if reference.contains("-----BEGIN") {
return Err(EvidenceRefError::LooksLikeKeyMaterial);
}
if let EvidenceKind::WebCitation {
ref_id,
url,
retrieved_at,
} = &kind
{
if ref_id != &reference {
return Err(EvidenceRefError::WebCitationReferenceMismatch);
}
let parsed = reqwest::Url::parse(url)
.ok()
.filter(|url| matches!(url.scheme(), "http" | "https"))
.filter(|url| url.host_str().is_some())
.filter(|url| url.username().is_empty() && url.password().is_none())
.filter(|url| !web_citation_url_has_sensitive_query(url));
if parsed.is_none() {
return Err(EvidenceRefError::InvalidWebCitationUrl);
}
if chrono::DateTime::parse_from_rfc3339(retrieved_at).is_err() {
return Err(EvidenceRefError::InvalidWebCitationTimestamp);
}
}
Ok(Self {
kind,
reference,
raw_bytes,
truncated,
})
}
#[must_use]
pub fn kind(&self) -> &EvidenceKind {
&self.kind
}
#[must_use]
pub fn reference(&self) -> &str {
&self.reference
}
#[must_use]
pub fn raw_bytes(&self) -> Option<u64> {
self.raw_bytes
}
#[must_use]
pub fn truncated(&self) -> bool {
self.truncated
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EvidenceRefRaw {
kind: EvidenceKind,
reference: String,
raw_bytes: Option<u64>,
truncated: bool,
}
impl TryFrom<EvidenceRefRaw> for EvidenceRef {
type Error = EvidenceRefError;
fn try_from(raw: EvidenceRefRaw) -> Result<Self, Self::Error> {
EvidenceRef::new(raw.kind, raw.reference, raw.raw_bytes, raw.truncated)
}
}
impl From<EvidenceRef> for EvidenceRefRaw {
fn from(value: EvidenceRef) -> Self {
EvidenceRefRaw {
kind: value.kind,
reference: value.reference,
raw_bytes: value.raw_bytes,
truncated: value.truncated,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AcceptanceRequirement {
EvidenceOfKind { kind: EvidenceKindTag },
}
impl AcceptanceRequirement {
#[must_use]
pub fn is_satisfied_by(&self, evidence: &EvidenceRef) -> bool {
match self {
AcceptanceRequirement::EvidenceOfKind { kind } => evidence.kind().tag() == *kind,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Provenance {
Import {
source_digest: String,
ordinal: Option<u32>,
},
ToolUpdate {
tool: String,
call_id: String,
},
RuntimeReconcile {
source: String,
observed_at: Ts,
},
UserEdit {
proposal_id: super::ids::ProposalId,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OperationBinding {
pub external: String,
pub durable: bool,
#[serde(default)]
pub last_observation: Option<ObservationSummary>,
}
#[must_use]
pub fn external_identity_is_well_formed(external: &str) -> bool {
fn plain(id: &str) -> bool {
!id.is_empty() && !id.chars().any(|c| c.is_whitespace() || c.is_control())
}
if let Some(rest) = external.strip_prefix("fleet:") {
return match rest.split_once('/') {
Some((run, task)) => plain(run) && plain(task),
None => false,
};
}
["task:", "shell:", "worker:", "workflow:", "lane:"]
.iter()
.any(|prefix| external.strip_prefix(prefix).is_some_and(plain))
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WorkNode {
pub id: WorkNodeId,
pub kind: NodeKind,
pub title: String,
pub state: NodeState,
pub acceptance: Vec<AcceptanceRequirement>,
pub binding: Option<OperationBinding>,
pub evidence: Option<EvidenceRef>,
pub provenance: Provenance,
pub created_at: Ts,
pub updated_at: Ts,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WorkEdge {
pub id: WorkEdgeId,
pub kind: EdgeKind,
pub from: WorkNodeId,
pub to: WorkNodeId,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompatPlanMetadata {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub objective: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_summary: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub explanation: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sources_used: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub critical_files: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub constraints: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recommended_approach: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verification_plan: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub risks_and_unknowns: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub handoff_packet: Option<String>,
}
impl CompatPlanMetadata {
#[must_use]
pub fn is_empty(&self) -> bool {
self.title.is_none()
&& self.objective.is_none()
&& self.context_summary.is_none()
&& self.explanation.is_none()
&& self.sources_used.is_empty()
&& self.critical_files.is_empty()
&& self.constraints.is_empty()
&& self.recommended_approach.is_none()
&& self.verification_plan.is_none()
&& self.risks_and_unknowns.is_none()
&& self.handoff_packet.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompatTodoBinding {
pub legacy_id: u32,
pub node: WorkNodeId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub plan_index: Option<u32>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CompatProjectionState {
#[serde(default, skip_serializing_if = "CompatPlanMetadata::is_empty")]
pub plan: CompatPlanMetadata,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub plan_order: Vec<WorkNodeId>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub todos: Vec<CompatTodoBinding>,
}
impl CompatProjectionState {
#[must_use]
pub fn is_empty(&self) -> bool {
self.plan.is_empty() && self.plan_order.is_empty() && self.todos.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub struct IdempotencyKey {
pub binding: BindingId,
pub seq: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BoundedVec<T, const N: usize> {
items: Vec<T>,
}
impl<T, const N: usize> BoundedVec<T, N> {
#[must_use]
pub fn new() -> Self {
Self { items: Vec::new() }
}
pub fn push_bounded(&mut self, item: T) {
if self.items.len() >= N {
self.items.remove(0);
}
self.items.push(item);
}
#[must_use]
pub fn len(&self) -> usize {
self.items.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
#[must_use]
pub fn last(&self) -> Option<&T> {
self.items.last()
}
pub fn iter(&self) -> std::slice::Iter<'_, T> {
self.items.iter()
}
#[must_use]
pub const fn capacity() -> usize {
N
}
}
impl<T, const N: usize> Default for BoundedVec<T, N> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct BoundedSet<T, const N: usize> {
items: Vec<T>,
}
impl<T: PartialEq, const N: usize> BoundedSet<T, N> {
#[must_use]
pub fn new() -> Self {
Self { items: Vec::new() }
}
#[must_use]
pub fn contains(&self, item: &T) -> bool {
self.items.contains(item)
}
pub fn insert(&mut self, item: T) -> bool {
if self.contains(&item) {
return false;
}
if self.items.len() >= N {
self.items.remove(0);
}
self.items.push(item);
true
}
#[must_use]
pub fn len(&self) -> usize {
self.items.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
impl<T: PartialEq, const N: usize> Default for BoundedSet<T, N> {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct WorkGraphSnapshot {
pub schema: u32,
pub revision: u64,
pub nodes: Vec<WorkNode>,
pub edges: Vec<WorkEdge>,
pub history: BoundedVec<ChangeReceipt, HISTORY_CAP>,
#[serde(default, skip_serializing_if = "BoundedVec::is_empty")]
pub activities: BoundedVec<WorkActivityEvent, ACTIVITY_CAP>,
pub import_digest: Option<String>,
pub seen_keys: BoundedSet<IdempotencyKey, SEEN_KEYS_CAP>,
pub proposals: Vec<WorkGraphProposal>,
#[serde(default, skip_serializing_if = "CompatProjectionState::is_empty")]
pub compat: CompatProjectionState,
}
impl WorkGraphSnapshot {
#[must_use]
pub fn new() -> Self {
Self {
schema: SCHEMA_VERSION,
revision: 0,
nodes: Vec::new(),
edges: Vec::new(),
history: BoundedVec::new(),
activities: BoundedVec::new(),
import_digest: None,
seen_keys: BoundedSet::new(),
proposals: Vec::new(),
compat: CompatProjectionState::default(),
}
}
#[must_use]
pub fn node(&self, id: &WorkNodeId) -> Option<&WorkNode> {
self.nodes.iter().find(|n| &n.id == id)
}
pub(super) fn node_mut(&mut self, id: &WorkNodeId) -> Option<&mut WorkNode> {
self.nodes.iter_mut().find(|n| &n.id == id)
}
#[must_use]
pub fn edge(&self, id: &WorkEdgeId) -> Option<&WorkEdge> {
self.edges.iter().find(|e| &e.id == id)
}
#[must_use]
pub fn node_is_done(node: &WorkNode) -> bool {
matches!(node.state, NodeState::Verified)
|| (matches!(node.state, NodeState::Completed) && node.acceptance.is_empty())
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
&& self.edges.is_empty()
&& self.history.is_empty()
&& self.activities.is_empty()
&& self.import_digest.is_none()
&& self.proposals.is_empty()
&& self.compat.is_empty()
}
}
impl Default for WorkGraphSnapshot {
fn default() -> Self {
Self::new()
}
}