use std::io::Write;
use std::path::Path;
use std::process::{Command, Stdio};
use harn_serve::adapters::acp::{
ACP_PROMPT_ERROR_DATA_SCHEMA, ACP_SCHEMA_COMPATIBILITY, HARN_AGENT_EVENT_KINDS,
HARN_AGENT_EVENT_METHOD, HARN_CONTENT_EXTENSION_FIELDS, HARN_PROMPT_RESULT_EXTENSION_FIELDS,
HARN_PROVIDER_CATALOG_METHOD, HARN_SESSION_UPDATE_EXTENSIONS,
HARN_TOOL_LIFECYCLE_EXTENSION_FIELDS,
};
use super::activity::ActivityVocabulary;
use super::connector_setup::ConnectorSetupVocabulary;
use super::constants::*;
use super::external_action::ExternalActionVocabulary;
use super::prepared_session::append_rust_prepared_session_types;
use super::session_recap::append_rust_session_recap_types;
use super::session_update_payloads::append_rust_session_update_payloads;
use super::support::*;
use super::values::*;
#[cfg(test)]
pub(super) fn generate_rust_for_tests() -> String {
generate_rust(
&ExternalActionVocabulary::load_for_tests(),
&ConnectorSetupVocabulary::load_for_tests(),
&ActivityVocabulary::load_for_tests(),
)
}
pub(super) fn generate_rust(
external_actions: &ExternalActionVocabulary,
connector_setup: &ConnectorSetupVocabulary,
activity: &ActivityVocabulary,
) -> String {
let mut out = String::new();
out.push_str("// GENERATED by `harn dump-protocol-artifacts` - do not edit by hand.\n");
out.push_str("// Source: Harn adapter schemas and Harn-owned wire registries.\n\n");
out.push_str("//! Rust bindings for Harn's host/integrator protocol surface.\n");
out.push_str("//!\n");
out.push_str("//! Mirrors the TypeScript, Swift, Python, and Go artifacts generated\n");
out.push_str("//! alongside this module. Constants carry literal JSON wire strings, while\n");
out.push_str("//! serde DTOs own the canonical ACP permission and Harn agent-event shapes.\n");
out.push_str("//! Adding a wire value or optional field is minor-version compatible.\n");
out.push_str("#![allow(dead_code)]\n\n");
out.push_str("use serde::{Deserialize, Deserializer, Serialize, Serializer};\n");
out.push_str("use serde_json::{Map, Value};\n\n");
out.push_str("pub const HARN_TOOL_PERMISSION_DECISION_SCHEMA: &str = \"harn.tool_permission_decision.v1\";\n");
out.push_str("pub const HARN_TOOL_PERMISSION_ACTIVITY_SCHEMA: &str = \"harn.tool_permission_activity.v1\";\n\n");
out.push_str("pub const HARN_EXTERNAL_ACTION_ACTIVITY_SCHEMA: &str = \"harn.external_action_activity.v1\";\n");
out.push_str("pub const HARN_EXTERNAL_ACTION_RECEIPT_SCHEMA: &str = \"harn.external_action_receipt.v1\";\n\n");
out.push_str("/// Upstream ACP schema version Harn tracks.\n");
out.push_str(&format!(
"pub const ACP_SCHEMA_COMPATIBILITY: &str = {};\n\n",
json_string_literal(ACP_SCHEMA_COMPATIBILITY)
));
out.push_str("/// JSON-RPC method for `_harn/agentEvent` extension notifications.\n");
out.push_str(&format!(
"pub const HARN_AGENT_EVENT_METHOD: &str = {};\n\n",
json_string_literal(HARN_AGENT_EVENT_METHOD)
));
out.push_str("/// JSON-RPC method for Harn's provider catalog extension.\n");
out.push_str(&format!(
"pub const HARN_PROVIDER_CATALOG_METHOD: &str = {};\n\n",
json_string_literal(HARN_PROVIDER_CATALOG_METHOD)
));
out.push_str("/// Schema discriminator for typed `session/prompt` JSON-RPC error data.\n");
out.push_str(&format!(
"pub const ACP_PROMPT_ERROR_DATA_SCHEMA: &str = {};\n\n",
json_string_literal(ACP_PROMPT_ERROR_DATA_SCHEMA)
));
for (key, suffix, values) in external_actions.projections() {
out.push_str(&rust_string_enum(
&format!("HarnExternalAction{suffix}"),
&format!(
"Closed external-action {} owned by `std/external_action/vocabulary`.",
key.replace('_', " ")
),
values,
));
}
out.push_str(&rust_terminal_activity_status(
&external_actions.terminal_activity_statuses,
));
out.push_str(&rust_activity_progress(
&external_actions.progress_activity_statuses,
));
for record in &external_actions.records {
record.append(&mut out, super::records::Target::Rust);
}
for (key, name, values) in activity.projections() {
out.push_str(&rust_string_enum(
name,
&format!(
"Closed {} owned by `std/activity/vocabulary`.",
key.replace('_', " ")
),
values,
));
}
out.push_str(&rust_string_enum(
"HarnACPToolKind",
"Closed ACP tool kinds owned by Harn's tool annotation registry.",
&tool_kind_values(),
));
out.push_str(&rust_string_enum(
"HarnSideEffectLevel",
"Closed side-effect levels owned by Harn's tool annotation registry.",
&side_effect_level_values(),
));
out.push_str(&rust_string_enum(
"HarnCompletionEvidenceRole",
"Closed completion-evidence roles owned by Harn's tool annotation registry.",
&completion_evidence_role_values(),
));
out.push_str(
"#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]\n\
pub struct HarnToolArgSchema {\n\
\x20 pub path_params: Vec<String>,\n\
\x20 pub dependency_key_params: Vec<String>,\n\
\x20 pub dependency_range_params: Vec<std::collections::BTreeMap<String, String>>,\n\
\x20 pub arg_aliases: std::collections::BTreeMap<String, String>,\n\
\x20 pub required: Vec<String>,\n\
}\n\n\
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]\n\
pub struct HarnToolAnnotations {\n\
\x20 pub kind: HarnACPToolKind,\n\
\x20 pub side_effect_level: HarnSideEffectLevel,\n\
\x20 #[serde(skip_serializing_if = \"Option::is_none\")]\n\
\x20 pub completion_evidence_role: Option<HarnCompletionEvidenceRole>,\n\
\x20 pub arg_schema: HarnToolArgSchema,\n\
\x20 pub capabilities: std::collections::BTreeMap<String, Vec<String>>,\n\
\x20 pub emits_artifacts: bool,\n\
\x20 pub result_readers: Vec<String>,\n\
\x20 pub inline_result: bool,\n\
}\n\n",
);
for record in &activity.records {
record.append(&mut out, super::records::Target::Rust);
}
out.push_str(&rust_string_enum(
"HarnConnectorSetupStage",
"Closed connector-setup stages owned by `std/connectors/setup`.",
&connector_setup.stages,
));
out.push_str(&rust_string_enum(
"HarnConnectorSetupStatus",
"Closed connector-setup statuses owned by `std/connectors/setup`.",
&connector_setup.statuses,
));
out.push_str(&rust_string_enum(
"HarnConnectorSetupInteraction",
"Closed connector-setup interactions owned by `std/connectors/setup`.",
&connector_setup.interactions,
));
out.push_str(&rust_string_enum(
"HarnConnectorSetupConfigurationField",
"Closed connector-setup configuration fields owned by `std/connectors/setup`.",
&connector_setup.configuration_fields,
));
out.push_str(&rust_string_enum(
"HarnConnectorSetupErrorCode",
"Closed connector-setup error codes owned by `std/connectors/setup`.",
&connector_setup.error_codes,
));
for record in &connector_setup.records {
record.append(&mut out, super::records::Target::Rust);
}
out.push_str(&rust_wire_types());
for vocabulary in acp_method_vocabularies() {
out.push_str(&rust_const_group_owned(
vocabulary.rust_const_prefix,
vocabulary.rust_slice_name,
vocabulary.rust_doc,
&vocabulary.values,
));
}
out.push_str(&rust_const_group(
"HARN_SESSION_TIMELINE_METHOD",
"HARN_SESSION_TIMELINE_METHODS",
"Session-timeline extension methods Harn accepts or emits.",
HARN_SESSION_TIMELINE_METHODS,
));
out.push_str(&rust_const_group(
"ACP_AGENT_NOTIFICATION",
"ACP_AGENT_NOTIFICATIONS",
"ACP notifications the agent emits to the host.",
ACP_AGENT_NOTIFICATIONS,
));
let session_updates = all_acp_session_updates();
out.push_str(&rust_const_group_owned(
"ACP_SESSION_UPDATE",
"ACP_SESSION_UPDATES",
"Every `session/update` discriminator Harn emits (canonical ACP variants \
plus Harn extensions), the union the Swift binding exposes as \
`acpSessionUpdateExtensions` merged with the base ACP set.",
&session_updates,
));
out.push_str(&rust_const_group(
"HARN_ACP_SESSION_UPDATE_EXTENSION",
"HARN_ACP_SESSION_UPDATE_EXTENSIONS",
"Harn-specific `session/update` discriminators beyond the canonical ACP \
set (the values Swift publishes as `acpSessionUpdateExtensions`).",
HARN_SESSION_UPDATE_EXTENSIONS,
));
out.push_str(&rust_const_group(
"HARN_AGENT_EVENT_KIND",
"HARN_AGENT_EVENT_KINDS",
"Pipeline-loop milestone kinds emitted via `_harn/agentEvent`.",
HARN_AGENT_EVENT_KINDS,
));
out.push_str(&rust_const_group_owned(
"AGENT_TERMINAL_CLASS",
"AGENT_TERMINAL_CLASSES",
"Stable terminal classes carried by typed ACP prompt-error data. \
Superseded by `HarnAgentTerminalClass`; retained for one release so \
existing consumers keep compiling.",
&agent_terminal_class_values(),
));
out.push_str(&rust_const_group_owned(
"AGENT_TERMINAL_KIND",
"AGENT_TERMINAL_KINDS",
"Producer-owned agent terminal outcome kinds. Superseded by \
`HarnAgentTerminalKind`; retained for one release so existing \
consumers keep compiling.",
&agent_terminal_kind_values(),
));
out.push_str(&rust_const_group_owned(
"AGENT_TERMINAL_OWNER",
"AGENT_TERMINAL_OWNERS",
"Owners attributed by producer-owned agent terminal outcomes. \
Superseded by `HarnAgentTerminalOwner`; retained for one release so \
existing consumers keep compiling.",
&agent_terminal_owner_values(),
));
out.push_str(&rust_open_string_enum(
"HarnAgentTerminalClass",
"Stable terminal classes carried by typed ACP prompt-error data.",
&agent_terminal_class_values(),
));
out.push_str(&rust_open_string_enum(
"HarnAgentTerminalKind",
"Producer-owned agent terminal outcome kinds.",
&agent_terminal_kind_values(),
));
out.push_str(&rust_open_string_enum(
"HarnAgentTerminalOwner",
"Owners attributed by producer-owned agent terminal outcomes.",
&agent_terminal_owner_values(),
));
out.push_str(&rust_open_string_enum(
"HarnLlmErrorCategory",
"Thrown-error categories carried in `category` on the \
`harn.acp.prompt_error.v1` envelope. Owned by `harn_vm`'s \
`ErrorCategory`.",
&llm_error_category_values(),
));
out.push_str(&rust_open_string_enum(
"HarnLlmErrorKind",
"Coarse retry semantics carried in `kind` on the \
`harn.acp.prompt_error.v1` envelope. Owned by `harn_vm`'s \
`LlmErrorKind`. `transient` means a byte-identical replay may \
succeed; `terminal` means it cannot.",
&llm_error_kind_values(),
));
out.push_str(&rust_open_string_enum(
"HarnLlmErrorReason",
"Canonical provider-failure reason carried in `reason` on the \
`harn.acp.prompt_error.v1` envelope. Owned by `harn_vm`'s \
`LlmErrorReason`. The sibling `code` field is a PROVIDER PASSTHROUGH \
with no closed set: it is opaque diagnostic text, and a host must \
never branch on it. Branch on `reason` instead.",
&llm_error_reason_values(),
));
out.push_str(&rust_const_group(
"HARN_PROMPT_RESULT_EXTENSION_FIELD",
"HARN_PROMPT_RESULT_EXTENSION_FIELDS",
"`_meta.harn` extension keys on successful ACP prompt results.",
HARN_PROMPT_RESULT_EXTENSION_FIELDS,
));
out.push_str(&rust_const_group(
"HARN_CONTENT_EXTENSION_FIELD",
"HARN_CONTENT_EXTENSION_FIELDS",
"`_meta.harn` extension keys Harn attaches to ACP content.",
HARN_CONTENT_EXTENSION_FIELDS,
));
out.push_str(&rust_const_group(
"HARN_TOOL_LIFECYCLE_EXTENSION_FIELD",
"HARN_TOOL_LIFECYCLE_EXTENSION_FIELDS",
"`_meta.harn` tool-lifecycle extension keys on tool_call / \
tool_call_update notifications.",
HARN_TOOL_LIFECYCLE_EXTENSION_FIELDS,
));
append_rust_session_update_payloads(&mut out);
append_rust_prepared_session_types(&mut out);
append_rust_session_recap_types(&mut out);
super::plan_records::append(&mut out, super::records::Target::Rust);
while out.ends_with("\n\n") {
out.pop();
}
out
}
pub(super) fn format_rust_source(source: String, repo_root: &Path) -> Result<String, String> {
let mut child = match Command::new("rustfmt")
.args(["--emit", "stdout", "--quiet", "--edition", "2021"])
.current_dir(repo_root)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Err(
"rustfmt is required to generate canonical Rust protocol bindings; install the pinned Rust toolchain or run `rustup component add rustfmt`".to_string(),
);
}
Err(error) => return Err(format!("failed to spawn rustfmt: {error}")),
};
child
.stdin
.as_mut()
.ok_or_else(|| "failed to open rustfmt stdin".to_string())?
.write_all(source.as_bytes())
.map_err(|error| format!("failed to write generated Rust to rustfmt: {error}"))?;
let output = child
.wait_with_output()
.map_err(|error| format!("failed to wait for rustfmt: {error}"))?;
if !output.status.success() {
return Err(format!(
"rustfmt failed on generated Rust protocol artifact: {}",
String::from_utf8_lossy(&output.stderr)
));
}
String::from_utf8(output.stdout).map_err(|error| {
format!("rustfmt returned non-UTF-8 output for generated Rust protocol artifact: {error}")
})
}
fn rust_open_string_enum(name: &str, doc: &str, values: &[String]) -> String {
for value in values {
assert!(
rust_type_name(value) != "Unrecognized",
"wire value `{value}` in `{name}` collides with the open-enum escape variant"
);
}
let mut out = rust_doc_comment(doc);
out.push_str(&rust_doc_comment(
"Open vocabulary: unit variants are the values this binding was generated \
from, and `Unrecognized` carries any other string verbatim so a newer \
Harn never breaks an older consumer.",
));
out.push_str("#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]\n");
out.push_str("#[serde(from = \"String\", into = \"String\")]\n");
out.push_str(&format!("pub enum {name} {{\n"));
for value in values {
out.push_str(&format!(" {},\n", rust_type_name(value)));
}
out.push_str(
" /// A wire value outside the vocabulary this binding was generated \
from. Preserved verbatim.\n Unrecognized(String),\n",
);
out.push_str("}\n\n");
out.push_str(&format!("impl {name} {{\n"));
out.push_str(
" /// Every value this binding was generated from, in wire order.\n\
\x20 /// Excludes the `Unrecognized` escape.\n",
);
out.push_str(" pub const KNOWN: &'static [Self] = &[\n");
for value in values {
out.push_str(&format!(" Self::{},\n", rust_type_name(value)));
}
out.push_str(" ];\n\n");
out.push_str(" /// The JSON wire string for this value.\n");
out.push_str(" pub fn as_str(&self) -> &str {\n match self {\n");
for value in values {
out.push_str(&format!(
" Self::{} => {},\n",
rust_type_name(value),
json_string_literal(value)
));
}
out.push_str(" Self::Unrecognized(value) => value.as_str(),\n");
out.push_str(" }\n }\n\n");
out.push_str(
" /// Parse a wire string. An unrecognized value is preserved rather \
than rejected.\n",
);
out.push_str(" pub fn from_wire(value: &str) -> Self {\n match value {\n");
for value in values {
out.push_str(&format!(
" {} => Self::{},\n",
json_string_literal(value),
rust_type_name(value)
));
}
out.push_str(" other => Self::Unrecognized(other.to_string()),\n");
out.push_str(" }\n }\n\n");
out.push_str(
" /// Whether this value is part of the vocabulary this binding was \
generated from.\n",
);
out.push_str(" pub fn is_known(&self) -> bool {\n");
out.push_str(" !matches!(self, Self::Unrecognized(_))\n }\n");
out.push_str("}\n\n");
out.push_str(&format!(
"impl From<String> for {name} {{\n fn from(value: String) -> Self {{\n Self::from_wire(&value)\n }}\n}}\n\n"
));
out.push_str(&format!(
"impl From<{name}> for String {{\n fn from(value: {name}) -> Self {{\n value.as_str().to_string()\n }}\n}}\n\n"
));
out.push_str(&format!(
"impl std::fmt::Display for {name} {{\n fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{\n f.write_str(self.as_str())\n }}\n}}\n\n"
));
out
}
fn rust_string_enum(name: &str, doc: &str, values: &[String]) -> String {
let mut out = rust_doc_comment(doc);
out.push_str("#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]\n");
out.push_str(&format!("pub enum {name} {{\n"));
for value in values {
out.push_str(&format!(
" #[serde(rename = {})]\n {},\n",
json_string_literal(value),
rust_type_name(value)
));
}
out.push_str("}\n\n");
out.push_str(&format!("impl {name} {{\n"));
out.push_str(" pub const ALL: &'static [Self] = &[\n");
for value in values {
out.push_str(&format!(" Self::{},\n", rust_type_name(value)));
}
out.push_str(" ];\n\n");
out.push_str(" pub const fn as_str(self) -> &'static str {\n match self {\n");
for value in values {
out.push_str(&format!(
" Self::{} => {},\n",
rust_type_name(value),
json_string_literal(value)
));
}
out.push_str(" }\n }\n}\n\n");
out
}
fn rust_terminal_activity_status(values: &[String]) -> String {
let mut out = String::from(
"impl HarnExternalActionActivityStatus {\n\
\x20 /// Whether this snapshot is a final outcome and may only replay identically.\n\
\x20 pub const fn is_terminal(self) -> bool {\n\
\x20 matches!(self,\n",
);
for (index, value) in values.iter().enumerate() {
out.push_str(if index == 0 {
" "
} else {
" | "
});
out.push_str("Self::");
out.push_str(&rust_type_name(value));
out.push('\n');
}
out.push_str(" )\n }\n}\n\n");
out
}
fn rust_activity_progress(values: &[String]) -> String {
let mut out = String::from(
"impl HarnExternalActionActivityStatus {\n\
\x20 /// Whether a later snapshot may advance from this lifecycle status.\n\
\x20 pub const fn can_advance_to(self, next: Self) -> bool {\n\
\x20 if self.is_terminal() { return self as u8 == next as u8; }\n\
\x20 if next.is_terminal() { return true; }\n\
\x20 self.progress_rank() <= next.progress_rank()\n\
\x20 }\n\n\
\x20 const fn progress_rank(self) -> u8 {\n\
\x20 match self {\n",
);
for (index, value) in values.iter().enumerate() {
out.push_str(&format!(
" Self::{} => {index},\n",
rust_type_name(value)
));
}
out.push_str(" _ => u8::MAX,\n }\n }\n}\n\n");
out
}
fn rust_wire_types() -> String {
let mut out = String::from(
r#"/// Closed collaborative plan-comment lifecycle.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HarnPlanCommentState {
Open,
Addressed,
Resolved,
Reopened,
}
/// Closed executable-plan approval lifecycle.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum HarnPlanApprovalState {
Unrequested,
Requested,
Approved,
Rejected,
}
/// Typed mutation that produced an immutable plan revision.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum HarnPlanRevisionOperation {
Create { event_id: String },
Edit { event_id: String },
Comment { event_id: String, comment_id: String },
CommentState {
event_id: String,
comment_id: String,
state: HarnPlanCommentState,
},
}
/// Closed ACP permission-option vocabulary.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ACPPermissionOptionKind {
AllowOnce,
AllowAlways,
RejectOnce,
RejectAlways,
}
/// One option offered by `session/request_permission`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ACPPermissionOption {
pub option_id: String,
pub name: String,
pub kind: ACPPermissionOptionKind,
}
/// Canonical ACP tool-call fields carried by a permission request.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ACPPermissionToolCall {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_update: Option<String>,
pub tool_call_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub content: Option<Vec<Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub raw_input: Option<Value>,
#[serde(default, rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<Value>,
}
/// Params for the ACP `session/request_permission` client request.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ACPSessionRequestPermissionParams {
pub session_id: String,
pub tool_call: ACPPermissionToolCall,
pub options: Vec<ACPPermissionOption>,
}
/// User decision returned to an ACP permission request.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum ACPPermissionOutcome {
Selected {
#[serde(rename = "optionId")]
option_id: String,
},
Cancelled,
}
/// Result envelope for an ACP `session/request_permission` client request.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ACPSessionRequestPermissionResult {
pub outcome: ACPPermissionOutcome,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
/// Harn agent-event discriminator.
///
/// Unknown values remain lossless so older clients can ignore newly added
/// events while still proxying or recording the original notification.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum HarnAgentEventKind {
"#,
);
for value in HARN_AGENT_EVENT_KINDS {
out.push_str(" ");
out.push_str(&rust_type_name(value));
out.push_str(",\n");
}
out.push_str(" Other(String),\n}\n\n");
out.push_str("impl HarnAgentEventKind {\n");
out.push_str(" pub fn as_str(&self) -> &str {\n");
out.push_str(" match self {\n");
for value in HARN_AGENT_EVENT_KINDS {
out.push_str(" Self::");
out.push_str(&rust_type_name(value));
out.push_str(" => ");
out.push_str(&json_string_literal(value));
out.push_str(",\n");
}
out.push_str(" Self::Other(value) => value,\n");
out.push_str(" }\n");
out.push_str(" }\n");
out.push_str("}\n\n");
out.push_str(
r"impl Serialize for HarnAgentEventKind {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for HarnAgentEventKind {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Ok(match value.as_str() {
",
);
for value in HARN_AGENT_EVENT_KINDS {
out.push_str(" ");
out.push_str(&json_string_literal(value));
out.push_str(" => Self::");
out.push_str(&rust_type_name(value));
out.push_str(",\n");
}
out.push_str(" _ => Self::Other(value),\n");
out.push_str(" })\n");
out.push_str(" }\n");
out.push_str("}\n\n");
out.push_str(
r#"/// Params carried by a `_harn/agentEvent` notification.
///
/// `sessionId` and `kind` are stable. Kind-specific fields stay flattened so
/// the generated binding round-trips every current and future event payload.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HarnAgentEventParams {
pub session_id: String,
pub kind: HarnAgentEventKind,
#[serde(default, flatten)]
pub fields: Map<String, Value>,
}
/// JSON-RPC envelope for `_harn/agentEvent`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnAgentEventNotification {
pub jsonrpc: String,
pub method: String,
pub params: HarnAgentEventParams,
#[serde(default, flatten)]
pub fields: Map<String, Value>,
}
/// Cursor over the Harn-owned event topics projected into a session timeline.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnSessionTimelineCursor {
#[serde(default)]
pub topics: std::collections::BTreeMap<String, u64>,
}
/// Filters and bounds Harn's canonical semantic session timeline.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct HarnSessionTimelineQuery {
pub session_id: Option<String>,
pub run_id: Option<String>,
pub run_path: Option<String>,
pub project_id: Option<String>,
pub from_cursor: HarnSessionTimelineCursor,
pub limit: Option<usize>,
}
/// Stable source reference carried by one semantic timeline node.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HarnSessionTimelineReference {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub topic: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub event_id: Option<u64>,
}
/// Causal or identity link carried by one semantic timeline node.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HarnSessionTimelineLink {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trace_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub event_id: Option<String>,
}
/// Harn-owned semantic chronology row. `kind` remains open for forward compatibility.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HarnSessionTimelineNode {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_id: Option<String>,
#[serde(default)]
pub children: Vec<String>,
pub category: String,
pub kind: String,
pub name: String,
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trace_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub span_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub occurred_at_ms: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub start_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duration_ms: Option<u64>,
#[serde(default)]
pub attributes: Value,
#[serde(default)]
pub references: Vec<HarnSessionTimelineReference>,
#[serde(default)]
pub links: Vec<HarnSessionTimelineLink>,
pub order: u64,
}
/// States whether a bounded session-timeline snapshot is complete.
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "camelCase")]
pub struct HarnSessionTimelineCoverage {
pub returned: usize,
pub available: Option<usize>,
pub truncated: bool,
}
/// Point-in-time result of a canonical session-timeline query.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HarnSessionTimelineSnapshot {
pub schema_version: u32,
pub query: HarnSessionTimelineQuery,
pub cursor: HarnSessionTimelineCursor,
#[serde(default)]
pub coverage: HarnSessionTimelineCoverage,
pub nodes: Vec<HarnSessionTimelineNode>,
}
/// Incremental semantic timeline revision emitted to subscribers.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HarnSessionTimelineUpdate {
pub schema_version: u32,
pub cursor: HarnSessionTimelineCursor,
pub node: HarnSessionTimelineNode,
}
"#,
);
out
}
pub(super) fn rust_const_group(
const_prefix: &str,
slice_name: &str,
doc: &str,
values: &[&str],
) -> String {
rust_const_group_owned(const_prefix, slice_name, doc, &strs_to_strings(values))
}
pub(super) fn rust_const_group_owned(
const_prefix: &str,
slice_name: &str,
doc: &str,
values: &[String],
) -> String {
let mut out = String::new();
for value in values {
out.push_str(&format!(
"pub const {}: &str = {};\n",
rust_const_name(const_prefix, value),
json_string_literal(value)
));
}
out.push('\n');
out.push_str(&rust_doc_comment(doc));
out.push_str(&format!("pub const {slice_name}: &[&str] = &[\n"));
for value in values {
out.push_str(" ");
out.push_str(&json_string_literal(value));
out.push_str(",\n");
}
out.push_str("];\n\n");
out
}
pub(super) fn rust_const_name(prefix: &str, value: &str) -> String {
let mut suffix = String::with_capacity(value.len());
for ch in value.chars() {
if ch.is_ascii_alphanumeric() {
suffix.extend(ch.to_uppercase());
} else {
suffix.push('_');
}
}
let suffix = collapse_repeated_underscores(suffix.trim_matches('_'));
let name = if suffix.is_empty() {
prefix.to_string()
} else {
format!("{prefix}_{suffix}")
};
if name.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
format!("_{name}")
} else {
name
}
}
pub(super) fn rust_type_name(value: &str) -> String {
let mut out = String::new();
let mut capitalize = true;
for ch in value.chars() {
if ch.is_ascii_alphanumeric() {
if capitalize {
out.extend(ch.to_uppercase());
} else {
out.push(ch);
}
capitalize = false;
} else {
capitalize = true;
}
}
if out.chars().next().is_some_and(|ch| ch.is_ascii_digit()) {
out.insert(0, '_');
}
out
}
pub(super) fn rust_doc_comment(doc: &str) -> String {
let mut out = String::new();
let normalized = doc.split_whitespace().collect::<Vec<_>>().join(" ");
out.push_str("/// ");
out.push_str(&normalized);
out.push('\n');
out
}