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::constants::*;
use super::support::*;
use super::values::*;
#[cfg(test)]
pub(super) fn generate_rust() -> String {
generate_rust_for_version(env!("CARGO_PKG_VERSION"))
}
pub(super) fn generate_rust_for_version(artifact_version: &str) -> 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 Rust wire vocabulary.\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("/// Harn release that generated this binding.\n");
out.push_str(&format!(
"pub const HARN_PROTOCOL_ARTIFACT_VERSION: &str = {};\n\n",
json_string_literal(artifact_version)
));
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)
));
out.push_str(&rust_wire_types());
out.push_str(&rust_const_group(
"ACP_AGENT_METHOD",
"ACP_AGENT_METHODS",
"Stable host-facing ACP agent methods (matches the TypeScript/Swift/Python/Go bindings).",
ACP_AGENT_METHODS,
));
out.push_str(&rust_const_group(
"ACP_DISPATCHED_METHOD",
"ACP_DISPATCHED_METHODS",
"Every JSON-RPC method the ACP adapter actually dispatches, including the \
workspace-management, workflow-control, and HITL methods the stable \
bindings do not yet expose as typed enums. Reconciled against the \
`match` arms in `harn-serve`'s ACP adapter.",
ACP_DISPATCHED_METHODS,
));
out.push_str(&rust_const_group(
"ACP_TRANSPORT_CONTROL_METHOD",
"ACP_TRANSPORT_CONTROL_METHODS",
"ACP control frames consumed by the transport before regular adapter \
dispatch.",
ACP_TRANSPORT_CONTROL_METHODS,
));
let handled_methods =
concat_unique_wire_values(&[ACP_TRANSPORT_CONTROL_METHODS, ACP_DISPATCHED_METHODS]);
out.push_str(&rust_const_group_owned(
"ACP_HANDLED_METHOD",
"ACP_HANDLED_METHODS",
"Every inbound ACP method Harn handles, whether by transport preemption \
or regular adapter dispatch.",
&handled_methods,
));
out.push_str(&rust_const_group(
"ACP_CLIENT_METHOD",
"ACP_CLIENT_METHODS",
"ACP client methods the agent calls back into the host for.",
ACP_CLIENT_METHODS,
));
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.",
&agent_terminal_class_values(),
));
out.push_str(&rust_const_group_owned(
"AGENT_TERMINAL_KIND",
"AGENT_TERMINAL_KINDS",
"Producer-owned agent terminal outcome kinds.",
&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.",
&agent_terminal_owner_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,
));
out
}
fn rust_wire_types() -> String {
let mut out = String::from(
r#"/// Author identity on a collaborative plan revision or comment.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnPlanAuthor {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
}
/// Source identity for an immutable plan revision.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnPlanSource {
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uri: Option<String>,
}
/// 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,
}
/// One normalized executable plan step.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnPlanStep {
pub id: String,
pub content: String,
pub status: String,
pub priority: Option<Value>,
}
/// Typed approval state for an executable plan.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnPlanApproval {
pub state: HarnPlanApprovalState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reviewer: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub reviewers: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approved_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
/// Normalized executable plan embedded in a collaborative revision.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnPlanArtifact {
#[serde(rename = "_type")]
pub type_name: String,
pub schema_version: String,
pub id: String,
pub tool: String,
pub title: String,
pub summary: String,
pub steps: Vec<HarnPlanStep>,
pub assumptions: Vec<String>,
pub open_questions: Vec<String>,
pub verification_commands: Vec<String>,
pub approval: HarnPlanApproval,
}
/// 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,
},
}
/// Immutable collaborative plan revision.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnPlanRevision {
pub revision_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parent_revision_id: Option<String>,
pub markdown: String,
pub plan: HarnPlanArtifact,
pub author: HarnPlanAuthor,
pub source: HarnPlanSource,
pub created_at: String,
pub operation: HarnPlanRevisionOperation,
}
/// UTF-8 byte range fallback for a plan-comment anchor.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnPlanTextRange {
pub start: usize,
pub end: usize,
}
/// Stable-step and quoted-text/range anchor for a plan comment.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnPlanCommentAnchor {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub step_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub quoted_text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub range: Option<HarnPlanTextRange>,
}
/// Anchored collaborative plan comment.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnPlanComment {
pub comment_id: String,
pub anchor: HarnPlanCommentAnchor,
pub body: String,
pub state: HarnPlanCommentState,
pub author: HarnPlanAuthor,
pub created_at: String,
pub updated_at: String,
}
/// Receipt binding a comment resolution to input/output revisions and an agent event.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnPlanCommentResolutionReceipt {
pub receipt_id: String,
pub comment_id: String,
pub input_revision_id: String,
pub output_revision_id: String,
pub agent_run_id: String,
pub event_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub explanation: Option<String>,
pub created_at: String,
}
/// Harn's canonical collaborative plan-document contract.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HarnPlanDocument {
#[serde(rename = "_type")]
pub type_name: String,
pub schema_version: String,
pub document_id: String,
pub current_revision: HarnPlanRevision,
pub comments: Vec<HarnPlanComment>,
pub resolution_receipts: Vec<HarnPlanCommentResolutionReceipt>,
pub created_at: String,
pub updated_at: String,
}
/// 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>,
}
"#,
);
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
}