1use std::collections::HashMap;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use futures::Stream;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use thiserror::Error;
11
12#[cfg(feature = "schemars")]
13pub use schemars;
14
15pub mod context_budget;
16pub mod token_counter;
17
18pub use context_budget::{ContextBudget, ContextSlot, Priority};
19pub use token_counter::{HeuristicTokenCounter, TokenCounter};
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(tag = "type", rename_all = "snake_case")]
28pub enum ContentBlock {
29 Text {
30 text: String,
31 },
32 Image {
33 url: String,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 detail: Option<String>,
36 },
37 Audio {
38 url: String,
39 },
40 Video {
41 url: String,
42 },
43 File {
44 url: String,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
46 mime_type: Option<String>,
47 },
48 Data {
49 data: Value,
50 },
51 Reasoning {
52 content: String,
53 },
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[serde(tag = "role")]
63pub enum Message {
64 #[serde(rename = "system")]
65 System {
66 content: String,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
68 id: Option<String>,
69 #[serde(default, skip_serializing_if = "Option::is_none")]
70 name: Option<String>,
71 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
72 additional_kwargs: HashMap<String, Value>,
73 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
74 response_metadata: HashMap<String, Value>,
75 #[serde(default, skip_serializing_if = "Vec::is_empty")]
76 content_blocks: Vec<ContentBlock>,
77 },
78 #[serde(rename = "human")]
79 Human {
80 content: String,
81 #[serde(default, skip_serializing_if = "Option::is_none")]
82 id: Option<String>,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
84 name: Option<String>,
85 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
86 additional_kwargs: HashMap<String, Value>,
87 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
88 response_metadata: HashMap<String, Value>,
89 #[serde(default, skip_serializing_if = "Vec::is_empty")]
90 content_blocks: Vec<ContentBlock>,
91 },
92 #[serde(rename = "assistant")]
93 AI {
94 content: String,
95 #[serde(default, skip_serializing_if = "Vec::is_empty")]
96 tool_calls: Vec<ToolCall>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 id: Option<String>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 name: Option<String>,
101 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
102 additional_kwargs: HashMap<String, Value>,
103 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
104 response_metadata: HashMap<String, Value>,
105 #[serde(default, skip_serializing_if = "Vec::is_empty")]
106 content_blocks: Vec<ContentBlock>,
107 #[serde(default, skip_serializing_if = "Option::is_none")]
108 usage_metadata: Option<TokenUsage>,
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
110 invalid_tool_calls: Vec<InvalidToolCall>,
111 },
112 #[serde(rename = "tool")]
113 Tool {
114 content: String,
115 tool_call_id: String,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
117 id: Option<String>,
118 #[serde(default, skip_serializing_if = "Option::is_none")]
119 name: Option<String>,
120 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
121 additional_kwargs: HashMap<String, Value>,
122 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
123 response_metadata: HashMap<String, Value>,
124 #[serde(default, skip_serializing_if = "Vec::is_empty")]
125 content_blocks: Vec<ContentBlock>,
126 },
127 #[serde(rename = "chat")]
128 Chat {
129 custom_role: String,
130 content: String,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 id: Option<String>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 name: Option<String>,
135 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
136 additional_kwargs: HashMap<String, Value>,
137 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
138 response_metadata: HashMap<String, Value>,
139 #[serde(default, skip_serializing_if = "Vec::is_empty")]
140 content_blocks: Vec<ContentBlock>,
141 },
142 #[serde(rename = "remove")]
145 Remove {
146 id: String,
148 },
149}
150
151macro_rules! set_message_field {
154 ($self:expr, $field:ident, $value:expr) => {
155 match $self {
156 Message::System { $field, .. } => *$field = $value,
157 Message::Human { $field, .. } => *$field = $value,
158 Message::AI { $field, .. } => *$field = $value,
159 Message::Tool { $field, .. } => *$field = $value,
160 Message::Chat { $field, .. } => *$field = $value,
161 Message::Remove { .. } => { }
162 }
163 };
164}
165
166macro_rules! get_message_field {
169 ($self:expr, $field:ident) => {
170 match $self {
171 Message::System { $field, .. } => $field,
172 Message::Human { $field, .. } => $field,
173 Message::AI { $field, .. } => $field,
174 Message::Tool { $field, .. } => $field,
175 Message::Chat { $field, .. } => $field,
176 Message::Remove { .. } => unreachable!("get_message_field called on Remove variant"),
177 }
178 };
179}
180
181impl Message {
182 pub fn system(content: impl Into<String>) -> Self {
185 Message::System {
186 content: content.into(),
187 id: None,
188 name: None,
189 additional_kwargs: HashMap::new(),
190 response_metadata: HashMap::new(),
191 content_blocks: Vec::new(),
192 }
193 }
194
195 pub fn human(content: impl Into<String>) -> Self {
196 Message::Human {
197 content: content.into(),
198 id: None,
199 name: None,
200 additional_kwargs: HashMap::new(),
201 response_metadata: HashMap::new(),
202 content_blocks: Vec::new(),
203 }
204 }
205
206 pub fn ai(content: impl Into<String>) -> Self {
207 Message::AI {
208 content: content.into(),
209 tool_calls: vec![],
210 id: None,
211 name: None,
212 additional_kwargs: HashMap::new(),
213 response_metadata: HashMap::new(),
214 content_blocks: Vec::new(),
215 usage_metadata: None,
216 invalid_tool_calls: Vec::new(),
217 }
218 }
219
220 pub fn ai_with_tool_calls(content: impl Into<String>, tool_calls: Vec<ToolCall>) -> Self {
221 Message::AI {
222 content: content.into(),
223 tool_calls,
224 id: None,
225 name: None,
226 additional_kwargs: HashMap::new(),
227 response_metadata: HashMap::new(),
228 content_blocks: Vec::new(),
229 usage_metadata: None,
230 invalid_tool_calls: Vec::new(),
231 }
232 }
233
234 pub fn tool(content: impl Into<String>, tool_call_id: impl Into<String>) -> Self {
235 Message::Tool {
236 content: content.into(),
237 tool_call_id: tool_call_id.into(),
238 id: None,
239 name: None,
240 additional_kwargs: HashMap::new(),
241 response_metadata: HashMap::new(),
242 content_blocks: Vec::new(),
243 }
244 }
245
246 pub fn chat(role: impl Into<String>, content: impl Into<String>) -> Self {
247 Message::Chat {
248 custom_role: role.into(),
249 content: content.into(),
250 id: None,
251 name: None,
252 additional_kwargs: HashMap::new(),
253 response_metadata: HashMap::new(),
254 content_blocks: Vec::new(),
255 }
256 }
257
258 pub fn remove(id: impl Into<String>) -> Self {
260 Message::Remove { id: id.into() }
261 }
262
263 pub fn with_id(mut self, value: impl Into<String>) -> Self {
266 set_message_field!(&mut self, id, Some(value.into()));
267 self
268 }
269
270 pub fn with_name(mut self, value: impl Into<String>) -> Self {
271 set_message_field!(&mut self, name, Some(value.into()));
272 self
273 }
274
275 pub fn with_additional_kwarg(mut self, key: impl Into<String>, value: Value) -> Self {
276 match &mut self {
277 Message::System {
278 additional_kwargs, ..
279 }
280 | Message::Human {
281 additional_kwargs, ..
282 }
283 | Message::AI {
284 additional_kwargs, ..
285 }
286 | Message::Tool {
287 additional_kwargs, ..
288 }
289 | Message::Chat {
290 additional_kwargs, ..
291 } => {
292 additional_kwargs.insert(key.into(), value);
293 }
294 Message::Remove { .. } => { }
295 }
296 self
297 }
298
299 pub fn with_response_metadata_entry(mut self, key: impl Into<String>, value: Value) -> Self {
300 match &mut self {
301 Message::System {
302 response_metadata, ..
303 }
304 | Message::Human {
305 response_metadata, ..
306 }
307 | Message::AI {
308 response_metadata, ..
309 }
310 | Message::Tool {
311 response_metadata, ..
312 }
313 | Message::Chat {
314 response_metadata, ..
315 } => {
316 response_metadata.insert(key.into(), value);
317 }
318 Message::Remove { .. } => { }
319 }
320 self
321 }
322
323 pub fn with_content_blocks(mut self, blocks: Vec<ContentBlock>) -> Self {
324 set_message_field!(&mut self, content_blocks, blocks);
325 self
326 }
327
328 pub fn with_usage_metadata(mut self, usage: TokenUsage) -> Self {
329 if let Message::AI { usage_metadata, .. } = &mut self {
330 *usage_metadata = Some(usage);
331 }
332 self
333 }
334
335 pub fn content(&self) -> &str {
338 match self {
339 Message::Remove { .. } => "",
340 other => get_message_field!(other, content),
341 }
342 }
343
344 pub fn role(&self) -> &str {
345 match self {
346 Message::System { .. } => "system",
347 Message::Human { .. } => "human",
348 Message::AI { .. } => "assistant",
349 Message::Tool { .. } => "tool",
350 Message::Chat { custom_role, .. } => custom_role,
351 Message::Remove { .. } => "remove",
352 }
353 }
354
355 pub fn is_system(&self) -> bool {
356 matches!(self, Message::System { .. })
357 }
358
359 pub fn is_human(&self) -> bool {
360 matches!(self, Message::Human { .. })
361 }
362
363 pub fn is_ai(&self) -> bool {
364 matches!(self, Message::AI { .. })
365 }
366
367 pub fn is_tool(&self) -> bool {
368 matches!(self, Message::Tool { .. })
369 }
370
371 pub fn is_chat(&self) -> bool {
372 matches!(self, Message::Chat { .. })
373 }
374
375 pub fn is_remove(&self) -> bool {
376 matches!(self, Message::Remove { .. })
377 }
378
379 pub fn tool_calls(&self) -> &[ToolCall] {
380 match self {
381 Message::AI { tool_calls, .. } => tool_calls,
382 _ => &[],
383 }
384 }
385
386 pub fn tool_call_id(&self) -> Option<&str> {
387 match self {
388 Message::Tool { tool_call_id, .. } => Some(tool_call_id),
389 _ => None,
390 }
391 }
392
393 pub fn id(&self) -> Option<&str> {
394 match self {
395 Message::Remove { id } => Some(id),
396 other => get_message_field!(other, id).as_deref(),
397 }
398 }
399
400 pub fn name(&self) -> Option<&str> {
401 match self {
402 Message::Remove { .. } => None,
403 other => get_message_field!(other, name).as_deref(),
404 }
405 }
406
407 pub fn additional_kwargs(&self) -> &HashMap<String, Value> {
408 match self {
409 Message::System {
410 additional_kwargs, ..
411 }
412 | Message::Human {
413 additional_kwargs, ..
414 }
415 | Message::AI {
416 additional_kwargs, ..
417 }
418 | Message::Tool {
419 additional_kwargs, ..
420 }
421 | Message::Chat {
422 additional_kwargs, ..
423 } => additional_kwargs,
424 Message::Remove { .. } => {
425 static EMPTY: std::sync::OnceLock<HashMap<String, Value>> =
426 std::sync::OnceLock::new();
427 EMPTY.get_or_init(HashMap::new)
428 }
429 }
430 }
431
432 pub fn response_metadata(&self) -> &HashMap<String, Value> {
433 match self {
434 Message::System {
435 response_metadata, ..
436 }
437 | Message::Human {
438 response_metadata, ..
439 }
440 | Message::AI {
441 response_metadata, ..
442 }
443 | Message::Tool {
444 response_metadata, ..
445 }
446 | Message::Chat {
447 response_metadata, ..
448 } => response_metadata,
449 Message::Remove { .. } => {
450 static EMPTY: std::sync::OnceLock<HashMap<String, Value>> =
451 std::sync::OnceLock::new();
452 EMPTY.get_or_init(HashMap::new)
453 }
454 }
455 }
456
457 pub fn content_blocks(&self) -> &[ContentBlock] {
458 match self {
459 Message::Remove { .. } => &[],
460 other => get_message_field!(other, content_blocks),
461 }
462 }
463
464 pub fn remove_id(&self) -> Option<&str> {
466 match self {
467 Message::Remove { id } => Some(id),
468 _ => None,
469 }
470 }
471
472 pub fn usage_metadata(&self) -> Option<&TokenUsage> {
473 match self {
474 Message::AI { usage_metadata, .. } => usage_metadata.as_ref(),
475 _ => None,
476 }
477 }
478
479 pub fn invalid_tool_calls(&self) -> &[InvalidToolCall] {
480 match self {
481 Message::AI {
482 invalid_tool_calls, ..
483 } => invalid_tool_calls,
484 _ => &[],
485 }
486 }
487
488 pub fn set_content(&mut self, new_content: impl Into<String>) {
490 let new_content = new_content.into();
491 set_message_field!(self, content, new_content);
492 }
493}
494
495pub fn filter_messages(
501 messages: &[Message],
502 include_types: Option<&[&str]>,
503 exclude_types: Option<&[&str]>,
504 include_names: Option<&[&str]>,
505 exclude_names: Option<&[&str]>,
506 include_ids: Option<&[&str]>,
507 exclude_ids: Option<&[&str]>,
508) -> Vec<Message> {
509 messages
510 .iter()
511 .filter(|msg| {
512 if let Some(include) = include_types {
513 if !include.contains(&msg.role()) {
514 return false;
515 }
516 }
517 if let Some(exclude) = exclude_types {
518 if exclude.contains(&msg.role()) {
519 return false;
520 }
521 }
522 if let Some(include) = include_names {
523 match msg.name() {
524 Some(name) => {
525 if !include.contains(&name) {
526 return false;
527 }
528 }
529 None => return false,
530 }
531 }
532 if let Some(exclude) = exclude_names {
533 if let Some(name) = msg.name() {
534 if exclude.contains(&name) {
535 return false;
536 }
537 }
538 }
539 if let Some(include) = include_ids {
540 match msg.id() {
541 Some(id) => {
542 if !include.contains(&id) {
543 return false;
544 }
545 }
546 None => return false,
547 }
548 }
549 if let Some(exclude) = exclude_ids {
550 if let Some(id) = msg.id() {
551 if exclude.contains(&id) {
552 return false;
553 }
554 }
555 }
556 true
557 })
558 .cloned()
559 .collect()
560}
561
562#[derive(Debug, Clone, Copy, PartialEq, Eq)]
564pub enum TrimStrategy {
565 First,
567 Last,
569}
570
571pub fn trim_messages(
577 messages: Vec<Message>,
578 max_tokens: usize,
579 token_counter: impl Fn(&Message) -> usize,
580 strategy: TrimStrategy,
581 include_system: bool,
582) -> Vec<Message> {
583 if messages.is_empty() {
584 return messages;
585 }
586
587 match strategy {
588 TrimStrategy::First => {
589 let mut result = Vec::new();
590 let mut total = 0;
591 for msg in messages {
592 let count = token_counter(&msg);
593 if total + count > max_tokens {
594 break;
595 }
596 total += count;
597 result.push(msg);
598 }
599 result
600 }
601 TrimStrategy::Last => {
602 let (system_msg, rest) = if include_system && messages[0].is_system() {
603 (Some(messages[0].clone()), &messages[1..])
604 } else {
605 (None, messages.as_slice())
606 };
607
608 let system_tokens = system_msg.as_ref().map(&token_counter).unwrap_or(0);
609 let budget = max_tokens.saturating_sub(system_tokens);
610
611 let mut selected = Vec::new();
612 let mut total = 0;
613 for msg in rest.iter().rev() {
614 let count = token_counter(msg);
615 if total + count > budget {
616 break;
617 }
618 total += count;
619 selected.push(msg.clone());
620 }
621 selected.reverse();
622
623 let mut result = Vec::new();
624 if let Some(sys) = system_msg {
625 result.push(sys);
626 }
627 result.extend(selected);
628 result
629 }
630 }
631}
632
633pub fn merge_message_runs(messages: Vec<Message>) -> Vec<Message> {
635 if messages.is_empty() {
636 return messages;
637 }
638
639 let mut result: Vec<Message> = Vec::new();
640
641 for msg in messages {
642 let should_merge = result
643 .last()
644 .map(|last| last.role() == msg.role())
645 .unwrap_or(false);
646
647 if should_merge {
648 let last = result.last_mut().unwrap();
649 let merged_content = format!("{}\n{}", last.content(), msg.content());
651 match last {
652 Message::System { content, .. } => *content = merged_content,
653 Message::Human { content, .. } => *content = merged_content,
654 Message::AI {
655 content,
656 tool_calls,
657 invalid_tool_calls,
658 ..
659 } => {
660 *content = merged_content;
661 tool_calls.extend(msg.tool_calls().to_vec());
662 invalid_tool_calls.extend(msg.invalid_tool_calls().to_vec());
663 }
664 Message::Tool { content, .. } => *content = merged_content,
665 Message::Chat { content, .. } => *content = merged_content,
666 Message::Remove { .. } => { }
667 }
668 } else {
669 result.push(msg);
670 }
671 }
672
673 result
674}
675
676pub fn get_buffer_string(messages: &[Message], human_prefix: &str, ai_prefix: &str) -> String {
678 messages
679 .iter()
680 .map(|msg| {
681 let prefix = match msg {
682 Message::System { .. } => "System",
683 Message::Human { .. } => human_prefix,
684 Message::AI { .. } => ai_prefix,
685 Message::Tool { .. } => "Tool",
686 Message::Chat { custom_role, .. } => custom_role.as_str(),
687 Message::Remove { .. } => "Remove",
688 };
689 format!("{prefix}: {}", msg.content())
690 })
691 .collect::<Vec<_>>()
692 .join("\n")
693}
694
695#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
701pub struct AIMessageChunk {
702 pub content: String,
703 #[serde(default, skip_serializing_if = "Vec::is_empty")]
704 pub tool_calls: Vec<ToolCall>,
705 #[serde(default, skip_serializing_if = "Option::is_none")]
706 pub usage: Option<TokenUsage>,
707 #[serde(default, skip_serializing_if = "Option::is_none")]
708 pub id: Option<String>,
709 #[serde(default, skip_serializing_if = "Vec::is_empty")]
710 pub tool_call_chunks: Vec<ToolCallChunk>,
711 #[serde(default, skip_serializing_if = "Vec::is_empty")]
712 pub invalid_tool_calls: Vec<InvalidToolCall>,
713}
714
715impl AIMessageChunk {
716 pub fn into_message(self) -> Message {
717 Message::ai_with_tool_calls(self.content, self.tool_calls)
718 }
719}
720
721impl std::ops::Add for AIMessageChunk {
722 type Output = Self;
723
724 fn add(mut self, rhs: Self) -> Self {
725 self += rhs;
726 self
727 }
728}
729
730impl std::ops::AddAssign for AIMessageChunk {
731 fn add_assign(&mut self, rhs: Self) {
732 self.content.push_str(&rhs.content);
733 self.tool_calls.extend(rhs.tool_calls);
734 self.tool_call_chunks.extend(rhs.tool_call_chunks);
735 self.invalid_tool_calls.extend(rhs.invalid_tool_calls);
736 if self.id.is_none() {
737 self.id = rhs.id;
738 }
739 match (&mut self.usage, rhs.usage) {
740 (Some(u), Some(rhs_u)) => {
741 u.input_tokens += rhs_u.input_tokens;
742 u.output_tokens += rhs_u.output_tokens;
743 u.total_tokens += rhs_u.total_tokens;
744 }
745 (None, Some(rhs_u)) => {
746 self.usage = Some(rhs_u);
747 }
748 _ => {}
749 }
750 }
751}
752
753#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
759pub struct ToolCall {
760 pub id: String,
761 pub name: String,
762 pub arguments: Value,
763}
764
765#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
767pub struct InvalidToolCall {
768 #[serde(default, skip_serializing_if = "Option::is_none")]
769 pub id: Option<String>,
770 #[serde(default, skip_serializing_if = "Option::is_none")]
771 pub name: Option<String>,
772 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub arguments: Option<String>,
774 pub error: String,
775}
776
777#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
779pub struct ToolCallChunk {
780 #[serde(default, skip_serializing_if = "Option::is_none")]
781 pub id: Option<String>,
782 #[serde(default, skip_serializing_if = "Option::is_none")]
783 pub name: Option<String>,
784 #[serde(default, skip_serializing_if = "Option::is_none")]
785 pub arguments: Option<String>,
786 #[serde(default, skip_serializing_if = "Option::is_none")]
787 pub index: Option<usize>,
788}
789
790#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
792pub struct ToolDefinition {
793 pub name: String,
794 pub description: String,
795 pub parameters: Value,
796 #[serde(default, skip_serializing_if = "Option::is_none")]
798 pub extras: Option<HashMap<String, Value>>,
799}
800
801#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
803#[serde(rename_all = "lowercase")]
804pub enum ToolChoice {
805 Auto,
806 Required,
807 None,
808 Specific(String),
809}
810
811#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
817pub struct ChatRequest {
818 pub messages: Vec<Message>,
819 #[serde(default, skip_serializing_if = "Vec::is_empty")]
820 pub tools: Vec<ToolDefinition>,
821 #[serde(default, skip_serializing_if = "Option::is_none")]
822 pub tool_choice: Option<ToolChoice>,
823}
824
825impl ChatRequest {
826 pub fn new(messages: Vec<Message>) -> Self {
827 Self {
828 messages,
829 tools: vec![],
830 tool_choice: None,
831 }
832 }
833
834 pub fn with_tools(mut self, tools: Vec<ToolDefinition>) -> Self {
835 self.tools = tools;
836 self
837 }
838
839 pub fn with_tool_choice(mut self, choice: ToolChoice) -> Self {
840 self.tool_choice = Some(choice);
841 self
842 }
843}
844
845#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
847pub struct ChatResponse {
848 pub message: Message,
849 pub usage: Option<TokenUsage>,
850}
851
852#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
857pub struct TokenUsage {
858 pub input_tokens: u32,
859 pub output_tokens: u32,
860 pub total_tokens: u32,
861 #[serde(default, skip_serializing_if = "Option::is_none")]
862 pub input_details: Option<InputTokenDetails>,
863 #[serde(default, skip_serializing_if = "Option::is_none")]
864 pub output_details: Option<OutputTokenDetails>,
865}
866
867#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
869pub struct InputTokenDetails {
870 #[serde(default)]
871 pub cached: u32,
872 #[serde(default)]
873 pub audio: u32,
874}
875
876#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
878pub struct OutputTokenDetails {
879 #[serde(default)]
880 pub reasoning: u32,
881 #[serde(default)]
882 pub audio: u32,
883}
884
885#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
891pub enum RunEvent {
892 RunStarted {
893 run_id: String,
894 session_id: String,
895 },
896 RunStep {
897 run_id: String,
898 step: usize,
899 },
900 LlmCalled {
901 run_id: String,
902 message_count: usize,
903 },
904 ToolCalled {
905 run_id: String,
906 tool_name: String,
907 },
908 RunFinished {
909 run_id: String,
910 output: String,
911 },
912 RunFailed {
913 run_id: String,
914 error: String,
915 },
916}
917
918#[derive(Debug, Error)]
924pub enum SynapticError {
925 #[error("prompt error: {0}")]
926 Prompt(String),
927 #[error("model error: {0}")]
928 Model(String),
929 #[error("tool error: {0}")]
930 Tool(String),
931 #[error("tool not found: {0}")]
932 ToolNotFound(String),
933 #[error("memory error: {0}")]
934 Memory(String),
935 #[error("rate limit: {0}")]
936 RateLimit(String),
937 #[error("timeout: {0}")]
938 Timeout(String),
939 #[error("validation error: {0}")]
940 Validation(String),
941 #[error("parsing error: {0}")]
942 Parsing(String),
943 #[error("callback error: {0}")]
944 Callback(String),
945 #[error("max steps exceeded: {max_steps}")]
946 MaxStepsExceeded { max_steps: usize },
947 #[error("embedding error: {0}")]
948 Embedding(String),
949 #[error("vector store error: {0}")]
950 VectorStore(String),
951 #[error("retriever error: {0}")]
952 Retriever(String),
953 #[error("loader error: {0}")]
954 Loader(String),
955 #[error("splitter error: {0}")]
956 Splitter(String),
957 #[error("graph error: {0}")]
958 Graph(String),
959 #[error("cache error: {0}")]
960 Cache(String),
961 #[error("store error: {0}")]
962 Store(String),
963 #[error("config error: {0}")]
964 Config(String),
965 #[error("mcp error: {0}")]
966 Mcp(String),
967}
968
969pub type ChatStream<'a> =
975 Pin<Box<dyn Stream<Item = Result<AIMessageChunk, SynapticError>> + Send + 'a>>;
976
977#[derive(Debug, Clone, Serialize, Deserialize)]
979pub struct ModelProfile {
980 pub name: String,
981 pub provider: String,
982 pub supports_tool_calling: bool,
983 pub supports_structured_output: bool,
984 pub supports_streaming: bool,
985 pub max_input_tokens: Option<usize>,
986 pub max_output_tokens: Option<usize>,
987}
988
989#[async_trait]
991pub trait ChatModel: Send + Sync {
992 async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, SynapticError>;
993
994 fn profile(&self) -> Option<ModelProfile> {
996 None
997 }
998
999 fn stream_chat(&self, request: ChatRequest) -> ChatStream<'_> {
1000 Box::pin(async_stream::stream! {
1001 match self.chat(request).await {
1002 Ok(response) => {
1003 yield Ok(AIMessageChunk {
1004 content: response.message.content().to_string(),
1005 tool_calls: response.message.tool_calls().to_vec(),
1006 usage: response.usage,
1007 ..Default::default()
1008 });
1009 }
1010 Err(e) => yield Err(e),
1011 }
1012 })
1013 }
1014}
1015
1016#[async_trait]
1018pub trait Tool: Send + Sync {
1019 fn name(&self) -> &'static str;
1020 fn description(&self) -> &'static str;
1021
1022 fn parameters(&self) -> Option<Value> {
1023 None
1024 }
1025
1026 async fn call(&self, args: Value) -> Result<Value, SynapticError>;
1027
1028 fn as_tool_definition(&self) -> ToolDefinition {
1029 ToolDefinition {
1030 name: self.name().to_string(),
1031 description: self.description().to_string(),
1032 parameters: self
1033 .parameters()
1034 .unwrap_or(serde_json::json!({"type": "object", "properties": {}})),
1035 extras: None,
1036 }
1037 }
1038}
1039
1040#[derive(Debug, Clone, Default)]
1049pub struct ToolContext {
1050 pub state: Option<Value>,
1052 pub tool_call_id: String,
1054}
1055
1056#[async_trait]
1062pub trait ContextAwareTool: Send + Sync {
1063 fn name(&self) -> &'static str;
1064 fn description(&self) -> &'static str;
1065 async fn call_with_context(
1066 &self,
1067 args: Value,
1068 ctx: ToolContext,
1069 ) -> Result<Value, SynapticError>;
1070}
1071
1072pub struct ContextAwareToolAdapter {
1077 inner: Arc<dyn ContextAwareTool>,
1078}
1079
1080impl ContextAwareToolAdapter {
1081 pub fn new(inner: Arc<dyn ContextAwareTool>) -> Self {
1082 Self { inner }
1083 }
1084}
1085
1086#[async_trait]
1087impl Tool for ContextAwareToolAdapter {
1088 fn name(&self) -> &'static str {
1089 self.inner.name()
1090 }
1091
1092 fn description(&self) -> &'static str {
1093 self.inner.description()
1094 }
1095
1096 async fn call(&self, args: Value) -> Result<Value, SynapticError> {
1097 self.inner
1098 .call_with_context(args, ToolContext::default())
1099 .await
1100 }
1101}
1102
1103#[async_trait]
1109pub trait MemoryStore: Send + Sync {
1110 async fn append(&self, session_id: &str, message: Message) -> Result<(), SynapticError>;
1111 async fn load(&self, session_id: &str) -> Result<Vec<Message>, SynapticError>;
1112 async fn clear(&self, session_id: &str) -> Result<(), SynapticError>;
1113}
1114
1115#[async_trait]
1117pub trait CallbackHandler: Send + Sync {
1118 async fn on_event(&self, event: RunEvent) -> Result<(), SynapticError>;
1119}
1120
1121#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1127pub struct RunnableConfig {
1128 #[serde(default)]
1129 pub tags: Vec<String>,
1130 #[serde(default)]
1131 pub metadata: HashMap<String, Value>,
1132 #[serde(default)]
1133 pub max_concurrency: Option<usize>,
1134 #[serde(default)]
1135 pub recursion_limit: Option<usize>,
1136 #[serde(default)]
1137 pub run_id: Option<String>,
1138 #[serde(default)]
1139 pub run_name: Option<String>,
1140}
1141
1142impl RunnableConfig {
1143 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
1144 self.tags = tags;
1145 self
1146 }
1147
1148 pub fn with_run_name(mut self, name: impl Into<String>) -> Self {
1149 self.run_name = Some(name.into());
1150 self
1151 }
1152
1153 pub fn with_run_id(mut self, id: impl Into<String>) -> Self {
1154 self.run_id = Some(id.into());
1155 self
1156 }
1157
1158 pub fn with_max_concurrency(mut self, max: usize) -> Self {
1159 self.max_concurrency = Some(max);
1160 self
1161 }
1162
1163 pub fn with_recursion_limit(mut self, limit: usize) -> Self {
1164 self.recursion_limit = Some(limit);
1165 self
1166 }
1167
1168 pub fn with_metadata(mut self, key: impl Into<String>, value: Value) -> Self {
1169 self.metadata.insert(key.into(), value);
1170 self
1171 }
1172}
1173
1174#[derive(Debug, Clone, Serialize, Deserialize)]
1180pub struct Item {
1181 pub namespace: Vec<String>,
1182 pub key: String,
1183 pub value: Value,
1184 pub created_at: String,
1185 pub updated_at: String,
1186 #[serde(default, skip_serializing_if = "Option::is_none")]
1188 pub score: Option<f64>,
1189}
1190
1191#[async_trait]
1196pub trait Store: Send + Sync {
1197 async fn get(&self, namespace: &[&str], key: &str) -> Result<Option<Item>, SynapticError>;
1199
1200 async fn search(
1202 &self,
1203 namespace: &[&str],
1204 query: Option<&str>,
1205 limit: usize,
1206 ) -> Result<Vec<Item>, SynapticError>;
1207
1208 async fn put(&self, namespace: &[&str], key: &str, value: Value) -> Result<(), SynapticError>;
1210
1211 async fn delete(&self, namespace: &[&str], key: &str) -> Result<(), SynapticError>;
1213
1214 async fn list_namespaces(&self, prefix: &[&str]) -> Result<Vec<Vec<String>>, SynapticError>;
1216}
1217
1218#[async_trait]
1224pub trait Embeddings: Send + Sync {
1225 async fn embed_documents(&self, texts: &[&str]) -> Result<Vec<Vec<f32>>, SynapticError>;
1227
1228 async fn embed_query(&self, text: &str) -> Result<Vec<f32>, SynapticError>;
1230}
1231
1232pub type StreamWriter = Arc<dyn Fn(Value) + Send + Sync>;
1238
1239#[derive(Clone)]
1245pub struct Runtime {
1246 pub store: Option<Arc<dyn Store>>,
1247 pub stream_writer: Option<StreamWriter>,
1248}
1249
1250#[derive(Clone)]
1252pub struct ToolRuntime {
1253 pub store: Option<Arc<dyn Store>>,
1254 pub stream_writer: Option<StreamWriter>,
1255 pub state: Option<Value>,
1256 pub tool_call_id: String,
1257 pub config: Option<RunnableConfig>,
1258}
1259
1260#[async_trait]
1270pub trait RuntimeAwareTool: Send + Sync {
1271 fn name(&self) -> &'static str;
1272 fn description(&self) -> &'static str;
1273
1274 fn parameters(&self) -> Option<Value> {
1275 None
1276 }
1277
1278 async fn call_with_runtime(
1279 &self,
1280 args: Value,
1281 runtime: ToolRuntime,
1282 ) -> Result<Value, SynapticError>;
1283
1284 fn as_tool_definition(&self) -> ToolDefinition {
1285 ToolDefinition {
1286 name: self.name().to_string(),
1287 description: self.description().to_string(),
1288 parameters: self
1289 .parameters()
1290 .unwrap_or(serde_json::json!({"type": "object", "properties": {}})),
1291 extras: None,
1292 }
1293 }
1294}
1295
1296pub struct RuntimeAwareToolAdapter {
1301 inner: Arc<dyn RuntimeAwareTool>,
1302 runtime: Arc<tokio::sync::RwLock<Option<ToolRuntime>>>,
1303}
1304
1305impl RuntimeAwareToolAdapter {
1306 pub fn new(tool: Arc<dyn RuntimeAwareTool>) -> Self {
1307 Self {
1308 inner: tool,
1309 runtime: Arc::new(tokio::sync::RwLock::new(None)),
1310 }
1311 }
1312
1313 pub async fn set_runtime(&self, runtime: ToolRuntime) {
1314 *self.runtime.write().await = Some(runtime);
1315 }
1316}
1317
1318#[async_trait]
1319impl Tool for RuntimeAwareToolAdapter {
1320 fn name(&self) -> &'static str {
1321 self.inner.name()
1322 }
1323
1324 fn description(&self) -> &'static str {
1325 self.inner.description()
1326 }
1327
1328 fn parameters(&self) -> Option<Value> {
1329 self.inner.parameters()
1330 }
1331
1332 async fn call(&self, args: Value) -> Result<Value, SynapticError> {
1333 let runtime = self.runtime.read().await.clone().unwrap_or(ToolRuntime {
1334 store: None,
1335 stream_writer: None,
1336 state: None,
1337 tool_call_id: String::new(),
1338 config: None,
1339 });
1340 self.inner.call_with_runtime(args, runtime).await
1341 }
1342}
1343
1344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1350pub struct Document {
1351 pub id: String,
1352 pub content: String,
1353 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1354 pub metadata: HashMap<String, Value>,
1355}
1356
1357impl Document {
1358 pub fn new(id: impl Into<String>, content: impl Into<String>) -> Self {
1359 Self {
1360 id: id.into(),
1361 content: content.into(),
1362 metadata: HashMap::new(),
1363 }
1364 }
1365
1366 pub fn with_metadata(
1367 id: impl Into<String>,
1368 content: impl Into<String>,
1369 metadata: HashMap<String, Value>,
1370 ) -> Self {
1371 Self {
1372 id: id.into(),
1373 content: content.into(),
1374 metadata,
1375 }
1376 }
1377}
1378
1379#[async_trait]
1385pub trait Retriever: Send + Sync {
1386 async fn retrieve(&self, query: &str, top_k: usize) -> Result<Vec<Document>, SynapticError>;
1387}
1388
1389#[async_trait]
1395pub trait VectorStore: Send + Sync {
1396 async fn add_documents(
1398 &self,
1399 docs: Vec<Document>,
1400 embeddings: &dyn Embeddings,
1401 ) -> Result<Vec<String>, SynapticError>;
1402
1403 async fn similarity_search(
1405 &self,
1406 query: &str,
1407 k: usize,
1408 embeddings: &dyn Embeddings,
1409 ) -> Result<Vec<Document>, SynapticError>;
1410
1411 async fn similarity_search_with_score(
1413 &self,
1414 query: &str,
1415 k: usize,
1416 embeddings: &dyn Embeddings,
1417 ) -> Result<Vec<(Document, f32)>, SynapticError>;
1418
1419 async fn similarity_search_by_vector(
1421 &self,
1422 embedding: &[f32],
1423 k: usize,
1424 ) -> Result<Vec<Document>, SynapticError>;
1425
1426 async fn delete(&self, ids: &[&str]) -> Result<(), SynapticError>;
1428}
1429
1430#[async_trait]
1436pub trait Loader: Send + Sync {
1437 async fn load(&self) -> Result<Vec<Document>, SynapticError>;
1439
1440 fn lazy_load(
1442 &self,
1443 ) -> Pin<Box<dyn Stream<Item = Result<Document, SynapticError>> + Send + '_>> {
1444 Box::pin(async_stream::stream! {
1445 match self.load().await {
1446 Ok(docs) => {
1447 for doc in docs {
1448 yield Ok(doc);
1449 }
1450 }
1451 Err(e) => yield Err(e),
1452 }
1453 })
1454 }
1455}
1456
1457#[async_trait]
1463pub trait LlmCache: Send + Sync {
1464 async fn get(&self, key: &str) -> Result<Option<ChatResponse>, SynapticError>;
1466 async fn put(&self, key: &str, response: &ChatResponse) -> Result<(), SynapticError>;
1468 async fn clear(&self) -> Result<(), SynapticError>;
1470}
1471
1472#[derive(Debug, Clone)]
1478pub struct EntrypointConfig {
1479 pub name: &'static str,
1480 pub checkpointer: Option<&'static str>,
1481}
1482
1483pub type EntrypointFn = dyn Fn(Value) -> Pin<Box<dyn Future<Output = Result<Value, SynapticError>> + Send>>
1488 + Send
1489 + Sync;
1490
1491pub struct Entrypoint {
1492 pub config: EntrypointConfig,
1493 pub invoke_fn: Box<EntrypointFn>,
1494}
1495
1496impl Entrypoint {
1497 pub async fn invoke(&self, input: Value) -> Result<Value, SynapticError> {
1498 (self.invoke_fn)(input).await
1499 }
1500}