1use futures_util::StreamExt;
4use serde::Serialize;
5use serde_json::Value as JsonValue;
6use simple_agent_type::coercion::{CoercionFlag, CoercionResult};
7use simple_agent_type::message::{Message, Role};
8use simple_agent_type::prelude::{ApiKey, CompletionRequest, Provider, Result, SimpleAgentsError};
9use simple_agent_type::response::{CompletionResponse, FinishReason, Usage};
10use simple_agent_type::tool::{ToolCall, ToolType};
11use simple_agents_core::{
12 CompletionMode, CompletionOptions, CompletionOutcome, HealedJsonResponse, HealedSchemaResponse,
13 SimpleAgentsClient, SimpleAgentsClientBuilder,
14};
15use simple_agents_healing::schema::{Field as SchemaField, ObjectSchema, Schema, StreamAnnotation};
16use simple_agents_providers::anthropic::AnthropicProvider;
17use simple_agents_providers::openai::OpenAIProvider;
18use simple_agents_providers::openrouter::OpenRouterProvider;
19use simple_agents_workflow::{
20 run_email_workflow_yaml_file_with_client,
21 run_workflow_yaml_file_with_client_and_custom_worker_and_events_and_options, YamlWorkflowEvent,
22 YamlWorkflowEventSink, YamlWorkflowRunOptions,
23};
24use std::cell::RefCell;
25use std::ffi::{CStr, CString};
26use std::os::raw::{c_char, c_void};
27use std::panic::{catch_unwind, AssertUnwindSafe};
28use std::sync::{Arc, Mutex};
29
30type Runtime = tokio::runtime::Runtime;
32
33struct FfiClient {
34 runtime: Mutex<Runtime>,
35 client: SimpleAgentsClient,
36}
37
38#[repr(C)]
39pub struct SAClient {
40 inner: FfiClient,
41}
42
43#[repr(C)]
44pub struct SAMessage {
45 pub role: *const c_char,
46 pub content: *const c_char,
47 pub name: *const c_char,
48 pub tool_call_id: *const c_char,
49}
50
51#[derive(Serialize)]
52struct FfiToolCallFunction {
53 name: String,
54 arguments: String,
55}
56
57#[derive(Serialize)]
58struct FfiToolCall {
59 id: String,
60 tool_type: String,
61 function: FfiToolCallFunction,
62}
63
64#[derive(Serialize)]
65struct FfiUsage {
66 prompt_tokens: u32,
67 completion_tokens: u32,
68 total_tokens: u32,
69}
70
71#[derive(Serialize)]
72struct FfiHealingData {
73 value: JsonValue,
74 flags: Vec<CoercionFlag>,
75 confidence: f32,
76}
77
78#[derive(Serialize)]
79struct FfiCompletionResult {
80 id: String,
81 model: String,
82 role: String,
83 content: Option<String>,
84 tool_calls: Option<Vec<FfiToolCall>>,
85 finish_reason: Option<String>,
86 usage: FfiUsage,
87 raw: Option<String>,
88 healed: Option<FfiHealingData>,
89 coerced: Option<FfiHealingData>,
90}
91
92type SAStreamCallback =
93 Option<extern "C" fn(event_json: *const c_char, user_data: *mut c_void) -> i32>;
94
95type SAWorkflowEventCallback =
96 Option<extern "C" fn(event_json: *const c_char, user_data: *mut c_void) -> i32>;
97
98struct RecordingWorkflowEventSink {
99 events: Mutex<Vec<YamlWorkflowEvent>>,
100}
101
102impl RecordingWorkflowEventSink {
103 fn new() -> Self {
104 Self {
105 events: Mutex::new(Vec::new()),
106 }
107 }
108
109 fn attach_to_output(&self, output: &mut JsonValue) -> Result<()> {
110 let events = self
111 .events
112 .lock()
113 .map_err(|_| {
114 SimpleAgentsError::Config("workflow event sink lock poisoned".to_string())
115 })?
116 .clone();
117 let events_value = serde_json::to_value(events)
118 .map_err(|e| SimpleAgentsError::Config(format!("serialize workflow events: {e}")))?;
119 if let JsonValue::Object(object) = output {
120 object.insert("events".to_string(), events_value);
121 }
122 Ok(())
123 }
124}
125
126impl YamlWorkflowEventSink for RecordingWorkflowEventSink {
127 fn emit(&self, event: &YamlWorkflowEvent) {
128 if let Ok(mut events) = self.events.lock() {
129 events.push(event.clone());
130 }
131 }
132}
133
134struct CallbackWorkflowEventSink {
135 callback: extern "C" fn(event_json: *const c_char, user_data: *mut c_void) -> i32,
136 user_data: *mut c_void,
137 callback_failed: Mutex<bool>,
138}
139
140impl CallbackWorkflowEventSink {
141 fn new(
142 callback: extern "C" fn(event_json: *const c_char, user_data: *mut c_void) -> i32,
143 user_data: *mut c_void,
144 ) -> Self {
145 Self {
146 callback,
147 user_data,
148 callback_failed: Mutex::new(false),
149 }
150 }
151
152 fn callback_failed(&self) -> bool {
153 self.callback_failed
154 .lock()
155 .map(|flag| *flag)
156 .unwrap_or(true)
157 }
158}
159
160unsafe impl Send for CallbackWorkflowEventSink {}
166unsafe impl Sync for CallbackWorkflowEventSink {}
168
169impl YamlWorkflowEventSink for CallbackWorkflowEventSink {
170 fn emit(&self, event: &YamlWorkflowEvent) {
171 let payload = match serde_json::to_string(event) {
172 Ok(value) => value,
173 Err(_) => {
174 if let Ok(mut failed) = self.callback_failed.lock() {
175 *failed = true;
176 }
177 return;
178 }
179 };
180 let payload = match CString::new(payload) {
181 Ok(value) => value,
182 Err(_) => {
183 if let Ok(mut failed) = self.callback_failed.lock() {
184 *failed = true;
185 }
186 return;
187 }
188 };
189 let status = (self.callback)(payload.as_ptr(), self.user_data);
190 if status != 0 {
191 if let Ok(mut failed) = self.callback_failed.lock() {
192 *failed = true;
193 }
194 }
195 }
196
197 fn is_cancelled(&self) -> bool {
198 self.callback_failed()
199 }
200}
201
202#[derive(Serialize)]
203#[serde(tag = "type", rename_all = "snake_case")]
204enum FfiStreamEvent {
205 Chunk {
206 chunk: simple_agent_type::response::CompletionChunk,
207 },
208 Error {
209 message: String,
210 },
211 Done,
212}
213
214thread_local! {
215 static LAST_ERROR: RefCell<Option<String>> = const { RefCell::new(None) };
216}
217
218fn set_last_error(message: impl Into<String>) {
219 LAST_ERROR.with(|slot| {
220 *slot.borrow_mut() = Some(message.into());
221 });
222}
223
224fn clear_last_error() {
225 LAST_ERROR.with(|slot| {
226 *slot.borrow_mut() = None;
227 });
228}
229
230fn take_last_error() -> Option<String> {
231 LAST_ERROR.with(|slot| slot.borrow_mut().take())
232}
233
234fn build_runtime() -> Result<Runtime> {
235 Runtime::new().map_err(|e| SimpleAgentsError::Config(format!("Failed to build runtime: {e}")))
236}
237
238fn provider_from_env(provider_name: &str) -> Result<Arc<dyn Provider>> {
239 match provider_name {
240 "openai" => Ok(Arc::new(OpenAIProvider::from_env()?)),
241 "anthropic" => Ok(Arc::new(AnthropicProvider::from_env()?)),
242 "openrouter" => Ok(Arc::new(openrouter_from_env()?)),
243 _ => Err(SimpleAgentsError::Config(format!(
244 "Unknown provider '{provider_name}'"
245 ))),
246 }
247}
248
249fn openrouter_from_env() -> Result<OpenRouterProvider> {
250 let api_key = std::env::var("OPENROUTER_API_KEY").map_err(|_| {
251 SimpleAgentsError::Config("OPENROUTER_API_KEY environment variable is required".to_string())
252 })?;
253 let api_key = ApiKey::new(api_key)?;
254 let base_url = std::env::var("OPENROUTER_API_BASE")
255 .unwrap_or_else(|_| OpenRouterProvider::DEFAULT_BASE_URL.to_string());
256 OpenRouterProvider::with_base_url(api_key, base_url)
257}
258
259unsafe fn cstr_to_string(ptr: *const c_char, field: &str) -> Result<String> {
260 if ptr.is_null() {
261 return Err(SimpleAgentsError::Config(format!("{field} cannot be null")));
262 }
263
264 let c_str = CStr::from_ptr(ptr);
265 let value = c_str
266 .to_str()
267 .map_err(|_| SimpleAgentsError::Config(format!("{field} must be valid UTF-8")))?;
268 if value.is_empty() {
269 return Err(SimpleAgentsError::Config(format!(
270 "{field} cannot be empty"
271 )));
272 }
273
274 Ok(value.to_string())
275}
276
277unsafe fn cstr_to_optional_string(ptr: *const c_char, field: &str) -> Result<Option<String>> {
278 if ptr.is_null() {
279 return Ok(None);
280 }
281 let c_str = CStr::from_ptr(ptr);
282 let value = c_str
283 .to_str()
284 .map_err(|_| SimpleAgentsError::Config(format!("{field} must be valid UTF-8")))?;
285 if value.is_empty() {
286 return Ok(None);
287 }
288 Ok(Some(value.to_string()))
289}
290
291fn parse_workflow_run_options(raw_json: Option<String>) -> Result<YamlWorkflowRunOptions> {
292 match raw_json {
293 None => Ok(YamlWorkflowRunOptions::default()),
294 Some(value) => {
295 if value.trim().is_empty() {
296 return Ok(YamlWorkflowRunOptions::default());
297 }
298 serde_json::from_str::<YamlWorkflowRunOptions>(&value).map_err(|error| {
299 SimpleAgentsError::Config(format!(
300 "workflow_options_json must be valid JSON: {error}"
301 ))
302 })
303 }
304 }
305}
306
307fn build_client(provider: Arc<dyn Provider>) -> Result<SimpleAgentsClient> {
308 SimpleAgentsClientBuilder::new()
309 .with_provider(provider)
310 .build()
311}
312
313fn build_request_from_messages(
314 model: &str,
315 messages: Vec<Message>,
316 max_tokens: i32,
317 temperature: f32,
318 top_p: f32,
319) -> Result<CompletionRequest> {
320 let mut builder = CompletionRequest::builder().model(model).messages(messages);
321
322 if max_tokens > 0 {
323 builder = builder.max_tokens(max_tokens as u32);
324 }
325
326 if temperature >= 0.0 {
327 builder = builder.temperature(temperature);
328 }
329
330 if top_p >= 0.0 {
331 builder = builder.top_p(top_p);
332 }
333
334 builder.build()
335}
336
337fn build_request(
338 model: &str,
339 prompt: &str,
340 max_tokens: i32,
341 temperature: f32,
342) -> Result<CompletionRequest> {
343 build_request_from_messages(
344 model,
345 vec![Message::user(prompt)],
346 max_tokens,
347 temperature,
348 -1.0,
349 )
350}
351
352fn schema_aliases(value: Option<&JsonValue>) -> Vec<String> {
353 value
354 .and_then(JsonValue::as_array)
355 .map(|arr| {
356 arr.iter()
357 .filter_map(|v| v.as_str().map(str::to_string))
358 .collect()
359 })
360 .unwrap_or_default()
361}
362
363fn parse_schema_field(value: &JsonValue) -> Result<SchemaField> {
364 let name = value
365 .get("name")
366 .and_then(JsonValue::as_str)
367 .ok_or_else(|| SimpleAgentsError::Config("schema field missing `name`".to_string()))?;
368 let schema_value = value.get("schema").ok_or_else(|| {
369 SimpleAgentsError::Config(format!("schema field `{name}` missing `schema`"))
370 })?;
371
372 Ok(SchemaField {
373 name: name.to_string(),
374 schema: parse_schema(schema_value)?,
375 required: value
376 .get("required")
377 .and_then(JsonValue::as_bool)
378 .unwrap_or(true),
379 aliases: schema_aliases(value.get("aliases")),
380 default: None,
381 description: None,
382 stream_annotation: StreamAnnotation::Normal,
383 })
384}
385
386fn parse_schema(value: &JsonValue) -> Result<Schema> {
387 let kind = value
388 .get("kind")
389 .and_then(JsonValue::as_str)
390 .ok_or_else(|| SimpleAgentsError::Config("schema requires `kind`".to_string()))?
391 .to_lowercase();
392
393 match kind.as_str() {
394 "string" => Ok(Schema::String),
395 "int" => Ok(Schema::Int),
396 "uint" => Ok(Schema::UInt),
397 "float" => Ok(Schema::Float),
398 "bool" => Ok(Schema::Bool),
399 "null" => Ok(Schema::Null),
400 "any" => Ok(Schema::Any),
401 "array" => {
402 let elements = value.get("elements").ok_or_else(|| {
403 SimpleAgentsError::Config("array schema requires `elements`".to_string())
404 })?;
405 Ok(Schema::array(parse_schema(elements)?))
406 }
407 "union" => {
408 let variants = value
409 .get("variants")
410 .and_then(JsonValue::as_array)
411 .ok_or_else(|| {
412 SimpleAgentsError::Config("union schema requires `variants` array".to_string())
413 })?;
414 let schemas = variants
415 .iter()
416 .map(parse_schema)
417 .collect::<Result<Vec<_>>>()?;
418 Ok(Schema::union(schemas))
419 }
420 "object" => {
421 let fields = value
422 .get("fields")
423 .and_then(JsonValue::as_array)
424 .ok_or_else(|| {
425 SimpleAgentsError::Config("object schema requires `fields` array".to_string())
426 })?;
427 let converted = fields
428 .iter()
429 .map(parse_schema_field)
430 .collect::<Result<Vec<_>>>()?;
431 Ok(Schema::Object(ObjectSchema {
432 fields: converted,
433 allow_additional_fields: value
434 .get("allow_additional_fields")
435 .and_then(JsonValue::as_bool)
436 .unwrap_or(false),
437 }))
438 }
439 other => Err(SimpleAgentsError::Config(format!(
440 "unsupported schema kind `{other}`"
441 ))),
442 }
443}
444
445fn completion_options(mode: Option<&str>, schema_json: Option<&str>) -> Result<CompletionOptions> {
446 let mode = match mode.map(|m| m.to_ascii_lowercase()) {
447 None => CompletionMode::Standard,
448 Some(m) if m.is_empty() || m == "standard" => CompletionMode::Standard,
449 Some(m) if m == "healed_json" => CompletionMode::HealedJson,
450 Some(m) if m == "schema" => {
451 let raw_schema = schema_json.ok_or_else(|| {
452 SimpleAgentsError::Config("mode `schema` requires `schema_json`".to_string())
453 })?;
454 let value: JsonValue = serde_json::from_str(raw_schema)
455 .map_err(|e| SimpleAgentsError::Config(format!("invalid `schema_json`: {e}")))?;
456 CompletionMode::CoercedSchema(parse_schema(&value)?)
457 }
458 Some(other) => {
459 return Err(SimpleAgentsError::Config(format!(
460 "unknown mode `{other}` (expected standard|healed_json|schema)"
461 )))
462 }
463 };
464
465 Ok(CompletionOptions { mode })
466}
467
468fn role_to_string(role: Role) -> String {
469 role.as_str().to_string()
470}
471
472fn finish_reason_to_string(finish_reason: FinishReason) -> String {
473 finish_reason.as_str().to_string()
474}
475
476fn tool_type_to_string(tool_type: ToolType) -> String {
477 match tool_type {
478 ToolType::Function => "function".to_string(),
479 }
480}
481
482fn usage_to_ffi(usage: Usage) -> FfiUsage {
483 FfiUsage {
484 prompt_tokens: usage.prompt_tokens,
485 completion_tokens: usage.completion_tokens,
486 total_tokens: usage.total_tokens,
487 }
488}
489
490fn map_tool_calls(tool_calls: Option<Vec<ToolCall>>) -> Option<Vec<FfiToolCall>> {
491 tool_calls.map(|calls| {
492 calls
493 .into_iter()
494 .map(|call| FfiToolCall {
495 id: call.id,
496 tool_type: tool_type_to_string(call.tool_type),
497 function: FfiToolCallFunction {
498 name: call.function.name,
499 arguments: call.function.arguments,
500 },
501 })
502 .collect()
503 })
504}
505
506fn healing_data_from(result: CoercionResult<JsonValue>) -> FfiHealingData {
507 FfiHealingData {
508 value: result.value,
509 flags: result.flags,
510 confidence: result.confidence,
511 }
512}
513
514fn completion_result_from_response(
515 response: CompletionResponse,
516 healed: Option<FfiHealingData>,
517 coerced: Option<FfiHealingData>,
518) -> FfiCompletionResult {
519 let content = response.content().map(str::to_string);
520 let choice = response.choices.first();
521 let role = choice
522 .map(|c| role_to_string(c.message.role))
523 .unwrap_or_else(|| "assistant".to_string());
524 let finish_reason = choice.map(|c| finish_reason_to_string(c.finish_reason));
525 let tool_calls = choice.and_then(|c| c.message.tool_calls.clone());
526 let usage = response.usage;
527
528 FfiCompletionResult {
529 id: response.id.clone(),
530 model: response.model.clone(),
531 role: role.clone(),
532 content: content.clone(),
533 tool_calls: map_tool_calls(tool_calls),
534 finish_reason,
535 usage: usage_to_ffi(usage),
536 raw: content,
537 healed,
538 coerced,
539 }
540}
541
542fn parse_messages(messages: *const SAMessage, messages_len: usize) -> Result<Vec<Message>> {
543 if messages.is_null() {
544 return Err(SimpleAgentsError::Config(
545 "messages cannot be null".to_string(),
546 ));
547 }
548 if messages_len == 0 {
549 return Err(SimpleAgentsError::Config(
550 "messages cannot be empty".to_string(),
551 ));
552 }
553
554 let input = unsafe { std::slice::from_raw_parts(messages, messages_len) };
555 input
556 .iter()
557 .enumerate()
558 .map(|(idx, msg)| {
559 let role = unsafe { cstr_to_string(msg.role, &format!("messages[{idx}].role"))? }
560 .to_ascii_lowercase();
561 let content =
562 unsafe { cstr_to_string(msg.content, &format!("messages[{idx}].content"))? };
563 let name =
564 unsafe { cstr_to_optional_string(msg.name, &format!("messages[{idx}].name"))? };
565 let tool_call_id = unsafe {
566 cstr_to_optional_string(msg.tool_call_id, &format!("messages[{idx}].tool_call_id"))?
567 };
568
569 let parsed_role = role.parse::<Role>().map_err(|_| {
570 SimpleAgentsError::Config(format!(
571 "messages[{idx}].role must be one of user|assistant|system|tool"
572 ))
573 })?;
574
575 let parsed = match parsed_role {
576 Role::User => Message::user(content),
577 Role::Assistant => Message::assistant(content),
578 Role::System => Message::system(content),
579 Role::Tool => {
580 let call_id = tool_call_id.ok_or_else(|| {
581 SimpleAgentsError::Config(format!(
582 "messages[{idx}].tool_call_id is required for tool role"
583 ))
584 })?;
585 Message::tool(content, call_id)
586 }
587 };
588
589 Ok(match name {
590 Some(name) => parsed.with_name(name),
591 None => parsed,
592 })
593 })
594 .collect()
595}
596
597fn ffi_result_string(result: Result<String>) -> *mut c_char {
598 match result {
599 Ok(value) => match CString::new(value) {
600 Ok(c_string) => {
601 clear_last_error();
602 c_string.into_raw()
603 }
604 Err(_) => {
605 set_last_error("Response contained an interior null byte".to_string());
606 std::ptr::null_mut()
607 }
608 },
609 Err(error) => {
610 set_last_error(error.to_string());
611 std::ptr::null_mut()
612 }
613 }
614}
615
616fn ffi_guard<T>(action: impl FnOnce() -> Result<T>) -> *mut c_char
617where
618 T: Into<String>,
619{
620 let result = catch_unwind(AssertUnwindSafe(action));
621 match result {
622 Ok(inner) => ffi_result_string(inner.map(Into::into)),
623 Err(_) => {
624 set_last_error("Panic occurred in FFI call".to_string());
625 std::ptr::null_mut()
626 }
627 }
628}
629
630fn ffi_guard_status(action: impl FnOnce() -> Result<()>) -> i32 {
631 let result = catch_unwind(AssertUnwindSafe(action));
632 match result {
633 Ok(Ok(())) => {
634 clear_last_error();
635 0
636 }
637 Ok(Err(error)) => {
638 set_last_error(error.to_string());
639 -1
640 }
641 Err(_) => {
642 set_last_error("Panic occurred in FFI call".to_string());
643 -1
644 }
645 }
646}
647
648fn emit_stream_event(
649 callback: extern "C" fn(*const c_char, *mut c_void) -> i32,
650 user_data: *mut c_void,
651 event: FfiStreamEvent,
652) -> Result<()> {
653 let payload = serde_json::to_string(&event)
654 .map_err(|e| SimpleAgentsError::Config(format!("failed to serialize stream event: {e}")))?;
655 let payload = CString::new(payload).map_err(|_| {
656 SimpleAgentsError::Config("stream event contains interior null byte".to_string())
657 })?;
658
659 let callback_status = callback(payload.as_ptr(), user_data);
660 if callback_status == 0 {
661 Ok(())
662 } else {
663 Err(SimpleAgentsError::Config(
664 "stream cancelled by callback".to_string(),
665 ))
666 }
667}
668
669#[no_mangle]
678pub unsafe extern "C" fn sa_client_new_from_env(provider_name: *const c_char) -> *mut SAClient {
679 let result = catch_unwind(AssertUnwindSafe(|| -> Result<Box<SAClient>> {
680 let provider = cstr_to_string(provider_name, "provider_name")?;
681 let provider = provider_from_env(&provider)?;
682 let client = build_client(provider)?;
683 let runtime = build_runtime()?;
684
685 Ok(Box::new(SAClient {
686 inner: FfiClient {
687 runtime: Mutex::new(runtime),
688 client,
689 },
690 }))
691 }));
692
693 match result {
694 Ok(Ok(client)) => {
695 clear_last_error();
696 Box::into_raw(client)
697 }
698 Ok(Err(error)) => {
699 set_last_error(error.to_string());
700 std::ptr::null_mut()
701 }
702 Err(_) => {
703 set_last_error("Panic occurred in sa_client_new_from_env".to_string());
704 std::ptr::null_mut()
705 }
706 }
707}
708
709#[no_mangle]
716pub unsafe extern "C" fn sa_client_free(client: *mut SAClient) {
717 if client.is_null() {
718 return;
719 }
720
721 drop(Box::from_raw(client));
722}
723
724#[no_mangle]
734pub unsafe extern "C" fn sa_complete(
735 client: *mut SAClient,
736 model: *const c_char,
737 prompt: *const c_char,
738 max_tokens: i32,
739 temperature: f32,
740) -> *mut c_char {
741 if client.is_null() {
742 set_last_error("client cannot be null".to_string());
743 return std::ptr::null_mut();
744 }
745
746 ffi_guard(|| {
747 let model = cstr_to_string(model, "model")?;
748 let prompt = cstr_to_string(prompt, "prompt")?;
749 let request = build_request(&model, &prompt, max_tokens, temperature)?;
750
751 let client = &(*client).inner;
752 let runtime = client
753 .runtime
754 .lock()
755 .map_err(|_| SimpleAgentsError::Config("runtime lock poisoned".to_string()))?;
756 let outcome = runtime.block_on(
757 client
758 .client
759 .complete(&request, CompletionOptions::default()),
760 )?;
761 let response = match outcome {
762 CompletionOutcome::Response(response) => response,
763 CompletionOutcome::Stream(_) => {
764 return Err(SimpleAgentsError::Config(
765 "streaming response returned from complete".to_string(),
766 ))
767 }
768 CompletionOutcome::HealedJson(_) => {
769 return Err(SimpleAgentsError::Config(
770 "healed json response returned from complete".to_string(),
771 ))
772 }
773 CompletionOutcome::CoercedSchema(_) => {
774 return Err(SimpleAgentsError::Config(
775 "schema response returned from complete".to_string(),
776 ))
777 }
778 };
779
780 Ok(response.content().unwrap_or_default().to_string())
781 })
782}
783
784#[no_mangle]
797pub unsafe extern "C" fn sa_complete_messages_json(
798 client: *mut SAClient,
799 model: *const c_char,
800 messages: *const SAMessage,
801 messages_len: usize,
802 max_tokens: i32,
803 temperature: f32,
804 top_p: f32,
805 mode: *const c_char,
806 schema_json: *const c_char,
807) -> *mut c_char {
808 if client.is_null() {
809 set_last_error("client cannot be null".to_string());
810 return std::ptr::null_mut();
811 }
812
813 ffi_guard(|| {
814 let model = cstr_to_string(model, "model")?;
815 let messages = parse_messages(messages, messages_len)?;
816 let request =
817 build_request_from_messages(&model, messages, max_tokens, temperature, top_p)?;
818
819 let mode = cstr_to_optional_string(mode, "mode")?;
820 let schema_json = cstr_to_optional_string(schema_json, "schema_json")?;
821 let options = completion_options(mode.as_deref(), schema_json.as_deref())?;
822
823 let client = &(*client).inner;
824 let runtime = client
825 .runtime
826 .lock()
827 .map_err(|_| SimpleAgentsError::Config("runtime lock poisoned".to_string()))?;
828 let outcome = runtime.block_on(client.client.complete(&request, options))?;
829
830 let payload = match outcome {
831 CompletionOutcome::Response(response) => {
832 completion_result_from_response(response, None, None)
833 }
834 CompletionOutcome::HealedJson(HealedJsonResponse { response, parsed }) => {
835 completion_result_from_response(response, Some(healing_data_from(parsed)), None)
836 }
837 CompletionOutcome::CoercedSchema(HealedSchemaResponse {
838 response,
839 parsed,
840 coerced,
841 }) => completion_result_from_response(
842 response,
843 Some(healing_data_from(parsed)),
844 Some(healing_data_from(coerced)),
845 ),
846 CompletionOutcome::Stream(_) => {
847 return Err(SimpleAgentsError::Config(
848 "streaming mode is not supported via sa_complete_messages_json".to_string(),
849 ))
850 }
851 };
852
853 serde_json::to_string(&payload)
854 .map_err(|e| SimpleAgentsError::Config(format!("failed to serialize result: {e}")))
855 })
856}
857
858#[no_mangle]
869pub unsafe extern "C" fn sa_stream_messages(
870 client: *mut SAClient,
871 model: *const c_char,
872 messages: *const SAMessage,
873 messages_len: usize,
874 max_tokens: i32,
875 temperature: f32,
876 top_p: f32,
877 callback: SAStreamCallback,
878 user_data: *mut c_void,
879) -> i32 {
880 if client.is_null() {
881 set_last_error("client cannot be null".to_string());
882 return -1;
883 }
884
885 let Some(callback) = callback else {
886 set_last_error("callback cannot be null".to_string());
887 return -1;
888 };
889
890 ffi_guard_status(|| {
891 let model = cstr_to_string(model, "model")?;
892 let messages = parse_messages(messages, messages_len)?;
893
894 let mut builder = CompletionRequest::builder()
895 .model(&model)
896 .messages(messages);
897 if max_tokens > 0 {
898 builder = builder.max_tokens(max_tokens as u32);
899 }
900 if temperature >= 0.0 {
901 builder = builder.temperature(temperature);
902 }
903 if top_p >= 0.0 {
904 builder = builder.top_p(top_p);
905 }
906 builder = builder.stream(true);
907 let request = builder.build()?;
908
909 let client = &(*client).inner;
910 let runtime = client
911 .runtime
912 .lock()
913 .map_err(|_| SimpleAgentsError::Config("runtime lock poisoned".to_string()))?;
914
915 let outcome = runtime.block_on(
916 client
917 .client
918 .complete(&request, CompletionOptions::default()),
919 )?;
920 let mut stream = match outcome {
921 CompletionOutcome::Stream(stream) => stream,
922 CompletionOutcome::Response(_) => {
923 return Err(SimpleAgentsError::Config(
924 "non-streaming response returned from sa_stream_messages".to_string(),
925 ))
926 }
927 CompletionOutcome::HealedJson(_) => {
928 return Err(SimpleAgentsError::Config(
929 "healed json response returned from sa_stream_messages".to_string(),
930 ))
931 }
932 CompletionOutcome::CoercedSchema(_) => {
933 return Err(SimpleAgentsError::Config(
934 "schema response returned from sa_stream_messages".to_string(),
935 ))
936 }
937 };
938
939 runtime.block_on(async {
940 while let Some(chunk_result) = stream.next().await {
941 match chunk_result {
942 Ok(chunk) => {
943 emit_stream_event(callback, user_data, FfiStreamEvent::Chunk { chunk })?;
944 }
945 Err(error) => {
946 let message = error.to_string();
947 let _ = emit_stream_event(
948 callback,
949 user_data,
950 FfiStreamEvent::Error {
951 message: message.clone(),
952 },
953 );
954 return Err(error);
955 }
956 }
957 }
958
959 emit_stream_event(callback, user_data, FfiStreamEvent::Done)
960 })
961 })
962}
963
964#[no_mangle]
972pub unsafe extern "C" fn sa_run_email_workflow_yaml(
973 client: *mut SAClient,
974 workflow_path: *const c_char,
975 email_text: *const c_char,
976) -> *mut c_char {
977 if client.is_null() {
978 set_last_error("client cannot be null".to_string());
979 return std::ptr::null_mut();
980 }
981
982 ffi_guard(|| {
983 let workflow_path = cstr_to_string(workflow_path, "workflow_path")?;
984 let email_text = cstr_to_string(email_text, "email_text")?;
985
986 let client = &(*client).inner;
987 let runtime = client
988 .runtime
989 .lock()
990 .map_err(|_| SimpleAgentsError::Config("runtime lock poisoned".to_string()))?;
991
992 let output = runtime
993 .block_on(run_email_workflow_yaml_file_with_client(
994 std::path::Path::new(workflow_path.as_str()),
995 email_text.as_str(),
996 &client.client,
997 ))
998 .map_err(|error| {
999 SimpleAgentsError::Config(format!("failed to run workflow yaml: {error}"))
1000 })?;
1001
1002 serde_json::to_string(&output)
1003 .map_err(|e| SimpleAgentsError::Config(format!("failed to serialize result: {e}")))
1004 })
1005}
1006
1007#[no_mangle]
1016pub unsafe extern "C" fn sa_run_workflow_yaml(
1017 client: *mut SAClient,
1018 workflow_path: *const c_char,
1019 workflow_input_json: *const c_char,
1020) -> *mut c_char {
1021 sa_run_workflow_yaml_with_options(client, workflow_path, workflow_input_json, std::ptr::null())
1022}
1023
1024#[no_mangle]
1036pub unsafe extern "C" fn sa_run_workflow_yaml_with_options(
1037 client: *mut SAClient,
1038 workflow_path: *const c_char,
1039 workflow_input_json: *const c_char,
1040 workflow_options_json: *const c_char,
1041) -> *mut c_char {
1042 if client.is_null() {
1043 set_last_error("client cannot be null".to_string());
1044 return std::ptr::null_mut();
1045 }
1046
1047 ffi_guard(|| {
1048 let workflow_path = cstr_to_string(workflow_path, "workflow_path")?;
1049 let workflow_input_json = cstr_to_string(workflow_input_json, "workflow_input_json")?;
1050 let workflow_options_json =
1051 cstr_to_optional_string(workflow_options_json, "workflow_options_json")?;
1052 let workflow_input: JsonValue =
1053 serde_json::from_str(&workflow_input_json).map_err(|e| {
1054 SimpleAgentsError::Config(format!("workflow_input_json must be valid JSON: {e}"))
1055 })?;
1056 if !workflow_input.is_object() {
1057 return Err(SimpleAgentsError::Config(
1058 "workflow_input_json must decode to a JSON object".to_string(),
1059 ));
1060 }
1061 let workflow_options = parse_workflow_run_options(workflow_options_json)?;
1062
1063 let client = &(*client).inner;
1064 let runtime = client
1065 .runtime
1066 .lock()
1067 .map_err(|_| SimpleAgentsError::Config("runtime lock poisoned".to_string()))?;
1068
1069 let output = runtime
1070 .block_on(
1071 run_workflow_yaml_file_with_client_and_custom_worker_and_events_and_options(
1072 std::path::Path::new(workflow_path.as_str()),
1073 &workflow_input,
1074 &client.client,
1075 None,
1076 None,
1077 &workflow_options,
1078 ),
1079 )
1080 .map_err(|error| {
1081 SimpleAgentsError::Config(format!("failed to run workflow yaml: {error}"))
1082 })?;
1083
1084 serde_json::to_string(&output)
1085 .map_err(|e| SimpleAgentsError::Config(format!("failed to serialize result: {e}")))
1086 })
1087}
1088
1089#[no_mangle]
1099pub unsafe extern "C" fn sa_run_workflow_yaml_with_events(
1100 client: *mut SAClient,
1101 workflow_path: *const c_char,
1102 workflow_input_json: *const c_char,
1103 workflow_options_json: *const c_char,
1104) -> *mut c_char {
1105 if client.is_null() {
1106 set_last_error("client cannot be null".to_string());
1107 return std::ptr::null_mut();
1108 }
1109
1110 ffi_guard(|| {
1111 let workflow_path = cstr_to_string(workflow_path, "workflow_path")?;
1112 let workflow_input_json = cstr_to_string(workflow_input_json, "workflow_input_json")?;
1113 let workflow_options_json =
1114 cstr_to_optional_string(workflow_options_json, "workflow_options_json")?;
1115 let workflow_input: JsonValue =
1116 serde_json::from_str(&workflow_input_json).map_err(|e| {
1117 SimpleAgentsError::Config(format!("workflow_input_json must be valid JSON: {e}"))
1118 })?;
1119 if !workflow_input.is_object() {
1120 return Err(SimpleAgentsError::Config(
1121 "workflow_input_json must decode to a JSON object".to_string(),
1122 ));
1123 }
1124 let workflow_options = parse_workflow_run_options(workflow_options_json)?;
1125
1126 let client = &(*client).inner;
1127 let runtime = client
1128 .runtime
1129 .lock()
1130 .map_err(|_| SimpleAgentsError::Config("runtime lock poisoned".to_string()))?;
1131
1132 let event_sink = RecordingWorkflowEventSink::new();
1133 let output = runtime
1134 .block_on(
1135 run_workflow_yaml_file_with_client_and_custom_worker_and_events_and_options(
1136 std::path::Path::new(workflow_path.as_str()),
1137 &workflow_input,
1138 &client.client,
1139 None,
1140 Some(&event_sink),
1141 &workflow_options,
1142 ),
1143 )
1144 .map_err(|error| {
1145 SimpleAgentsError::Config(format!("failed to run workflow yaml: {error}"))
1146 })?;
1147
1148 let mut output_value = serde_json::to_value(output)
1149 .map_err(|e| SimpleAgentsError::Config(format!("failed to serialize result: {e}")))?;
1150 event_sink.attach_to_output(&mut output_value)?;
1151 serde_json::to_string(&output_value)
1152 .map_err(|e| SimpleAgentsError::Config(format!("failed to serialize result: {e}")))
1153 })
1154}
1155
1156#[no_mangle]
1167pub unsafe extern "C" fn sa_run_workflow_yaml_stream_events(
1168 client: *mut SAClient,
1169 workflow_path: *const c_char,
1170 workflow_input_json: *const c_char,
1171 workflow_options_json: *const c_char,
1172 callback: SAWorkflowEventCallback,
1173 user_data: *mut c_void,
1174) -> *mut c_char {
1175 if client.is_null() {
1176 set_last_error("client cannot be null".to_string());
1177 return std::ptr::null_mut();
1178 }
1179
1180 let Some(callback) = callback else {
1181 set_last_error("callback cannot be null".to_string());
1182 return std::ptr::null_mut();
1183 };
1184
1185 ffi_guard(|| {
1186 let workflow_path = cstr_to_string(workflow_path, "workflow_path")?;
1187 let workflow_input_json = cstr_to_string(workflow_input_json, "workflow_input_json")?;
1188 let workflow_options_json =
1189 cstr_to_optional_string(workflow_options_json, "workflow_options_json")?;
1190 let workflow_input: JsonValue =
1191 serde_json::from_str(&workflow_input_json).map_err(|e| {
1192 SimpleAgentsError::Config(format!("workflow_input_json must be valid JSON: {e}"))
1193 })?;
1194 if !workflow_input.is_object() {
1195 return Err(SimpleAgentsError::Config(
1196 "workflow_input_json must decode to a JSON object".to_string(),
1197 ));
1198 }
1199 let workflow_options = parse_workflow_run_options(workflow_options_json)?;
1200
1201 let client = &(*client).inner;
1202 let runtime = client
1203 .runtime
1204 .lock()
1205 .map_err(|_| SimpleAgentsError::Config("runtime lock poisoned".to_string()))?;
1206
1207 let event_sink = CallbackWorkflowEventSink::new(callback, user_data);
1208 let output = runtime
1209 .block_on(
1210 run_workflow_yaml_file_with_client_and_custom_worker_and_events_and_options(
1211 std::path::Path::new(workflow_path.as_str()),
1212 &workflow_input,
1213 &client.client,
1214 None,
1215 Some(&event_sink),
1216 &workflow_options,
1217 ),
1218 )
1219 .map_err(|error| {
1220 SimpleAgentsError::Config(format!("failed to run workflow yaml: {error}"))
1221 })?;
1222
1223 if event_sink.callback_failed() {
1224 return Err(SimpleAgentsError::Config(
1225 "workflow event callback returned non-zero status or failed to serialize payload"
1226 .to_string(),
1227 ));
1228 }
1229
1230 serde_json::to_string(&output)
1231 .map_err(|e| SimpleAgentsError::Config(format!("failed to serialize result: {e}")))
1232 })
1233}
1234
1235#[no_mangle]
1239pub extern "C" fn sa_last_error_message() -> *mut c_char {
1240 match take_last_error() {
1241 Some(message) => match CString::new(message) {
1242 Ok(c_string) => c_string.into_raw(),
1243 Err(_) => std::ptr::null_mut(),
1244 },
1245 None => std::ptr::null_mut(),
1246 }
1247}
1248
1249#[no_mangle]
1256pub unsafe extern "C" fn sa_string_free(value: *mut c_char) {
1257 if value.is_null() {
1258 return;
1259 }
1260
1261 drop(CString::from_raw(value));
1262}