1use std::collections::HashMap;
2use std::time::{SystemTime, UNIX_EPOCH};
3
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use crate::agentic_coding::command_reroute::plan_symbolic_command_reroute;
8use crate::agentic_coding::narration::tool_action_narration;
9use crate::agentic_coding::planner::{plan_chat_step, AgenticPlan};
10use crate::dreaming_application::{
11 amended_answer, apply_retained_amendments, solve_with_standing_requirements,
12};
13use crate::engine::{
14 estimate_tokens, render_thinking_steps, stable_id, FormalAiEngine, SymbolicAnswer, ThinkingStep,
15};
16use crate::memory::MemoryEvent;
17use crate::protocol_memory::answer_from_memory_if_requested;
18use crate::protocol_policy::{
19 agentic_tool_permission_denial, is_hosted_tool_definition, is_tool_choice_request,
20 matches_tool_choice_none, response_tool_call_identity, tool_call_refusal_answer,
21 tool_choice_function_name, tool_definition_names, tool_permission_refusal_answer,
22};
23use crate::protocol_responses::response_arguments_for_tool;
24use crate::solver::UniversalSolver;
25
26mod content;
27mod output;
28mod recording;
29pub use output::*;
30pub use recording::{
31 chat_exchange_to_record, chat_tool_executions, messages_exchange_to_record,
32 responses_exchange_to_record,
33};
34use recording::{chat_prompt_and_history, response_prompt, value_to_prompt_text};
35
36fn resolved_request_model(model: Option<&str>) -> String {
37 crate::seed::resolve_model_id(model)
38}
39
40fn response_timestamp() -> u64 {
41 SystemTime::now()
42 .duration_since(UNIX_EPOCH)
43 .map_or(1, |duration| duration.as_secs().max(1))
44}
45
46#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47pub struct ChatCompletionRequest {
48 #[serde(default)]
49 pub model: Option<String>,
50 #[serde(default)]
51 pub messages: Vec<ChatMessage>,
52 #[serde(default)]
53 pub temperature: Option<f32>,
54 #[serde(default)]
55 pub stream: bool,
56 #[serde(default, skip_serializing_if = "Vec::is_empty")]
57 pub tools: Vec<Value>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub tool_choice: Option<Value>,
60 #[serde(default, skip_serializing_if = "Vec::is_empty")]
61 pub functions: Vec<Value>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub function_call: Option<Value>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub stream_options: Option<StreamOptions>,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
74pub struct StreamOptions {
75 #[serde(default)]
76 pub include_usage: bool,
77}
78
79impl ChatCompletionRequest {
80 #[must_use]
81 pub fn requests_tool_execution(&self) -> bool {
82 if self
83 .tool_choice
84 .as_ref()
85 .is_some_and(is_tool_choice_request)
86 || self
87 .function_call
88 .as_ref()
89 .is_some_and(is_tool_choice_request)
90 {
91 return true;
92 }
93
94 let tool_calls_disabled = self
95 .tool_choice
96 .as_ref()
97 .is_some_and(matches_tool_choice_none);
98 let function_calls_disabled = self
99 .function_call
100 .as_ref()
101 .is_some_and(matches_tool_choice_none);
102
103 (!self.tools.is_empty() && !tool_calls_disabled)
104 || (!self.functions.is_empty() && !function_calls_disabled)
105 }
106
107 fn requested_tool_names(&self) -> Vec<String> {
108 let mut names = Vec::new();
109 if let Some(name) = self
110 .tool_choice
111 .as_ref()
112 .and_then(tool_choice_function_name)
113 {
114 names.push(name);
115 }
116 if let Some(name) = self
117 .function_call
118 .as_ref()
119 .and_then(tool_choice_function_name)
120 {
121 names.push(name);
122 }
123 if !self
124 .tool_choice
125 .as_ref()
126 .is_some_and(matches_tool_choice_none)
127 {
128 names.extend(self.tools.iter().flat_map(tool_definition_names));
129 }
130 if !self
131 .function_call
132 .as_ref()
133 .is_some_and(matches_tool_choice_none)
134 {
135 names.extend(self.functions.iter().flat_map(tool_definition_names));
136 }
137 names.sort();
138 names.dedup();
139 names
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
144pub struct ChatMessage {
145 pub role: String,
146 #[serde(default, deserialize_with = "deserialize_null_content")]
147 pub content: MessageContent,
148 #[serde(default, skip_serializing_if = "Vec::is_empty")]
150 pub tool_calls: Vec<ToolCall>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub tool_call_id: Option<String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub name: Option<String>,
157 #[serde(default, skip_serializing_if = "Vec::is_empty")]
159 pub thinking_steps: Vec<ThinkingStep>,
160 #[serde(default, skip_serializing_if = "String::is_empty")]
163 pub reasoning_content: String,
164 #[serde(default, skip_serializing_if = "String::is_empty")]
167 pub reasoning: String,
168}
169
170impl ChatMessage {
171 #[must_use]
173 pub fn new(role: impl Into<String>, content: impl Into<String>) -> Self {
174 Self {
175 role: role.into(),
176 content: MessageContent::Text(content.into()),
177 ..Self::default()
178 }
179 }
180
181 #[must_use]
183 pub fn user(text: impl Into<String>) -> Self {
184 Self::new("user", text)
185 }
186
187 #[must_use]
189 pub fn assistant(text: impl Into<String>) -> Self {
190 Self::new("assistant", text)
191 }
192
193 #[must_use]
195 pub fn assistant_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
196 Self {
197 role: String::from("assistant"),
198 tool_calls,
199 ..Self::default()
200 }
201 }
202
203 #[must_use]
209 pub fn assistant_tool_calls_with_content(
210 content: impl Into<String>,
211 tool_calls: Vec<ToolCall>,
212 ) -> Self {
213 Self {
214 role: String::from("assistant"),
215 content: MessageContent::Text(content.into()),
216 tool_calls,
217 ..Self::default()
218 }
219 }
220
221 #[must_use]
224 pub fn tool_result(
225 tool_call_id: impl Into<String>,
226 name: impl Into<String>,
227 result: impl Into<String>,
228 ) -> Self {
229 Self {
230 role: String::from("tool"),
231 content: MessageContent::Text(result.into()),
232 tool_call_id: Some(tool_call_id.into()),
233 name: Some(name.into()),
234 ..Self::default()
235 }
236 }
237}
238
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240#[serde(untagged)]
241pub enum MessageContent {
242 Text(String),
243 Parts(Vec<MessageContentPart>),
244}
245
246impl Default for MessageContent {
247 fn default() -> Self {
248 Self::Text(String::new())
249 }
250}
251
252fn message_content_tokens(content: &MessageContent) -> u32 {
253 match content {
254 MessageContent::Text(text) => estimate_tokens(text),
255 MessageContent::Parts(parts) => parts.iter().fold(0, |total, part| {
256 total.saturating_add(part.text.as_deref().map_or(0, estimate_tokens))
257 }),
258 }
259}
260
261fn message_input_tokens(messages: &[ChatMessage]) -> u32 {
265 messages.iter().fold(0, |total, message| {
266 total.saturating_add(message_content_tokens(&message.content))
267 })
268}
269
270fn deserialize_null_content<'de, D>(deserializer: D) -> Result<MessageContent, D::Error>
281where
282 D: serde::Deserializer<'de>,
283{
284 Ok(Option::<MessageContent>::deserialize(deserializer)?.unwrap_or_default())
285}
286
287#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
288pub struct MessageContentPart {
289 #[serde(rename = "type")]
290 pub kind: String,
291 #[serde(default)]
292 pub text: Option<String>,
293}
294
295#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
297pub struct ToolCall {
298 pub id: String,
299 #[serde(rename = "type", default = "tool_call_kind")]
300 pub kind: String,
301 pub function: FunctionCall,
302}
303
304fn tool_call_kind() -> String {
305 String::from("function")
306}
307
308impl ToolCall {
309 #[must_use]
312 pub fn function(
313 id: impl Into<String>,
314 name: impl Into<String>,
315 arguments: impl Into<String>,
316 ) -> Self {
317 Self {
318 id: id.into(),
319 kind: tool_call_kind(),
320 function: FunctionCall {
321 name: name.into(),
322 arguments: arguments.into(),
323 },
324 }
325 }
326}
327
328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
330pub struct FunctionCall {
331 pub name: String,
332 pub arguments: String,
333}
334
335#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
336pub struct ChatCompletion {
337 pub id: String,
338 pub object: String,
339 pub created: u64,
340 pub model: String,
341 pub choices: Vec<ChatChoice>,
342 pub usage: TokenUsage,
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
346pub struct ChatChoice {
347 pub index: u32,
348 pub message: ChatMessage,
349 pub finish_reason: String,
350}
351
352#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
353pub struct TokenUsage {
354 pub prompt_tokens: u32,
355 pub completion_tokens: u32,
356 pub total_tokens: u32,
357}
358
359#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
360pub struct ResponsesRequest {
361 #[serde(default)]
362 pub model: Option<String>,
363 #[serde(default)]
364 pub input: Value,
365 #[serde(default)]
366 pub instructions: Option<String>,
367 #[serde(default)]
368 pub temperature: Option<f32>,
369 #[serde(default)]
370 pub stream: bool,
371 #[serde(default, skip_serializing_if = "Vec::is_empty")]
374 pub tools: Vec<Value>,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub tool_choice: Option<Value>,
377}
378
379impl ResponsesRequest {
380 #[must_use]
389 pub fn to_chat_completion_request(&self) -> ChatCompletionRequest {
390 let mut messages = Vec::new();
391 if let Some(instructions) = self.instructions.as_deref() {
392 if !instructions.trim().is_empty() {
393 messages.push(ChatMessage::new("system", instructions.trim()));
394 }
395 }
396 let mut tool_names_by_id: HashMap<String, String> = HashMap::new();
397 append_response_input(&self.input, &mut messages, &mut tool_names_by_id);
398 ChatCompletionRequest {
399 model: self.model.clone(),
400 messages,
401 temperature: self.temperature,
402 stream: false,
403 tools: self.tools.clone(),
404 tool_choice: self.tool_choice.clone(),
405 functions: Vec::new(),
406 function_call: None,
407 stream_options: None,
408 }
409 }
410}
411
412fn responses_input_tokens(request: &ResponsesRequest) -> u32 {
413 message_input_tokens(&request.to_chat_completion_request().messages)
414}
415
416fn append_response_input(
421 input: &Value,
422 out: &mut Vec<ChatMessage>,
423 tool_names_by_id: &mut HashMap<String, String>,
424) {
425 match input {
426 Value::String(text) => {
427 if !text.trim().is_empty() {
428 out.push(ChatMessage::user(text.clone()));
429 }
430 }
431 Value::Array(items) => {
432 for item in items {
433 append_response_item(item, out, tool_names_by_id);
434 }
435 }
436 Value::Object(_) => append_response_item(input, out, tool_names_by_id),
437 _ => {}
438 }
439}
440
441fn append_response_item(
444 item: &Value,
445 out: &mut Vec<ChatMessage>,
446 tool_names_by_id: &mut HashMap<String, String>,
447) {
448 let item_type = item
449 .get("type")
450 .and_then(Value::as_str)
451 .unwrap_or("message");
452 match item_type {
453 "function_call" => {
454 let call_id = item
455 .get("call_id")
456 .or_else(|| item.get("id"))
457 .and_then(Value::as_str)
458 .unwrap_or_default()
459 .to_owned();
460 let name = item
461 .get("name")
462 .and_then(Value::as_str)
463 .unwrap_or_default()
464 .to_owned();
465 let arguments = item
466 .get("arguments")
467 .and_then(Value::as_str)
468 .unwrap_or("{}")
469 .to_owned();
470 if !name.is_empty() {
471 tool_names_by_id.insert(call_id.clone(), name.clone());
472 }
473 out.push(ChatMessage::assistant_tool_calls(vec![ToolCall::function(
474 call_id, name, arguments,
475 )]));
476 }
477 "function_call_output" => {
478 let call_id = item
479 .get("call_id")
480 .and_then(Value::as_str)
481 .unwrap_or_default()
482 .to_owned();
483 let output = item
484 .get("output")
485 .map_or_else(String::new, value_to_prompt_text);
486 let name = tool_names_by_id.get(&call_id).cloned();
487 out.push(ChatMessage {
488 role: String::from("tool"),
489 content: MessageContent::Text(output),
490 tool_call_id: Some(call_id),
491 name,
492 ..ChatMessage::default()
493 });
494 }
495 _ => {
496 let role = item
497 .get("role")
498 .and_then(Value::as_str)
499 .unwrap_or("user")
500 .to_owned();
501 let content = item
502 .get("content")
503 .map_or_else(String::new, value_to_prompt_text);
504 if !content.trim().is_empty() {
505 out.push(ChatMessage::new(role, content));
506 }
507 }
508 }
509}
510
511#[must_use]
512pub fn create_chat_completion(request: &ChatCompletionRequest) -> ChatCompletion {
513 create_chat_completion_with_solver(request, &UniversalSolver::default())
514}
515
516#[must_use]
517pub fn create_chat_completion_with_solver(
518 request: &ChatCompletionRequest,
519 solver: &UniversalSolver,
520) -> ChatCompletion {
521 create_chat_completion_with_solver_and_memory(request, solver, &[])
522}
523
524#[must_use]
525pub fn create_chat_completion_with_solver_and_memory(
526 request: &ChatCompletionRequest,
527 solver: &UniversalSolver,
528 memory_events: &[MemoryEvent],
529) -> ChatCompletion {
530 let (prompt, history) = chat_prompt_and_history(&request.messages);
531
532 match agentic_outcome(request, solver.config.agent_mode) {
533 AgenticOutcome::Refused(answer) => {
534 return chat_completion_from_symbolic(request, &prompt, answer)
535 }
536 AgenticOutcome::Planned(plan) => {
537 return chat_completion_from_plan(request, &prompt, plan, memory_events)
538 }
539 AgenticOutcome::Fallthrough => {}
540 }
541
542 if let Some(mut symbolic_answer) =
543 answer_from_memory_if_requested(&prompt, &history, memory_events)
544 {
545 apply_retained_amendments(&prompt, &mut symbolic_answer, memory_events);
548 return chat_completion_from_symbolic(request, &prompt, symbolic_answer);
549 }
550
551 let symbolic_answer =
554 solve_with_standing_requirements(solver, &prompt, &history, memory_events);
555 if let Some(plan) = command_reroute_plan(request, solver.config.agent_mode, &symbolic_answer) {
556 return chat_completion_from_plan(request, &prompt, plan, memory_events);
557 }
558 chat_completion_from_symbolic(request, &prompt, symbolic_answer)
559}
560
561fn command_reroute_plan(
562 request: &ChatCompletionRequest,
563 agent_mode: bool,
564 symbolic_answer: &SymbolicAnswer,
565) -> Option<AgenticPlan> {
566 if !agent_mode || !request.requests_tool_execution() {
567 return None;
568 }
569 let owned_names = request.requested_tool_names();
570 let tool_names: Vec<&str> = owned_names.iter().map(String::as_str).collect();
571 plan_symbolic_command_reroute(&request.messages, &tool_names, symbolic_answer)
572}
573
574enum AgenticOutcome {
579 Refused(SymbolicAnswer),
582 Planned(AgenticPlan),
584 Fallthrough,
587}
588
589fn agentic_outcome(request: &ChatCompletionRequest, agent_mode: bool) -> AgenticOutcome {
594 let trace = std::env::var("FORMAL_AI_TRACE_REQUESTS").as_deref() == Ok("1");
595 if !request.requests_tool_execution() {
596 if trace {
597 eprintln!("[trace] agentic_outcome: fallthrough (no tool execution requested)");
598 }
599 return AgenticOutcome::Fallthrough;
600 }
601 if !agent_mode {
602 if trace {
603 eprintln!("[trace] agentic_outcome: refused (agent_mode off)");
604 }
605 return AgenticOutcome::Refused(tool_call_refusal_answer());
606 }
607 let owned_names = request.requested_tool_names();
608 if trace {
609 eprintln!(
610 "[trace] agentic_outcome: {} advertised tools: {owned_names:?}",
611 owned_names.len()
612 );
613 }
614 if let Some(denial) = agentic_tool_permission_denial(&owned_names) {
615 if trace {
616 eprintln!("[trace] agentic_outcome: refused by permission gate: {denial:?}");
617 }
618 return AgenticOutcome::Refused(tool_permission_refusal_answer(&denial));
619 }
620 let tool_names: Vec<&str> = owned_names.iter().map(String::as_str).collect();
624 let outcome = plan_chat_step(&request.messages, &tool_names)
625 .map_or(AgenticOutcome::Fallthrough, AgenticOutcome::Planned);
626 if trace {
627 match &outcome {
628 AgenticOutcome::Planned(plan) => eprintln!("[trace] agentic_outcome: planned {plan:?}"),
629 AgenticOutcome::Fallthrough => {
630 eprintln!("[trace] agentic_outcome: fallthrough (task unrecognised)");
631 }
632 AgenticOutcome::Refused(_) => {}
633 }
634 }
635 outcome
636}
637
638fn chat_completion_from_plan(
643 request: &ChatCompletionRequest,
644 prompt: &str,
645 plan: AgenticPlan,
646 memory_events: &[MemoryEvent],
647) -> ChatCompletion {
648 let model = resolved_request_model(request.model.as_deref());
649 let prompt_tokens = message_input_tokens(&request.messages);
650
651 let (message, finish_reason, completion_tokens) = match plan {
652 AgenticPlan::ToolCalls(calls) => {
653 let narration = tool_action_narration(prompt, &calls);
654 let tool_calls: Vec<_> = calls
655 .into_iter()
656 .enumerate()
657 .map(|(index, call)| {
658 let seed = format!("{prompt}|{index}|{}|{}", call.tool, call.arguments);
659 let arguments = response_arguments_for_tool(
660 &request.tools,
661 &call.tool,
662 call.arguments,
663 prompt,
664 );
665 ToolCall::function(stable_id("call", &seed), call.tool, arguments)
666 })
667 .collect();
668 let completion_tokens = estimate_tokens(&narration).saturating_add(
669 tool_calls
670 .iter()
671 .map(|call| {
672 estimate_tokens(&call.function.name)
673 .saturating_add(estimate_tokens(&call.function.arguments))
674 })
675 .sum(),
676 );
677 (
678 ChatMessage::assistant_tool_calls_with_content(narration, tool_calls),
679 String::from("tool_calls"),
680 completion_tokens,
681 )
682 }
683 AgenticPlan::Final(answer) => {
684 let answer = amended_answer(prompt, &answer, memory_events);
685 let completion_tokens = estimate_tokens(&answer);
686 (
687 ChatMessage::assistant(answer),
688 String::from("stop"),
689 completion_tokens,
690 )
691 }
692 };
693
694 ChatCompletion {
695 id: stable_id("chatcmpl", prompt),
696 object: String::from("chat.completion"),
697 created: response_timestamp(),
698 model,
699 choices: vec![ChatChoice {
700 index: 0,
701 message,
702 finish_reason,
703 }],
704 usage: TokenUsage {
705 prompt_tokens,
706 completion_tokens,
707 total_tokens: prompt_tokens.saturating_add(completion_tokens),
708 },
709 }
710}
711
712fn chat_completion_from_symbolic(
713 request: &ChatCompletionRequest,
714 prompt: &str,
715 symbolic_answer: SymbolicAnswer,
716) -> ChatCompletion {
717 let model = resolved_request_model(request.model.as_deref());
718 let prompt_tokens = message_input_tokens(&request.messages);
719 let completion_tokens = estimate_tokens(&symbolic_answer.answer);
720 let thinking_steps = symbolic_answer.thinking_steps;
721 let reasoning = render_thinking_steps(&thinking_steps);
722 let mut message = ChatMessage::assistant(symbolic_answer.answer);
723 message.thinking_steps = thinking_steps;
724 message.reasoning_content.clone_from(&reasoning);
725 message.reasoning = reasoning;
726
727 ChatCompletion {
728 id: stable_id("chatcmpl", prompt),
729 object: String::from("chat.completion"),
730 created: response_timestamp(),
731 model,
732 choices: vec![ChatChoice {
733 index: 0,
734 message,
735 finish_reason: String::from("stop"),
736 }],
737 usage: TokenUsage {
738 prompt_tokens,
739 completion_tokens,
740 total_tokens: prompt_tokens.saturating_add(completion_tokens),
741 },
742 }
743}
744
745#[must_use]
746pub fn create_response(request: &ResponsesRequest) -> ResponseObject {
747 let prompt = response_prompt(request);
748 let symbolic_answer = FormalAiEngine.answer(&prompt);
749 response_from_symbolic(request, &prompt, symbolic_answer)
750}
751
752#[must_use]
753pub fn create_response_with_solver(
754 request: &ResponsesRequest,
755 solver: &UniversalSolver,
756) -> ResponseObject {
757 create_response_with_solver_and_memory(request, solver, &[])
758}
759
760#[must_use]
761pub fn create_response_with_solver_and_memory(
762 request: &ResponsesRequest,
763 solver: &UniversalSolver,
764 memory_events: &[MemoryEvent],
765) -> ResponseObject {
766 let prompt = response_prompt(request);
767 let chat_request = request.to_chat_completion_request();
768 match agentic_outcome(&chat_request, solver.config.agent_mode) {
769 AgenticOutcome::Refused(answer) => return response_from_symbolic(request, &prompt, answer),
770 AgenticOutcome::Planned(plan) => {
771 return response_from_plan(request, &prompt, plan, memory_events)
772 }
773 AgenticOutcome::Fallthrough => {}
774 }
775 let (memory_prompt, history) = chat_prompt_and_history(&chat_request.messages);
776 let memory_prompt = if memory_prompt.trim().is_empty() {
777 prompt.as_str()
778 } else {
779 memory_prompt.as_str()
780 };
781 if let Some(mut symbolic_answer) =
782 answer_from_memory_if_requested(memory_prompt, &history, memory_events)
783 {
784 apply_retained_amendments(&prompt, &mut symbolic_answer, memory_events);
785 return response_from_symbolic(request, &prompt, symbolic_answer);
786 }
787 let symbolic_answer =
788 solve_with_standing_requirements(solver, memory_prompt, &history, memory_events);
789 if let Some(plan) =
790 command_reroute_plan(&chat_request, solver.config.agent_mode, &symbolic_answer)
791 {
792 return response_from_plan(request, &prompt, plan, memory_events);
793 }
794 response_from_symbolic(request, &prompt, symbolic_answer)
795}
796
797fn response_from_plan(
802 request: &ResponsesRequest,
803 prompt: &str,
804 plan: AgenticPlan,
805 memory_events: &[MemoryEvent],
806) -> ResponseObject {
807 let model = resolved_request_model(request.model.as_deref());
808 let input_tokens = responses_input_tokens(request);
809
810 let (output, output_tokens) = match plan {
811 AgenticPlan::ToolCalls(calls) => {
812 let narration = tool_action_narration(prompt, &calls);
813 let mut items = Vec::with_capacity(calls.len().saturating_add(1));
814 let mut output_tokens = estimate_tokens(&narration);
815 items.push(ResponseOutputItem::Message(ResponseOutputMessage {
816 id: stable_id("msg", &narration),
817 kind: String::from("message"),
818 role: String::from("assistant"),
819 content: vec![ResponseOutputContent {
820 kind: String::from("output_text"),
821 text: narration,
822 }],
823 thinking_steps: Vec::new(),
824 }));
825 for (index, call) in calls.into_iter().enumerate() {
826 let tool = call.tool;
827 let planned_arguments = call.arguments;
828 let seed = format!("{prompt}|{index}|{tool}|{planned_arguments}");
829 let arguments =
830 response_arguments_for_tool(&request.tools, &tool, planned_arguments, prompt);
831 output_tokens = output_tokens.saturating_add(
832 estimate_tokens(&tool).saturating_add(estimate_tokens(&arguments)),
833 );
834 if request
835 .tools
836 .iter()
837 .any(|definition| is_hosted_tool_definition(definition, &tool))
838 && tool == "web_search"
839 {
840 let query = serde_json::from_str::<Value>(&arguments)
841 .ok()
842 .and_then(|value| {
843 value
844 .get("query")
845 .and_then(Value::as_str)
846 .map(str::to_owned)
847 })
848 .unwrap_or_else(|| prompt.to_owned());
849 items.push(ResponseOutputItem::WebSearchCall(
850 ResponseWebSearchToolCall {
851 id: stable_id("ws", &seed),
852 kind: String::from("web_search_call"),
853 status: String::from("completed"),
854 action: ResponseWebSearchAction {
855 kind: String::from("search"),
856 queries: vec![query.clone()],
857 query,
858 },
859 },
860 ));
861 } else {
862 let (name, namespace) = response_tool_call_identity(&request.tools, &tool);
863 items.push(ResponseOutputItem::FunctionCall(ResponseFunctionToolCall {
864 id: stable_id("fc", &seed),
865 kind: function_call_kind(),
866 call_id: stable_id("call", &seed),
867 name,
868 namespace,
869 arguments,
870 status: String::from("completed"),
871 }));
872 }
873 }
874 (items, output_tokens)
875 }
876 AgenticPlan::Final(answer) => {
877 let answer = amended_answer(prompt, &answer, memory_events);
878 let output_tokens = estimate_tokens(&answer);
879 let message = ResponseOutputItem::Message(ResponseOutputMessage {
880 id: stable_id("msg", &answer),
881 kind: String::from("message"),
882 role: String::from("assistant"),
883 content: vec![ResponseOutputContent {
884 kind: String::from("output_text"),
885 text: answer,
886 }],
887 thinking_steps: Vec::new(),
888 });
889 (vec![message], output_tokens)
890 }
891 };
892
893 ResponseObject {
894 id: stable_id("resp", prompt),
895 object: String::from("response"),
896 created_at: response_timestamp(),
897 status: String::from("completed"),
898 model,
899 output,
900 usage: ResponseUsage {
901 input_tokens,
902 output_tokens,
903 total_tokens: input_tokens.saturating_add(output_tokens),
904 },
905 evidence_links: Vec::new(),
906 thinking_steps: Vec::new(),
907 }
908}
909
910fn response_from_symbolic(
911 request: &ResponsesRequest,
912 prompt: &str,
913 symbolic_answer: SymbolicAnswer,
914) -> ResponseObject {
915 let model = resolved_request_model(request.model.as_deref());
916 let input_tokens = responses_input_tokens(request);
917 let output_tokens = estimate_tokens(&symbolic_answer.answer);
918 let answer = symbolic_answer.answer;
919 let thinking_steps = symbolic_answer.thinking_steps;
920 let mut output = vec![ResponseOutputItem::Message(ResponseOutputMessage {
921 id: stable_id("msg", &answer),
922 kind: String::from("message"),
923 role: String::from("assistant"),
924 content: vec![ResponseOutputContent {
925 kind: String::from("output_text"),
926 text: answer,
927 }],
928 thinking_steps: thinking_steps.clone(),
929 })];
930 if let Some(reasoning) = response_reasoning_item(prompt, &thinking_steps) {
931 output.push(reasoning);
932 }
933
934 ResponseObject {
935 id: stable_id("resp", prompt),
936 object: String::from("response"),
937 created_at: response_timestamp(),
938 status: String::from("completed"),
939 model,
940 output,
941 usage: ResponseUsage {
942 input_tokens,
943 output_tokens,
944 total_tokens: input_tokens.saturating_add(output_tokens),
945 },
946 evidence_links: symbolic_answer.evidence_links,
947 thinking_steps,
948 }
949}
950
951fn response_reasoning_item(
952 prompt: &str,
953 thinking_steps: &[ThinkingStep],
954) -> Option<ResponseOutputItem> {
955 let text = render_thinking_steps(thinking_steps);
956 if text.is_empty() {
957 return None;
958 }
959 Some(ResponseOutputItem::Reasoning(ResponseReasoningItem {
960 id: stable_id("rs", prompt),
961 kind: String::from("reasoning"),
962 summary: vec![ResponseReasoningSummaryText {
963 kind: String::from("summary_text"),
964 text,
965 }],
966 }))
967}