1pub mod assistant;
79pub mod event;
81pub mod failure;
83pub mod message;
85pub mod model;
87pub mod provider;
89pub mod reasoning;
91pub mod researcher;
93pub mod tool;
95
96use crate::llm::{model::Parameters, tool::Tools};
97use alloc::{
98 boxed::Box,
99 string::{String, ToString},
100 sync::Arc,
101 vec,
102 vec::Vec,
103};
104use core::{any::TypeId, future::Future};
105pub use event::{Event, ToolCall, Usage};
106pub use failure::{Classify, Failure, FailureKind};
107use futures_core::Stream;
108use futures_lite::{StreamExt, pin};
109pub use message::{Attachment, Message, Role};
110pub use provider::LanguageModelProvider;
111pub use reasoning::ReasoningState;
112pub use researcher::{
113 ResearchCitation, ResearchEvent, ResearchFinding, ResearchOptions, ResearchReport,
114 ResearchRequest, ResearchSource, ResearchStage, Researcher, ResearcherProfile,
115};
116use schemars::{JsonSchema, schema_for};
117use serde::de::DeserializeOwned;
118pub use tool::{
119 AgentIdentity, CallIdentity, CommandOutcome, CommandResult, CommandStream, Interruption,
120 InterruptionCause, IntoToolResult, StoredFile, SubagentIdentity, Tool, ToolCallId, ToolContext,
121 ToolResult, render_interruptions,
122};
123
124use crate::llm::model::Profile;
125
126#[derive(Debug)]
135pub enum GenerateError<E> {
136 Provider(E),
138
139 Parse {
143 source: serde_json::Error,
145 response: String,
147 },
148}
149
150impl<E: core::fmt::Display> core::fmt::Display for GenerateError<E> {
151 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
152 match self {
153 Self::Provider(err) => write!(f, "language model request failed: {err}"),
154 Self::Parse { source, response } => {
155 write!(
156 f,
157 "structured output did not match the requested schema: {source}; response: {response}"
158 )
159 }
160 }
161 }
162}
163
164impl<E: core::error::Error + 'static> core::error::Error for GenerateError<E> {
165 fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
166 match self {
167 Self::Provider(err) => Some(err),
168 Self::Parse { source, .. } => Some(source),
169 }
170 }
171}
172
173#[derive(Debug, Clone)]
178pub struct LLMRequest {
179 messages: Vec<Message>,
180 parameters: Parameters,
181 tool_definitions: Vec<tool::ToolDefinition>,
182}
183
184impl LLMRequest {
185 pub fn new(messages: impl Into<Vec<Message>>) -> Self {
187 Self {
188 messages: messages.into(),
189 parameters: Parameters::default(),
190 tool_definitions: Vec::new(),
191 }
192 }
193
194 #[must_use]
199 pub fn with_tool_definitions(mut self, definitions: Vec<tool::ToolDefinition>) -> Self {
200 self.tool_definitions = definitions;
201 self
202 }
203
204 #[must_use]
206 pub fn with_tool<T: Tool>(mut self, tool: &T) -> Self {
207 self.tool_definitions.push(tool::ToolDefinition::new(tool));
208 self
209 }
210
211 #[must_use]
213 pub fn with_parameters(mut self, parameters: Parameters) -> Self {
214 self.parameters = parameters;
215 self
216 }
217
218 #[must_use]
220 pub fn messages(&self) -> &[Message] {
221 &self.messages
222 }
223
224 pub const fn messages_mut(&mut self) -> &mut Vec<Message> {
226 &mut self.messages
227 }
228
229 #[must_use]
231 pub const fn parameters(&self) -> &Parameters {
232 &self.parameters
233 }
234
235 #[must_use]
237 pub fn tool_definitions(&self) -> &[tool::ToolDefinition] {
238 &self.tool_definitions
239 }
240
241 #[must_use]
243 pub fn into_parts(self) -> (Vec<Message>, Parameters, Vec<tool::ToolDefinition>) {
244 (self.messages, self.parameters, self.tool_definitions)
245 }
246}
247
248#[derive(Debug)]
253pub struct LLMRequestWithTools<'tools> {
254 inner: LLMRequest,
255 tools: &'tools mut Tools,
256}
257
258impl LLMRequest {
259 pub fn with_tools(self, tools: &mut Tools) -> LLMRequestWithTools<'_> {
264 let definitions = tools.definitions();
265 LLMRequestWithTools {
266 inner: self.with_tool_definitions(definitions),
267 tools,
268 }
269 }
270}
271
272impl<'tools> LLMRequestWithTools<'tools> {
273 #[must_use]
275 pub const fn request(&self) -> &LLMRequest {
276 &self.inner
277 }
278
279 #[must_use]
281 pub const fn tools(&mut self) -> &mut Tools {
282 self.tools
283 }
284
285 #[must_use]
287 pub fn into_parts(self) -> (LLMRequest, &'tools mut Tools) {
288 (self.inner, self.tools)
289 }
290
291 pub async fn call_tool(
297 &mut self,
298 name: &str,
299 args_json: &str,
300 cx: ToolContext,
301 ) -> crate::Result<ToolResult> {
302 self.tools.call(name, args_json, cx).await
303 }
304}
305
306pub trait LanguageModel: Sized + Send + Sync {
314 type Error: core::error::Error + Classify + Send + Sync + 'static;
316
317 fn respond(&self, request: LLMRequest)
325 -> impl Stream<Item = Result<Event, Self::Error>> + Send;
326
327 fn respond_with_tools(
332 &self,
333 request: LLMRequestWithTools<'_>,
334 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
335 let (inner, _tools) = request.into_parts();
336 self.respond(inner)
337 }
338
339 fn generate<T: JsonSchema + DeserializeOwned + 'static>(
346 &self,
347 request: LLMRequest,
348 ) -> impl Future<Output = Result<T, GenerateError<Self::Error>>> + Send {
349 async { structured_generate(self, request).await }
350 }
351
352 fn complete(&self, prefix: &str) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
354 self.respond(oneshot("Please complete the following text:", prefix))
355 }
356
357 fn summarize(&self, text: &str) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
363 summarize(self, text)
364 }
365
366 fn categorize<T: JsonSchema + DeserializeOwned + 'static>(
372 &self,
373 text: &str,
374 ) -> impl Future<Output = Result<T, GenerateError<Self::Error>>> + Send {
375 async { categorize_text(self, text).await }
376 }
377
378 fn profile(&self) -> impl Future<Output = Result<Profile, Self::Error>> + Send;
388}
389
390macro_rules! impl_language_model {
391 ($($name:ident),*) => {
392 $(
393 impl<T: LanguageModel> LanguageModel for $name<T> {
394 type Error = T::Error;
395
396 fn respond(
397 &self,
398 request: LLMRequest,
399 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
400 T::respond(self, request)
401 }
402
403 fn respond_with_tools(
404 &self,
405 request: LLMRequestWithTools<'_>,
406 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
407 T::respond_with_tools(self, request)
408 }
409
410 fn generate<U: JsonSchema + DeserializeOwned + 'static>(
411 &self,
412 request: LLMRequest,
413 ) -> impl Future<Output = Result<U, GenerateError<Self::Error>>> + Send {
414 T::generate(self, request)
415 }
416
417 fn complete(
418 &self,
419 prefix: &str,
420 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
421 T::complete(self, prefix)
422 }
423
424 fn summarize(
425 &self,
426 text: &str,
427 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
428 T::summarize(self, text)
429 }
430
431 fn categorize<U: JsonSchema + DeserializeOwned + 'static>(
432 &self,
433 text: &str,
434 ) -> impl Future<Output = Result<U, GenerateError<Self::Error>>> + Send {
435 T::categorize(self, text)
436 }
437
438 fn profile(&self) -> impl Future<Output = Result<Profile, Self::Error>> + Send {
439 T::profile(self)
440 }
441 }
442 )*
443 };
444}
445
446impl<T: LanguageModel> LanguageModel for &T {
447 type Error = T::Error;
448
449 fn respond(
450 &self,
451 request: LLMRequest,
452 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
453 T::respond(self, request)
454 }
455
456 fn respond_with_tools(
457 &self,
458 request: LLMRequestWithTools<'_>,
459 ) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
460 T::respond_with_tools(self, request)
461 }
462
463 fn generate<U: JsonSchema + DeserializeOwned + 'static>(
464 &self,
465 request: LLMRequest,
466 ) -> impl Future<Output = Result<U, GenerateError<Self::Error>>> + Send {
467 T::generate(self, request)
468 }
469
470 fn complete(&self, prefix: &str) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
471 T::complete(self, prefix)
472 }
473
474 fn summarize(&self, text: &str) -> impl Stream<Item = Result<Event, Self::Error>> + Send {
475 T::summarize(self, text)
476 }
477
478 fn categorize<U: JsonSchema + DeserializeOwned + 'static>(
479 &self,
480 text: &str,
481 ) -> impl Future<Output = Result<U, GenerateError<Self::Error>>> + Send {
482 T::categorize(self, text)
483 }
484
485 fn profile(&self) -> impl Future<Output = Result<Profile, Self::Error>> + Send {
486 T::profile(self)
487 }
488}
489
490mod prompts;
491
492impl_language_model!(Arc, Box);
493
494pub async fn collect_text<S, E>(stream: S) -> Result<String, E>
500where
501 S: Stream<Item = Result<Event, E>>,
502{
503 pin!(stream);
504 let mut result = String::new();
505 while let Some(event) = stream.next().await {
506 if let Event::Text(text) = event? {
507 result.push_str(&text);
508 }
509 }
510 Ok(result)
511}
512
513async fn structured_generate<T: JsonSchema + DeserializeOwned + 'static, M: LanguageModel>(
514 model: &M,
515 mut request: LLMRequest,
516) -> Result<T, GenerateError<M::Error>> {
517 let schema = schema_for!(T);
518
519 let json = if schema.as_value().is_string() {
521 let stream = model.respond(request);
522 let response = collect_text(stream)
523 .await
524 .map_err(GenerateError::Provider)?;
525 serde_json::to_string(&response).map_err(|source| GenerateError::Parse {
527 source,
528 response: response.clone(),
529 })?
530 } else {
531 let schema =
534 serde_json::to_string_pretty(&schema).map_err(|source| GenerateError::Parse {
535 source,
536 response: String::new(),
537 })?;
538 let prompt = prompts::generate(&schema);
539 request.messages.push(Message::system(prompt));
540 request.parameters.structured_outputs = true;
541
542 let stream = model.respond(request);
543 collect_text(stream)
544 .await
545 .map_err(GenerateError::Provider)?
546 };
547
548 parse_json_with_recovery(&json).map_err(|source| GenerateError::Parse {
549 source,
550 response: truncate_for_error(&json),
551 })
552}
553
554fn truncate_for_error(response: &str) -> String {
556 const LIMIT: usize = 500;
557 response.chars().take(LIMIT).collect()
558}
559
560pub fn oneshot(system: impl Into<String>, user: impl Into<String>) -> LLMRequest {
562 let messages = vec![Message::system(system.into()), Message::user(user.into())];
563 LLMRequest::new(messages)
564}
565
566fn summarize<M: LanguageModel>(
567 model: &M,
568 text: &str,
569) -> impl Stream<Item = Result<Event, M::Error>> + Send {
570 let messages = oneshot("Summarize text:", text);
571 model.respond(messages)
572}
573
574async fn categorize_text<T: JsonSchema + DeserializeOwned + 'static, M: LanguageModel>(
575 model: &M,
576 text: &str,
577) -> Result<T, GenerateError<M::Error>> {
578 let request = oneshot("Categorize text by provided schema", text);
579 model.generate(request).await
580}
581
582fn parse_json_with_recovery<T: DeserializeOwned + 'static>(
583 json: &str,
584) -> Result<T, serde_json::Error> {
585 use serde::de::Error as _;
586
587 let trimmed = json.trim();
588 let mut last_error: Option<serde_json::Error> = None;
589 let mut last_candidate: Option<String> = None;
590
591 for candidate in build_json_candidates(trimmed) {
592 match serde_json::from_str::<T>(&candidate) {
593 Ok(value) => return Ok(value),
594 Err(err) => {
595 last_error = Some(err);
596 last_candidate = Some(candidate);
597 }
598 }
599 }
600
601 if is_string_type::<T>()
604 && let Some(candidate) = last_candidate
605 && let Ok(value) = serde_json::from_str::<serde_json::Value>(&candidate)
606 {
607 let text = match value {
608 serde_json::Value::String(s) => s,
609 other => other.to_string(),
610 };
611 let encoded = serde_json::to_string(&text)?;
612 if let Ok(value) = serde_json::from_str::<T>(&encoded) {
613 return Ok(value);
614 }
615 }
616
617 Err(last_error.unwrap_or_else(|| {
618 serde_json::Error::custom("structured output was empty or missing a JSON block")
619 }))
620}
621
622fn strip_code_fences(raw: &str) -> Option<String> {
623 let trimmed = raw.trim();
624 let fence_start = trimmed.find("```")?;
625 let after_fence = &trimmed[fence_start + 3..];
626 let mut lines = after_fence.lines();
627 let _maybe_lang = lines.next();
628 let body = lines.collect::<Vec<_>>().join("\n");
629 let content = body.rfind("```").map_or(body.as_str(), |end| &body[..end]);
630
631 let cleaned = content.trim();
632 if cleaned.is_empty() {
633 None
634 } else {
635 Some(cleaned.to_string())
636 }
637}
638
639fn extract_json_block(raw: &str) -> Option<String> {
640 if let (Some(start), Some(end)) = (raw.find('{'), raw.rfind('}'))
641 && end >= start
642 {
643 let candidate = &raw[start..=end];
644 if !candidate.trim().is_empty() {
645 return Some(candidate.trim().to_string());
646 }
647 }
648 if let (Some(start), Some(end)) = (raw.find('['), raw.rfind(']'))
649 && end >= start
650 {
651 let candidate = &raw[start..=end];
652 if !candidate.trim().is_empty() {
653 return Some(candidate.trim().to_string());
654 }
655 }
656 None
657}
658
659fn build_json_candidates(raw: &str) -> Vec<String> {
660 let mut candidates = Vec::new();
661
662 if !raw.is_empty() {
663 candidates.push(raw.to_string());
664 }
665
666 if let Some(fenced) = strip_code_fences(raw) {
667 candidates.push(fenced);
668 }
669
670 if let Some(block) = extract_json_block(raw) {
671 candidates.push(block);
672 }
673
674 if let Some(dequoted) = dequote_json_string(raw) {
675 candidates.push(dequoted);
676 }
677
678 if let Some(stripped) = strip_leading_label(raw, "json") {
679 candidates.push(stripped);
680 }
681
682 let mut deduped = Vec::new();
683 for candidate in candidates {
684 if deduped.iter().all(|seen| seen != &candidate) {
685 deduped.push(candidate);
686 }
687 }
688 deduped
689}
690
691fn dequote_json_string(raw: &str) -> Option<String> {
692 let trimmed = raw.trim();
693 if !(trimmed.starts_with('"') && trimmed.ends_with('"')) {
694 return None;
695 }
696 let inner: String = serde_json::from_str(trimmed).ok()?;
697 if inner.trim().is_empty() {
698 None
699 } else {
700 Some(inner)
701 }
702}
703
704fn strip_leading_label(raw: &str, label: &str) -> Option<String> {
705 let trimmed = raw.trim_start();
706 if !trimmed.to_ascii_lowercase().starts_with(label) {
707 return None;
708 }
709 let stripped = trimmed[label.len()..]
710 .trim_start_matches(|c: char| c.is_whitespace() || c == ':' || c == '-')
711 .trim();
712 if stripped.is_empty() {
713 None
714 } else {
715 Some(stripped.to_string())
716 }
717}
718
719fn is_string_type<T: 'static>() -> bool {
720 TypeId::of::<T>() == TypeId::of::<String>()
721}
722
723#[cfg(test)]
724mod tests {
725 use super::parse_json_with_recovery;
726 use alloc::string::String;
727 use serde::Deserialize;
728
729 #[derive(Debug, Deserialize, PartialEq, Eq)]
730 struct Foo {
731 a: u8,
732 }
733
734 #[test]
735 fn parses_plain_json() {
736 let foo: Foo = parse_json_with_recovery(r#"{"a":1}"#).unwrap();
737 assert_eq!(foo, Foo { a: 1 });
738 }
739
740 #[test]
741 fn parses_code_fence_json() {
742 let foo: Foo = parse_json_with_recovery("```json\n{\"a\":2}\n```").unwrap();
743 assert_eq!(foo, Foo { a: 2 });
744 }
745
746 #[test]
747 fn parses_embedded_block() {
748 let foo: Foo = parse_json_with_recovery("noise {\"a\":3} trailing").unwrap();
749 assert_eq!(foo, Foo { a: 3 });
750 }
751
752 #[test]
753 fn parses_quoted_json_string() {
754 let foo: Foo = parse_json_with_recovery(r#""{\"a\":4}""#).unwrap();
755 assert_eq!(foo, Foo { a: 4 });
756 }
757
758 #[test]
759 fn parses_labeled_json() {
760 let foo: Foo = parse_json_with_recovery("json {\"a\":5}").unwrap();
761 assert_eq!(foo, Foo { a: 5 });
762 }
763
764 #[test]
765 fn coerces_object_to_string() {
766 let value: String =
767 parse_json_with_recovery(r#"{"title":"summary","type":"content"}"#).unwrap();
768 assert!(
769 value.contains("\"title\":\"summary\"") && value.contains("\"type\":\"content\""),
770 "unexpected value: {value}"
771 );
772 }
773}