use std::collections::HashMap;
use std::ffi::{CStr, CString};
use std::os::raw::c_char;
use std::ptr;
use std::sync::{Mutex, OnceLock};
use crate::crdt_sync::ReplicaState;
use crate::{component, crdt, frontmatter, syntax, template};
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_free_string(ptr: *mut c_char) {
if !ptr.is_null() {
drop(unsafe { CString::from_raw(ptr) });
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_free_state(ptr: *mut u8, len: usize) {
if !ptr.is_null() {
drop(unsafe { Vec::from_raw_parts(ptr, len, len) });
}
}
#[repr(C)]
pub struct FfiComponentList {
pub json: *mut c_char,
pub count: usize,
}
#[repr(C)]
pub struct FfiPatchResult {
pub text: *mut c_char,
pub error: *mut c_char,
}
pub fn ffi_patch_ok(text: String) -> FfiPatchResult {
FfiPatchResult {
text: CString::new(text).unwrap_or_default().into_raw(),
error: ptr::null_mut(),
}
}
pub fn ffi_patch_err(msg: &str) -> FfiPatchResult {
FfiPatchResult {
text: ptr::null_mut(),
error: CString::new(msg).unwrap_or_default().into_raw(),
}
}
pub fn ffi_patch_from_result(result: anyhow::Result<String>) -> FfiPatchResult {
match result {
Ok(text) => ffi_patch_ok(text),
Err(e) => ffi_patch_err(&format!("{e:#}")),
}
}
pub fn normalize_editor_visible_result(text: String) -> anyhow::Result<String> {
template::normalize_editor_visible_template_structure(&text)
}
fn utf8_offsets_to_utf16_offsets(doc: &str, offsets: &[usize]) -> HashMap<usize, usize> {
let mut targets = offsets.to_vec();
targets.sort_unstable();
targets.dedup();
let mut mapped = HashMap::with_capacity(targets.len());
let mut target_idx = 0usize;
let mut utf8_offset = 0usize;
let mut utf16_offset = 0usize;
while target_idx < targets.len() && targets[target_idx] == 0 {
mapped.insert(0, 0);
target_idx += 1;
}
for ch in doc.chars() {
let next_utf8 = utf8_offset + ch.len_utf8();
while target_idx < targets.len() && targets[target_idx] < next_utf8 {
mapped.insert(targets[target_idx], utf16_offset);
target_idx += 1;
}
utf8_offset = next_utf8;
utf16_offset += ch.len_utf16();
while target_idx < targets.len() && targets[target_idx] == utf8_offset {
mapped.insert(targets[target_idx], utf16_offset);
target_idx += 1;
}
}
while target_idx < targets.len() {
mapped.insert(targets[target_idx], utf16_offset);
target_idx += 1;
}
mapped
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_parse_components(doc: *const c_char) -> FfiComponentList {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(_) => {
return FfiComponentList {
json: ptr::null_mut(),
count: 0,
};
}
};
let components = match component::parse(doc_str) {
Ok(c) => c,
Err(_) => {
return FfiComponentList {
json: ptr::null_mut(),
count: 0,
};
}
};
let count = components.len();
let json_items: Vec<serde_json::Value> = components
.iter()
.map(|c| {
serde_json::json!({
"name": c.name,
"attrs": c.attrs,
"open_start": c.open_start,
"open_end": c.open_end,
"close_start": c.close_start,
"close_end": c.close_end,
"content": c.content(doc_str),
})
})
.collect();
let json_str = serde_json::to_string(&json_items).unwrap_or_default();
let c_json = CString::new(json_str).unwrap_or_default();
FfiComponentList {
json: c_json.into_raw(),
count,
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_visual_tokens_json(doc: *const c_char) -> *mut c_char {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(_) => return ptr::null_mut(),
};
let tokens = syntax::collect_visual_tokens(doc_str);
let offsets: Vec<usize> = tokens
.iter()
.flat_map(|token| [token.start, token.end])
.collect();
let utf16_offsets = utf8_offsets_to_utf16_offsets(doc_str, &offsets);
let editor_tokens: Vec<_> = tokens
.iter()
.map(|token| {
serde_json::json!({
"kind": token.kind,
"start": utf16_offsets[&token.start],
"end": utf16_offsets[&token.end],
})
})
.collect();
let json = match serde_json::to_string(&editor_tokens) {
Ok(json) => json,
Err(_) => return ptr::null_mut(),
};
match CString::new(json) {
Ok(value) => value.into_raw(),
Err(_) => ptr::null_mut(),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_merge_frontmatter(
doc: *const c_char,
yaml_fields: *const c_char,
) -> FfiPatchResult {
let make_err = |msg: &str| FfiPatchResult {
text: ptr::null_mut(),
error: CString::new(msg).unwrap_or_default().into_raw(),
};
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return make_err(&format!("invalid doc UTF-8: {e}")),
};
let fields_str = match unsafe { CStr::from_ptr(yaml_fields) }.to_str() {
Ok(s) => s,
Err(e) => return make_err(&format!("invalid yaml_fields UTF-8: {e}")),
};
match frontmatter::merge_fields(doc_str, fields_str) {
Ok(result) => FfiPatchResult {
text: CString::new(result).unwrap_or_default().into_raw(),
error: ptr::null_mut(),
},
Err(e) => make_err(&format!("{e}")),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_converge_queue_auto(
doc: *const c_char,
want_auto: std::os::raw::c_int,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let converged = component::converge_queue_auto(doc_str, want_auto != 0)
.unwrap_or_else(|| doc_str.to_string());
FfiPatchResult {
text: CString::new(converged).unwrap_or_default().into_raw(),
error: ptr::null_mut(),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_normalize_template_structure(
doc: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
ffi_patch_from_result(template::normalize_editor_visible_template_structure(
doc_str,
))
}
#[repr(C)]
pub struct FfiMergeResult {
pub text: *mut c_char,
pub state: *mut u8,
pub state_len: usize,
pub error: *mut c_char,
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_crdt_merge(
base_state: *const u8,
base_state_len: usize,
ours: *const c_char,
theirs: *const c_char,
) -> FfiMergeResult {
let make_err = |msg: &str| FfiMergeResult {
text: ptr::null_mut(),
state: ptr::null_mut(),
state_len: 0,
error: CString::new(msg).unwrap_or_default().into_raw(),
};
let ours_str = match unsafe { CStr::from_ptr(ours) }.to_str() {
Ok(s) => s,
Err(e) => return make_err(&format!("invalid ours UTF-8: {e}")),
};
let theirs_str = match unsafe { CStr::from_ptr(theirs) }.to_str() {
Ok(s) => s,
Err(e) => return make_err(&format!("invalid theirs UTF-8: {e}")),
};
let base = if base_state.is_null() {
None
} else {
Some(unsafe { std::slice::from_raw_parts(base_state, base_state_len) })
};
match crdt::merge_by_component(base, ours_str, theirs_str) {
Ok(merged_text) => {
let doc = crdt::CrdtDoc::from_text(&merged_text);
let state_bytes = doc.encode_state();
let state_len = state_bytes.len();
let state_ptr = {
let mut boxed = state_bytes.into_boxed_slice();
let ptr = boxed.as_mut_ptr();
std::mem::forget(boxed);
ptr
};
FfiMergeResult {
text: CString::new(merged_text).unwrap_or_default().into_raw(),
state: state_ptr,
state_len,
error: ptr::null_mut(),
}
}
Err(e) => make_err(&format!("{e}")),
}
}
static REPLICA_REGISTRY: OnceLock<Mutex<HashMap<u64, ReplicaState>>> = OnceLock::new();
fn replica_registry() -> &'static Mutex<HashMap<u64, ReplicaState>> {
REPLICA_REGISTRY.get_or_init(|| Mutex::new(HashMap::new()))
}
fn leak_state(bytes: Vec<u8>, out_len: *mut usize) -> *mut u8 {
let len = bytes.len();
let mut boxed = bytes.into_boxed_slice();
let ptr = boxed.as_mut_ptr();
std::mem::forget(boxed);
if !out_len.is_null() {
unsafe { *out_len = len };
}
ptr
}
fn null_state(out_len: *mut usize) -> *mut u8 {
if !out_len.is_null() {
unsafe { *out_len = 0 };
}
ptr::null_mut()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_replica_open(
replica_id: u64,
init_state: *const u8,
init_len: usize,
) -> i32 {
let replica = if init_state.is_null() || init_len == 0 {
ReplicaState::new(replica_id)
} else {
let bytes = unsafe { std::slice::from_raw_parts(init_state, init_len) };
match ReplicaState::from_encoded(replica_id, bytes) {
Ok(r) => r,
Err(_) => return -2,
}
};
match replica_registry().lock() {
Ok(mut reg) => {
reg.insert(replica_id, replica);
0
}
Err(_) => -1,
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_replica_apply_local(
replica_id: u64,
offset: u32,
delete_len: u32,
insert: *const c_char,
) -> i32 {
let insert_str = if insert.is_null() {
""
} else {
match unsafe { CStr::from_ptr(insert) }.to_str() {
Ok(s) => s,
Err(_) => return -2,
}
};
match replica_registry().lock() {
Ok(reg) => match reg.get(&replica_id) {
Some(replica) => {
replica.apply_local_edit(offset, delete_len, insert_str);
0
}
None => -3,
},
Err(_) => -1,
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_replica_text(replica_id: u64) -> *mut c_char {
match replica_registry().lock() {
Ok(reg) => match reg.get(&replica_id) {
Some(replica) => CString::new(replica.text()).unwrap_or_default().into_raw(),
None => ptr::null_mut(),
},
Err(_) => ptr::null_mut(),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_replica_state_vector(
replica_id: u64,
out_len: *mut usize,
) -> *mut u8 {
match replica_registry().lock() {
Ok(reg) => match reg.get(&replica_id) {
Some(replica) => leak_state(replica.state_vector(), out_len),
None => null_state(out_len),
},
Err(_) => null_state(out_len),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_replica_diff(
replica_id: u64,
their_sv: *const u8,
their_sv_len: usize,
out_len: *mut usize,
) -> *mut u8 {
if their_sv.is_null() {
return null_state(out_len);
}
let sv = unsafe { std::slice::from_raw_parts(their_sv, their_sv_len) };
match replica_registry().lock() {
Ok(reg) => match reg.get(&replica_id) {
Some(replica) => match replica.diff(sv) {
Ok(update) => leak_state(update, out_len),
Err(_) => null_state(out_len),
},
None => null_state(out_len),
},
Err(_) => null_state(out_len),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_replica_apply_update(
replica_id: u64,
update: *const u8,
update_len: usize,
) -> i32 {
if update.is_null() {
return -2;
}
let bytes = unsafe { std::slice::from_raw_parts(update, update_len) };
match replica_registry().lock() {
Ok(reg) => match reg.get(&replica_id) {
Some(replica) => match replica.apply_update(bytes) {
Ok(()) => 0,
Err(_) => -2,
},
None => -3,
},
Err(_) => -1,
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_replica_encode_state(
replica_id: u64,
out_len: *mut usize,
) -> *mut u8 {
match replica_registry().lock() {
Ok(reg) => match reg.get(&replica_id) {
Some(replica) => leak_state(replica.encode_state(), out_len),
None => null_state(out_len),
},
Err(_) => null_state(out_len),
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_replica_close(replica_id: u64) -> i32 {
match replica_registry().lock() {
Ok(mut reg) => {
if reg.remove(&replica_id).is_some() {
0
} else {
-3
}
}
Err(_) => -1,
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_replica_persist(replica_id: u64, path: *const c_char) -> i32 {
if path.is_null() {
return -2;
}
let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
Ok(s) => s,
Err(_) => return -2,
};
let state = match replica_registry().lock() {
Ok(reg) => match reg.get(&replica_id) {
Some(replica) => replica.encode_state(),
None => return -3,
},
Err(_) => return -1,
};
match atomic_write_bytes(std::path::Path::new(path_str), &state) {
Ok(()) => 0,
Err(_) => -2,
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_replica_recover(replica_id: u64, path: *const c_char) -> i32 {
if path.is_null() {
return -2;
}
let path_str = match unsafe { CStr::from_ptr(path) }.to_str() {
Ok(s) => s,
Err(_) => return -2,
};
let bytes = match std::fs::read(path_str) {
Ok(b) => b,
Err(_) => return -2,
};
let replica = match ReplicaState::from_encoded(replica_id, &bytes) {
Ok(r) => r,
Err(_) => return -2,
};
match replica_registry().lock() {
Ok(mut reg) => {
reg.insert(replica_id, replica);
0
}
Err(_) => -1,
}
}
fn atomic_write_bytes(path: &std::path::Path, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write;
if let Some(parent) = path.parent()
&& !parent.as_os_str().is_empty()
{
std::fs::create_dir_all(parent)?;
}
let pid = std::process::id();
let tmp_path = match path.extension().and_then(|e| e.to_str()) {
Some(ext) => path.with_extension(format!("{ext}.tmp.{pid}")),
None => path.with_extension(format!("tmp.{pid}")),
};
{
let mut f = std::fs::File::create(&tmp_path)?;
f.write_all(bytes)?;
f.flush()?;
}
std::fs::rename(&tmp_path, path)?;
Ok(())
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_merge_crdt(
base: *const c_char,
ours: *const c_char,
theirs: *const c_char,
) -> *mut c_char {
let base_str = match unsafe { CStr::from_ptr(base) }.to_str() {
Ok(s) => s,
Err(_) => return CString::new("").unwrap_or_default().into_raw(),
};
let ours_str = match unsafe { CStr::from_ptr(ours) }.to_str() {
Ok(s) => s,
Err(_) => return CString::new("").unwrap_or_default().into_raw(),
};
let theirs_str = match unsafe { CStr::from_ptr(theirs) }.to_str() {
Ok(s) => s,
Err(_) => return CString::new("").unwrap_or_default().into_raw(),
};
let base_doc = crdt::CrdtDoc::from_text(base_str);
let base_state = base_doc.encode_state();
let merged = crdt::merge_by_component(Some(&base_state), ours_str, theirs_str)
.unwrap_or_else(|_| ours_str.to_string());
CString::new(merged).unwrap_or_default().into_raw()
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_reposition_boundary_to_end(
doc: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let result = template::reposition_boundary_to_end_clean(doc_str);
ffi_patch_from_result(normalize_editor_visible_result(result))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_reposition_boundary_to_end_with_id(
doc: *const c_char,
boundary_id: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let boundary_id_str = match unsafe { CStr::from_ptr(boundary_id) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid boundary_id UTF-8: {e}")),
};
let result = template::reposition_boundary_to_end_clean_with_id(doc_str, Some(boundary_id_str));
ffi_patch_from_result(normalize_editor_visible_result(result))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_reposition_boundary_to_end_preserve_head(
doc: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let result = template::reposition_boundary_to_end_preserve_head(doc_str);
ffi_patch_from_result(normalize_editor_visible_result(result))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_reposition_boundary_to_end_preserve_head_with_id(
doc: *const c_char,
boundary_id: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let boundary_id_str = match unsafe { CStr::from_ptr(boundary_id) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid boundary_id UTF-8: {e}")),
};
let result =
template::reposition_boundary_to_end_preserve_head_with_id(doc_str, Some(boundary_id_str));
ffi_patch_from_result(normalize_editor_visible_result(result))
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_apply_patch(
doc: *const c_char,
component_name: *const c_char,
content: *const c_char,
mode: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let name = match unsafe { CStr::from_ptr(component_name) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid component name UTF-8: {e}")),
};
let patch_content = match unsafe { CStr::from_ptr(content) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid content UTF-8: {e}")),
};
let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid mode UTF-8: {e}")),
};
let patch = template::PatchBlock::new(name, patch_content);
let mut overrides = HashMap::new();
overrides.insert(name.to_string(), mode_str.to_string());
ffi_patch_from_result(
template::apply_patches_with_overrides_pure(
doc_str,
&[patch],
"",
None,
&HashMap::new(),
&HashMap::new(),
&overrides,
)
.and_then(normalize_editor_visible_result),
)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_apply_patch_with_caret(
doc: *const c_char,
component_name: *const c_char,
content: *const c_char,
mode: *const c_char,
caret_offset: i32,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let name = match unsafe { CStr::from_ptr(component_name) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid component name UTF-8: {e}")),
};
let patch_content = match unsafe { CStr::from_ptr(content) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid content UTF-8: {e}")),
};
let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid mode UTF-8: {e}")),
};
if mode_str == "append" && caret_offset >= 0 {
let components = match component::parse(doc_str) {
Ok(c) => c,
Err(e) => return ffi_patch_err(&format!("{e}")),
};
if let Some(comp) = components.iter().find(|c| c.name == name) {
let result =
comp.append_with_caret(doc_str, patch_content, Some(caret_offset as usize));
return ffi_patch_from_result(normalize_editor_visible_result(result));
}
}
let patch = template::PatchBlock::new(name, patch_content);
let mut overrides = HashMap::new();
overrides.insert(name.to_string(), mode_str.to_string());
ffi_patch_from_result(
template::apply_patches_with_overrides_pure(
doc_str,
&[patch],
"",
None,
&HashMap::new(),
&HashMap::new(),
&overrides,
)
.and_then(normalize_editor_visible_result),
)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn agent_doc_apply_patch_with_boundary(
doc: *const c_char,
component_name: *const c_char,
content: *const c_char,
mode: *const c_char,
boundary_id: *const c_char,
) -> FfiPatchResult {
let doc_str = match unsafe { CStr::from_ptr(doc) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid doc UTF-8: {e}")),
};
let name = match unsafe { CStr::from_ptr(component_name) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid component name UTF-8: {e}")),
};
let patch_content = match unsafe { CStr::from_ptr(content) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid content UTF-8: {e}")),
};
let mode_str = match unsafe { CStr::from_ptr(mode) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid mode UTF-8: {e}")),
};
let bid = match unsafe { CStr::from_ptr(boundary_id) }.to_str() {
Ok(s) => s,
Err(e) => return ffi_patch_err(&format!("invalid boundary_id UTF-8: {e}")),
};
if mode_str == "append" && !bid.is_empty() {
let components = match component::parse(doc_str) {
Ok(c) => c,
Err(e) => return ffi_patch_err(&format!("{e}")),
};
if let Some(comp) = components.iter().find(|c| c.name == name) {
let result = comp.append_with_boundary(doc_str, patch_content, bid);
let result = if name == "exchange" {
template::annotate_exchange_headings_against_baseline(&result, doc_str)
} else {
result
};
return ffi_patch_from_result(normalize_editor_visible_result(result));
}
}
let patch = template::PatchBlock::new(name, patch_content);
let mut overrides = HashMap::new();
overrides.insert(name.to_string(), mode_str.to_string());
ffi_patch_from_result(
template::apply_patches_with_overrides_pure(
doc_str,
&[patch],
"",
None,
&HashMap::new(),
&HashMap::new(),
&overrides,
)
.and_then(normalize_editor_visible_result),
)
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::{CStr, CString};
#[test]
fn apply_patch_append_preserves_leading_code_fence_after_prompt_fence() {
let doc = concat!(
"<!-- agent:exchange patch=append -->\n",
"❯ show fenced prompt\n",
"```\n",
"prompt body\n",
"```\n",
"<!-- /agent:exchange -->\n",
);
let c_doc = CString::new(doc).unwrap();
let c_name = CString::new("exchange").unwrap();
let c_content = CString::new("```\nresponse body\n```\n").unwrap();
let c_mode = CString::new("append").unwrap();
let result = unsafe {
agent_doc_apply_patch(
c_doc.as_ptr(),
c_name.as_ptr(),
c_content.as_ptr(),
c_mode.as_ptr(),
)
};
assert!(result.error.is_null());
assert!(!result.text.is_null());
let text = unsafe { CStr::from_ptr(result.text) }.to_str().unwrap();
assert_eq!(
text.matches("```").count(),
4,
"native IPC append must keep prompt and response fences:\n{text}"
);
assert!(
text.contains("```\n```\nresponse body\n```"),
"native IPC append stripped the response opening fence:\n{text}"
);
unsafe { agent_doc_free_string(result.text) };
}
#[test]
fn replica_ffi_round_trip_converges_via_state_vector_exchange() {
use std::ffi::CString;
let a: u64 = 0xA11CE;
let b: u64 = 0xB0B;
assert_eq!(unsafe { agent_doc_replica_open(a, std::ptr::null(), 0) }, 0);
assert_eq!(unsafe { agent_doc_replica_open(b, std::ptr::null(), 0) }, 0);
let ins_a = CString::new("alpha").unwrap();
let ins_b = CString::new("beta").unwrap();
assert_eq!(
unsafe { agent_doc_replica_apply_local(a, 0, 0, ins_a.as_ptr()) },
0
);
assert_eq!(
unsafe { agent_doc_replica_apply_local(b, 0, 0, ins_b.as_ptr()) },
0
);
let mut b_sv_len: usize = 0;
let b_sv = unsafe { agent_doc_replica_state_vector(b, &mut b_sv_len) };
assert!(!b_sv.is_null());
let mut a_to_b_len: usize = 0;
let a_to_b = unsafe { agent_doc_replica_diff(a, b_sv, b_sv_len, &mut a_to_b_len) };
assert!(!a_to_b.is_null());
let mut a_sv_len: usize = 0;
let a_sv = unsafe { agent_doc_replica_state_vector(a, &mut a_sv_len) };
let mut b_to_a_len: usize = 0;
let b_to_a = unsafe { agent_doc_replica_diff(b, a_sv, a_sv_len, &mut b_to_a_len) };
assert_eq!(
unsafe { agent_doc_replica_apply_update(b, a_to_b, a_to_b_len) },
0
);
assert_eq!(
unsafe { agent_doc_replica_apply_update(a, b_to_a, b_to_a_len) },
0
);
let ta = unsafe { agent_doc_replica_text(a) };
let tb = unsafe { agent_doc_replica_text(b) };
let sa = unsafe { CStr::from_ptr(ta) }.to_str().unwrap().to_string();
let sb = unsafe { CStr::from_ptr(tb) }.to_str().unwrap().to_string();
assert_eq!(sa, sb, "replicas converge after FFI state-vector exchange");
assert!(sa.contains("alpha") && sa.contains("beta"));
assert_eq!(
unsafe { agent_doc_replica_apply_update(b, a_to_b, a_to_b_len) },
0
);
let tb2 = unsafe { agent_doc_replica_text(b) };
let sb2 = unsafe { CStr::from_ptr(tb2) }.to_str().unwrap().to_string();
assert_eq!(sb2, sb, "re-applying a known update via FFI is idempotent");
unsafe {
agent_doc_free_state(b_sv, b_sv_len);
agent_doc_free_state(a_sv, a_sv_len);
agent_doc_free_state(a_to_b, a_to_b_len);
agent_doc_free_state(b_to_a, b_to_a_len);
agent_doc_free_string(ta);
agent_doc_free_string(tb);
agent_doc_free_string(tb2);
}
assert_eq!(unsafe { agent_doc_replica_close(a) }, 0);
assert_eq!(unsafe { agent_doc_replica_close(b) }, 0);
assert_eq!(unsafe { agent_doc_replica_close(a) }, -3);
}
#[test]
fn replica_ffi_reports_unopened_replica() {
let id: u64 = 0xDEAD_BEEF;
let ins = std::ffi::CString::new("x").unwrap();
assert_eq!(
unsafe { agent_doc_replica_apply_local(id, 0, 0, ins.as_ptr()) },
-3,
"apply_local on an unopened replica reports not-open"
);
assert_eq!(
unsafe { agent_doc_replica_apply_update(id, std::ptr::null(), 0) },
-2
);
assert!(unsafe { agent_doc_replica_text(id) }.is_null());
let mut len: usize = 123;
assert!(unsafe { agent_doc_replica_state_vector(id, &mut len) }.is_null());
assert_eq!(len, 0, "null byte result sets out_len to 0");
assert_eq!(unsafe { agent_doc_replica_close(id) }, -3);
}
#[test]
fn replica_ffi_persist_then_recover_round_trips_disk_projection() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("node.yrs");
let path_c = std::ffi::CString::new(path.to_str().unwrap()).unwrap();
let src: u64 = 0x5151;
assert_eq!(
unsafe { agent_doc_replica_open(src, std::ptr::null(), 0) },
0
);
let ins = std::ffi::CString::new("crash-safe").unwrap();
assert_eq!(
unsafe { agent_doc_replica_apply_local(src, 0, 0, ins.as_ptr()) },
0
);
assert_eq!(
unsafe { agent_doc_replica_persist(src, path_c.as_ptr()) },
0,
"persist writes the recovery projection"
);
assert!(path.exists());
assert_eq!(unsafe { agent_doc_replica_close(src) }, 0);
let recovered: u64 = 0x5252;
assert_eq!(
unsafe { agent_doc_replica_recover(recovered, path_c.as_ptr()) },
0,
"recover rebuilds the in-memory replica from disk"
);
let t = unsafe { agent_doc_replica_text(recovered) };
assert!(!t.is_null());
let text = unsafe { CStr::from_ptr(t) }.to_str().unwrap().to_string();
unsafe { agent_doc_free_string(t) };
assert_eq!(text, "crash-safe");
assert_eq!(unsafe { agent_doc_replica_close(recovered) }, 0);
}
#[test]
fn replica_ffi_persist_recover_reject_bad_args() {
let id: u64 = 0x6363;
let p = std::ffi::CString::new("/tmp/agent-doc-nope.yrs").unwrap();
assert_eq!(unsafe { agent_doc_replica_persist(id, p.as_ptr()) }, -3);
assert_eq!(
unsafe { agent_doc_replica_persist(id, std::ptr::null()) },
-2
);
let missing = std::ffi::CString::new("/nonexistent/dir/agent-doc-missing.yrs").unwrap();
assert_eq!(
unsafe { agent_doc_replica_recover(id, missing.as_ptr()) },
-2
);
}
}