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::external_action_types::append_rust_external_action_types;
use super::support::*;
use super::values::*;
#[cfg(test)]
pub(super) fn generate_rust() -> String {
generate_rust_for_version(
env!("CARGO_PKG_VERSION"),
&ExternalActionVocabulary::load_for_tests(),
&ConnectorSetupVocabulary::load_for_tests(),
&ActivityVocabulary::load_for_tests(),
)
}
pub(super) fn generate_rust_for_version(
artifact_version: &str,
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("/// 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("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)
));
out.push_str(&rust_string_enum(
"HarnExternalActionOutcome",
"Closed external-action receipt outcomes owned by `std/external_action/vocabulary`.",
&external_actions.outcomes,
));
out.push_str(&rust_string_enum(
"HarnExternalActionReceiptStatus",
"Closed external-action receipt statuses owned by `std/external_action/vocabulary`.",
&external_actions.receipt_statuses,
));
out.push_str(&rust_string_enum(
"HarnExternalActionNextAction",
"Closed external-action follow-up actions owned by `std/external_action/vocabulary`.",
&external_actions.next_actions,
));
out.push_str(&rust_string_enum(
"HarnExternalActionEnvironment",
"Closed external-action environments owned by `std/external_action/vocabulary`.",
&external_actions.environments,
));
out.push_str(&rust_string_enum(
"HarnExternalActionAuthorizationMethod",
"Closed external-action authorization methods owned by `std/external_action/vocabulary`.",
&external_actions.authorization_methods,
));
out.push_str(&rust_string_enum(
"HarnExternalActionAuthenticationAssurance",
"Closed external-action authentication assurances owned by `std/external_action/vocabulary`.",
&external_actions.authentication_assurances,
));
out.push_str(&rust_string_enum(
"HarnExternalActionDisclosureSource",
"Closed external-action disclosure sources owned by `std/external_action/vocabulary`.",
&external_actions.disclosure_sources,
));
out.push_str(&rust_string_enum(
"HarnExternalActionErrorKind",
"Closed external-action error kinds owned by `std/external_action/vocabulary`.",
&external_actions.error_kinds,
));
out.push_str(&rust_string_enum(
"HarnExternalActionProtectedFieldClass",
"Closed protected-profile field classes owned by `std/external_action/vocabulary`.",
&external_actions.protected_field_classes,
));
out.push_str(&rust_string_enum(
"HarnExternalActionPassengerGender",
"Closed passenger gender markers owned by `std/external_action/vocabulary`.",
&external_actions.passenger_genders,
));
out.push_str(&rust_string_enum(
"HarnExternalActionActivityStatus",
"Closed external-action activity statuses owned by `std/external_action/vocabulary`.",
&external_actions.activity_statuses,
));
out.push_str(&rust_terminal_activity_status(
&external_actions.terminal_activity_statuses,
));
out.push_str(&rust_activity_progress(
&external_actions.progress_activity_statuses,
));
out.push_str(&rust_string_enum(
"HarnExternalActionPolicyLayer",
"Closed external-action policy layers owned by `std/external_action/vocabulary`.",
&external_actions.policy_layers,
));
out.push_str(&rust_string_enum(
"HarnExternalActionPolicyEvaluationOutcome",
"Closed policy evaluation outcomes owned by `std/external_action/vocabulary`.",
&external_actions.policy_evaluation_outcomes,
));
out.push_str(&rust_string_enum(
"HarnExternalActionDecisionOutcome",
"Closed external-action decision outcomes owned by `std/external_action/vocabulary`.",
&external_actions.decision_outcomes,
));
out.push_str(&rust_string_enum(
"HarnExternalActionDecider",
"Closed external-action decider kinds owned by `std/external_action/vocabulary`.",
&external_actions.deciders,
));
out.push_str(&rust_string_enum(
"HarnExternalActionReconciliationStatus",
"Closed external-action reconciliation statuses owned by `std/external_action/vocabulary`.",
&external_actions.reconciliation_statuses,
));
append_rust_external_action_types(&mut out);
out.push_str(&rust_string_enum(
"HarnActivityKind",
"Closed portable activity kinds owned by `std/activity/vocabulary`.",
&activity.kinds,
));
out.push_str(&rust_string_enum(
"HarnToolPermissionOutcome",
"Closed generic tool permission outcomes owned by `std/activity/vocabulary`.",
&activity.permission_outcomes,
));
out.push_str(&rust_string_enum(
"HarnToolPermissionDecider",
"Closed generic tool permission deciders owned by `std/activity/vocabulary`.",
&activity.permission_deciders,
));
out.push_str(&rust_string_enum(
"HarnToolPermissionPolicyLayer",
"Closed generic tool permission policy layers owned by `std/activity/vocabulary`.",
&activity.permission_policy_layers,
));
out.push_str(&rust_string_enum(
"HarnToolPermissionPolicyOutcome",
"Closed generic tool permission policy outcomes owned by `std/activity/vocabulary`.",
&activity.permission_policy_outcomes,
));
out.push_str(&rust_string_enum(
"HarnToolPermissionGrantScope",
"Closed generic tool permission grant scopes owned by `std/activity/vocabulary`.",
&activity.permission_grant_scopes,
));
out.push_str(&rust_string_enum(
"HarnToolPermissionGrantExpiry",
"Closed generic tool permission grant expiries owned by `std/activity/vocabulary`.",
&activity.permission_grant_expiries,
));
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(
"#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]\n\
pub struct HarnToolPermissionScope {\n\
\x20 pub tool_kind: HarnACPToolKind,\n\
\x20 pub side_effect: HarnSideEffectLevel,\n\
\x20 pub capabilities: Vec<String>,\n\
}\n\n\
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]\n\
pub struct HarnToolPermissionPolicyEvidence {\n\
\x20 pub layer: HarnToolPermissionPolicyLayer,\n\
\x20 pub outcome: HarnToolPermissionPolicyOutcome,\n\
\x20 #[serde(default, skip_serializing_if = \"Option::is_none\")]\n\
\x20 pub rule_id: Option<String>,\n\
\x20 pub risk_labels: Vec<String>,\n\
}\n\n\
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]\n\
pub struct HarnToolPermissionDecisionMetadata {\n\
\x20 pub schema: String,\n\
\x20 pub outcome: HarnToolPermissionOutcome,\n\
\x20 pub decider: HarnToolPermissionDecider,\n\
\x20 pub policy_evaluations: Vec<HarnToolPermissionPolicyEvidence>,\n\
\x20 #[serde(default, skip_serializing_if = \"Option::is_none\")]\n\
\x20 pub grant_scope: Option<HarnToolPermissionGrantScope>,\n\
}\n\n\
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]\n\
pub struct HarnToolPermissionGrantEvidence {\n\
\x20 pub scope: HarnToolPermissionGrantScope,\n\
\x20 pub expires: HarnToolPermissionGrantExpiry,\n\
\x20 pub reusable: bool,\n\
}\n\n\
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]\n\
pub struct HarnToolPermissionRequester {\n\
\x20 pub session_id: String,\n\
\x20 #[serde(default, skip_serializing_if = \"Option::is_none\")]\n\
\x20 pub agent_id: Option<String>,\n\
\x20 #[serde(default, skip_serializing_if = \"Option::is_none\")]\n\
\x20 pub model_provider: Option<String>,\n\
\x20 #[serde(default, skip_serializing_if = \"Option::is_none\")]\n\
\x20 pub model_id: Option<String>,\n\
}\n\n\
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]\n\
pub struct HarnToolPermissionActivityRecord {\n\
\x20 pub schema: String,\n\
\x20 pub kind: HarnActivityKind,\n\
\x20 pub id: String,\n\
\x20 pub request_id: String,\n\
\x20 pub tool_name: String,\n\
\x20 pub scope: HarnToolPermissionScope,\n\
\x20 pub outcome: HarnToolPermissionOutcome,\n\
\x20 pub decider: HarnToolPermissionDecider,\n\
\x20 pub policy_evaluations: Vec<HarnToolPermissionPolicyEvidence>,\n\
\x20 #[serde(default, skip_serializing_if = \"Option::is_none\")]\n\
\x20 pub grant: Option<HarnToolPermissionGrantEvidence>,\n\
\x20 pub requester: HarnToolPermissionRequester,\n\
\x20 pub occurred_at_ms: u64,\n\
}\n\n",
);
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,
));
out.push_str(
"#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n\
pub struct HarnConnectorSetupEvent {\n\
\x20 pub schema: String,\n\
\x20 pub sequence: u64,\n\
\x20 pub connector: String,\n\
\x20 pub stage: HarnConnectorSetupStage,\n\
\x20 pub status: HarnConnectorSetupStatus,\n\
\x20 pub interaction: HarnConnectorSetupInteraction,\n\
\x20 pub message: String,\n\
\x20 #[serde(default, skip_serializing_if = \"Option::is_none\")]\n\
\x20 pub error_code: Option<HarnConnectorSetupErrorCode>,\n\
\x20 #[serde(default, skip_serializing_if = \"Option::is_none\")]\n\
\x20 pub recovery: Option<String>,\n\
}\n\n",
);
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,
));
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_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#"/// 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>,
}
/// 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
}