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