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 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub learning_trace: Option<Value>,
345}
346
347#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
348pub struct ChatChoice {
349 pub index: u32,
350 pub message: ChatMessage,
351 pub finish_reason: String,
352}
353
354#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
355pub struct TokenUsage {
356 pub prompt_tokens: u32,
357 pub completion_tokens: u32,
358 pub total_tokens: u32,
359}
360
361#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
362pub struct ResponsesRequest {
363 #[serde(default)]
364 pub model: Option<String>,
365 #[serde(default)]
366 pub input: Value,
367 #[serde(default)]
368 pub instructions: Option<String>,
369 #[serde(default)]
370 pub temperature: Option<f32>,
371 #[serde(default)]
372 pub stream: bool,
373 #[serde(default, skip_serializing_if = "Vec::is_empty")]
376 pub tools: Vec<Value>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
378 pub tool_choice: Option<Value>,
379}
380
381impl ResponsesRequest {
382 #[must_use]
391 pub fn to_chat_completion_request(&self) -> ChatCompletionRequest {
392 let mut messages = Vec::new();
393 if let Some(instructions) = self.instructions.as_deref() {
394 if !instructions.trim().is_empty() {
395 messages.push(ChatMessage::new("system", instructions.trim()));
396 }
397 }
398 let mut tool_names_by_id: HashMap<String, String> = HashMap::new();
399 append_response_input(&self.input, &mut messages, &mut tool_names_by_id);
400 ChatCompletionRequest {
401 model: self.model.clone(),
402 messages,
403 temperature: self.temperature,
404 stream: false,
405 tools: self.tools.clone(),
406 tool_choice: self.tool_choice.clone(),
407 functions: Vec::new(),
408 function_call: None,
409 stream_options: None,
410 }
411 }
412}
413
414fn responses_input_tokens(request: &ResponsesRequest) -> u32 {
415 message_input_tokens(&request.to_chat_completion_request().messages)
416}
417
418fn append_response_input(
423 input: &Value,
424 out: &mut Vec<ChatMessage>,
425 tool_names_by_id: &mut HashMap<String, String>,
426) {
427 match input {
428 Value::String(text) => {
429 if !text.trim().is_empty() {
430 out.push(ChatMessage::user(text.clone()));
431 }
432 }
433 Value::Array(items) => {
434 for item in items {
435 append_response_item(item, out, tool_names_by_id);
436 }
437 }
438 Value::Object(_) => append_response_item(input, out, tool_names_by_id),
439 _ => {}
440 }
441}
442
443fn append_response_item(
446 item: &Value,
447 out: &mut Vec<ChatMessage>,
448 tool_names_by_id: &mut HashMap<String, String>,
449) {
450 let item_type = item
451 .get("type")
452 .and_then(Value::as_str)
453 .unwrap_or("message");
454 match item_type {
455 "function_call" => {
456 let call_id = item
457 .get("call_id")
458 .or_else(|| item.get("id"))
459 .and_then(Value::as_str)
460 .unwrap_or_default()
461 .to_owned();
462 let name = item
463 .get("name")
464 .and_then(Value::as_str)
465 .unwrap_or_default()
466 .to_owned();
467 let arguments = item
468 .get("arguments")
469 .and_then(Value::as_str)
470 .unwrap_or("{}")
471 .to_owned();
472 if !name.is_empty() {
473 tool_names_by_id.insert(call_id.clone(), name.clone());
474 }
475 out.push(ChatMessage::assistant_tool_calls(vec![ToolCall::function(
476 call_id, name, arguments,
477 )]));
478 }
479 "function_call_output" => {
480 let call_id = item
481 .get("call_id")
482 .and_then(Value::as_str)
483 .unwrap_or_default()
484 .to_owned();
485 let output = item
486 .get("output")
487 .map_or_else(String::new, value_to_prompt_text);
488 let name = tool_names_by_id.get(&call_id).cloned();
489 out.push(ChatMessage {
490 role: String::from("tool"),
491 content: MessageContent::Text(output),
492 tool_call_id: Some(call_id),
493 name,
494 ..ChatMessage::default()
495 });
496 }
497 _ => {
498 let role = item
499 .get("role")
500 .and_then(Value::as_str)
501 .unwrap_or("user")
502 .to_owned();
503 let content = item
504 .get("content")
505 .map_or_else(String::new, value_to_prompt_text);
506 if !content.trim().is_empty() {
507 out.push(ChatMessage::new(role, content));
508 }
509 }
510 }
511}
512
513#[must_use]
514pub fn create_chat_completion(request: &ChatCompletionRequest) -> ChatCompletion {
515 create_chat_completion_with_solver(request, &UniversalSolver::default())
516}
517
518#[must_use]
519pub fn create_chat_completion_with_solver(
520 request: &ChatCompletionRequest,
521 solver: &UniversalSolver,
522) -> ChatCompletion {
523 create_chat_completion_with_solver_and_memory(request, solver, &[])
524}
525
526#[must_use]
527pub fn create_chat_completion_with_solver_and_memory(
528 request: &ChatCompletionRequest,
529 solver: &UniversalSolver,
530 memory_events: &[MemoryEvent],
531) -> ChatCompletion {
532 let (prompt, history) = chat_prompt_and_history(&request.messages);
533
534 match agentic_outcome(request, solver.config.agent_mode) {
535 AgenticOutcome::Refused(answer) => {
536 return chat_completion_from_symbolic(request, &prompt, answer)
537 }
538 AgenticOutcome::Planned(plan) => {
539 return chat_completion_from_plan(request, &prompt, plan, memory_events)
540 }
541 AgenticOutcome::Fallthrough => {}
542 }
543
544 if let Some(mut symbolic_answer) =
545 answer_from_memory_if_requested(&prompt, &history, memory_events)
546 {
547 apply_retained_amendments(&prompt, &mut symbolic_answer, memory_events);
550 return chat_completion_from_symbolic(request, &prompt, symbolic_answer);
551 }
552
553 let symbolic_answer =
556 solve_with_standing_requirements(solver, &prompt, &history, memory_events);
557 if let Some(plan) = command_reroute_plan(request, solver.config.agent_mode, &symbolic_answer) {
558 return chat_completion_from_plan(request, &prompt, plan, memory_events);
559 }
560 chat_completion_from_symbolic(request, &prompt, symbolic_answer)
561}
562
563fn command_reroute_plan(
564 request: &ChatCompletionRequest,
565 agent_mode: bool,
566 symbolic_answer: &SymbolicAnswer,
567) -> Option<AgenticPlan> {
568 if !agent_mode || !request.requests_tool_execution() {
569 return None;
570 }
571 let owned_names = request.requested_tool_names();
572 let tool_names: Vec<&str> = owned_names.iter().map(String::as_str).collect();
573 plan_symbolic_command_reroute(&request.messages, &tool_names, symbolic_answer)
574}
575
576enum AgenticOutcome {
581 Refused(SymbolicAnswer),
584 Planned(AgenticPlan),
586 Fallthrough,
589}
590
591fn agentic_outcome(request: &ChatCompletionRequest, agent_mode: bool) -> AgenticOutcome {
596 let trace = std::env::var("FORMAL_AI_TRACE_REQUESTS").as_deref() == Ok("1");
597 if !request.requests_tool_execution() {
598 if trace {
599 eprintln!("[trace] agentic_outcome: fallthrough (no tool execution requested)");
600 }
601 return AgenticOutcome::Fallthrough;
602 }
603 if !agent_mode {
604 if trace {
605 eprintln!("[trace] agentic_outcome: refused (agent_mode off)");
606 }
607 return AgenticOutcome::Refused(tool_call_refusal_answer());
608 }
609 let owned_names = request.requested_tool_names();
610 if trace {
611 eprintln!(
612 "[trace] agentic_outcome: {} advertised tools: {owned_names:?}",
613 owned_names.len()
614 );
615 }
616 if let Some(denial) = agentic_tool_permission_denial(&owned_names) {
617 if trace {
618 eprintln!("[trace] agentic_outcome: refused by permission gate: {denial:?}");
619 }
620 return AgenticOutcome::Refused(tool_permission_refusal_answer(&denial));
621 }
622 let tool_names: Vec<&str> = owned_names.iter().map(String::as_str).collect();
626 let outcome = plan_chat_step(&request.messages, &tool_names)
627 .map_or(AgenticOutcome::Fallthrough, AgenticOutcome::Planned);
628 if trace {
629 match &outcome {
630 AgenticOutcome::Planned(plan) => eprintln!("[trace] agentic_outcome: planned {plan:?}"),
631 AgenticOutcome::Fallthrough => {
632 eprintln!("[trace] agentic_outcome: fallthrough (task unrecognised)");
633 }
634 AgenticOutcome::Refused(_) => {}
635 }
636 }
637 outcome
638}
639
640fn chat_completion_from_plan(
645 request: &ChatCompletionRequest,
646 prompt: &str,
647 plan: AgenticPlan,
648 memory_events: &[MemoryEvent],
649) -> ChatCompletion {
650 let model = resolved_request_model(request.model.as_deref());
651 let prompt_tokens = message_input_tokens(&request.messages);
652
653 let (message, finish_reason, completion_tokens) = match plan {
654 AgenticPlan::ToolCalls(calls) => {
655 let narration = tool_action_narration(prompt, &calls);
656 let tool_calls: Vec<_> = calls
657 .into_iter()
658 .enumerate()
659 .map(|(index, call)| {
660 let seed = format!("{prompt}|{index}|{}|{}", call.tool, call.arguments);
661 let arguments = response_arguments_for_tool(
662 &request.tools,
663 &call.tool,
664 call.arguments,
665 prompt,
666 );
667 ToolCall::function(stable_id("call", &seed), call.tool, arguments)
668 })
669 .collect();
670 let completion_tokens = estimate_tokens(&narration).saturating_add(
671 tool_calls
672 .iter()
673 .map(|call| {
674 estimate_tokens(&call.function.name)
675 .saturating_add(estimate_tokens(&call.function.arguments))
676 })
677 .sum(),
678 );
679 (
680 ChatMessage::assistant_tool_calls_with_content(narration, tool_calls),
681 String::from("tool_calls"),
682 completion_tokens,
683 )
684 }
685 AgenticPlan::Final(answer) => {
686 let answer = amended_answer(prompt, &answer, memory_events);
687 let completion_tokens = estimate_tokens(&answer);
688 (
689 ChatMessage::assistant(answer),
690 String::from("stop"),
691 completion_tokens,
692 )
693 }
694 };
695
696 ChatCompletion {
697 id: stable_id("chatcmpl", prompt),
698 object: String::from("chat.completion"),
699 created: response_timestamp(),
700 model,
701 choices: vec![ChatChoice {
702 index: 0,
703 message,
704 finish_reason,
705 }],
706 usage: TokenUsage {
707 prompt_tokens,
708 completion_tokens,
709 total_tokens: prompt_tokens.saturating_add(completion_tokens),
710 },
711 learning_trace: None,
712 }
713}
714
715fn chat_completion_from_symbolic(
716 request: &ChatCompletionRequest,
717 prompt: &str,
718 symbolic_answer: SymbolicAnswer,
719) -> ChatCompletion {
720 let model = resolved_request_model(request.model.as_deref());
721 let prompt_tokens = message_input_tokens(&request.messages);
722 let completion_tokens = estimate_tokens(&symbolic_answer.answer);
723 let learning_trace =
724 crate::self_improvement::learning_trace_from_symbolic_answer(prompt, &symbolic_answer);
725 let thinking_steps = symbolic_answer.thinking_steps;
726 let reasoning = render_thinking_steps(&thinking_steps);
727 let mut message = ChatMessage::assistant(symbolic_answer.answer);
728 message.thinking_steps = thinking_steps;
729 message.reasoning_content.clone_from(&reasoning);
730 message.reasoning = reasoning;
731
732 ChatCompletion {
733 id: stable_id("chatcmpl", prompt),
734 object: String::from("chat.completion"),
735 created: response_timestamp(),
736 model,
737 choices: vec![ChatChoice {
738 index: 0,
739 message,
740 finish_reason: String::from("stop"),
741 }],
742 usage: TokenUsage {
743 prompt_tokens,
744 completion_tokens,
745 total_tokens: prompt_tokens.saturating_add(completion_tokens),
746 },
747 learning_trace,
748 }
749}
750
751#[must_use]
752pub fn create_response(request: &ResponsesRequest) -> ResponseObject {
753 let prompt = response_prompt(request);
754 let symbolic_answer = FormalAiEngine.answer(&prompt);
755 response_from_symbolic(request, &prompt, symbolic_answer)
756}
757
758#[must_use]
759pub fn create_response_with_solver(
760 request: &ResponsesRequest,
761 solver: &UniversalSolver,
762) -> ResponseObject {
763 create_response_with_solver_and_memory(request, solver, &[])
764}
765
766#[must_use]
767pub fn create_response_with_solver_and_memory(
768 request: &ResponsesRequest,
769 solver: &UniversalSolver,
770 memory_events: &[MemoryEvent],
771) -> ResponseObject {
772 let prompt = response_prompt(request);
773 let chat_request = request.to_chat_completion_request();
774 match agentic_outcome(&chat_request, solver.config.agent_mode) {
775 AgenticOutcome::Refused(answer) => return response_from_symbolic(request, &prompt, answer),
776 AgenticOutcome::Planned(plan) => {
777 return response_from_plan(request, &prompt, plan, memory_events)
778 }
779 AgenticOutcome::Fallthrough => {}
780 }
781 let (memory_prompt, history) = chat_prompt_and_history(&chat_request.messages);
782 let memory_prompt = if memory_prompt.trim().is_empty() {
783 prompt.as_str()
784 } else {
785 memory_prompt.as_str()
786 };
787 if let Some(mut symbolic_answer) =
788 answer_from_memory_if_requested(memory_prompt, &history, memory_events)
789 {
790 apply_retained_amendments(&prompt, &mut symbolic_answer, memory_events);
791 return response_from_symbolic(request, &prompt, symbolic_answer);
792 }
793 let symbolic_answer =
794 solve_with_standing_requirements(solver, memory_prompt, &history, memory_events);
795 if let Some(plan) =
796 command_reroute_plan(&chat_request, solver.config.agent_mode, &symbolic_answer)
797 {
798 return response_from_plan(request, &prompt, plan, memory_events);
799 }
800 response_from_symbolic(request, &prompt, symbolic_answer)
801}
802
803fn response_from_plan(
808 request: &ResponsesRequest,
809 prompt: &str,
810 plan: AgenticPlan,
811 memory_events: &[MemoryEvent],
812) -> ResponseObject {
813 let model = resolved_request_model(request.model.as_deref());
814 let input_tokens = responses_input_tokens(request);
815
816 let (output, output_tokens) = match plan {
817 AgenticPlan::ToolCalls(calls) => {
818 let narration = tool_action_narration(prompt, &calls);
819 let mut items = Vec::with_capacity(calls.len().saturating_add(1));
820 let mut output_tokens = estimate_tokens(&narration);
821 items.push(ResponseOutputItem::Message(ResponseOutputMessage {
822 id: stable_id("msg", &narration),
823 kind: String::from("message"),
824 role: String::from("assistant"),
825 content: vec![ResponseOutputContent {
826 kind: String::from("output_text"),
827 text: narration,
828 }],
829 thinking_steps: Vec::new(),
830 }));
831 for (index, call) in calls.into_iter().enumerate() {
832 let tool = call.tool;
833 let planned_arguments = call.arguments;
834 let seed = format!("{prompt}|{index}|{tool}|{planned_arguments}");
835 let arguments =
836 response_arguments_for_tool(&request.tools, &tool, planned_arguments, prompt);
837 output_tokens = output_tokens.saturating_add(
838 estimate_tokens(&tool).saturating_add(estimate_tokens(&arguments)),
839 );
840 if request
841 .tools
842 .iter()
843 .any(|definition| is_hosted_tool_definition(definition, &tool))
844 && tool == "web_search"
845 {
846 let query = serde_json::from_str::<Value>(&arguments)
847 .ok()
848 .and_then(|value| {
849 value
850 .get("query")
851 .and_then(Value::as_str)
852 .map(str::to_owned)
853 })
854 .unwrap_or_else(|| prompt.to_owned());
855 items.push(ResponseOutputItem::WebSearchCall(
856 ResponseWebSearchToolCall {
857 id: stable_id("ws", &seed),
858 kind: String::from("web_search_call"),
859 status: String::from("completed"),
860 action: ResponseWebSearchAction {
861 kind: String::from("search"),
862 queries: vec![query.clone()],
863 query,
864 },
865 },
866 ));
867 } else {
868 let (name, namespace) = response_tool_call_identity(&request.tools, &tool);
869 items.push(ResponseOutputItem::FunctionCall(ResponseFunctionToolCall {
870 id: stable_id("fc", &seed),
871 kind: function_call_kind(),
872 call_id: stable_id("call", &seed),
873 name,
874 namespace,
875 arguments,
876 status: String::from("completed"),
877 }));
878 }
879 }
880 (items, output_tokens)
881 }
882 AgenticPlan::Final(answer) => {
883 let answer = amended_answer(prompt, &answer, memory_events);
884 let output_tokens = estimate_tokens(&answer);
885 let message = ResponseOutputItem::Message(ResponseOutputMessage {
886 id: stable_id("msg", &answer),
887 kind: String::from("message"),
888 role: String::from("assistant"),
889 content: vec![ResponseOutputContent {
890 kind: String::from("output_text"),
891 text: answer,
892 }],
893 thinking_steps: Vec::new(),
894 });
895 (vec![message], output_tokens)
896 }
897 };
898
899 ResponseObject {
900 id: stable_id("resp", prompt),
901 object: String::from("response"),
902 created_at: response_timestamp(),
903 status: String::from("completed"),
904 model,
905 output,
906 usage: ResponseUsage {
907 input_tokens,
908 output_tokens,
909 total_tokens: input_tokens.saturating_add(output_tokens),
910 },
911 evidence_links: Vec::new(),
912 thinking_steps: Vec::new(),
913 }
914}
915
916fn response_from_symbolic(
917 request: &ResponsesRequest,
918 prompt: &str,
919 symbolic_answer: SymbolicAnswer,
920) -> ResponseObject {
921 let model = resolved_request_model(request.model.as_deref());
922 let input_tokens = responses_input_tokens(request);
923 let output_tokens = estimate_tokens(&symbolic_answer.answer);
924 let answer = symbolic_answer.answer;
925 let thinking_steps = symbolic_answer.thinking_steps;
926 let mut output = vec![ResponseOutputItem::Message(ResponseOutputMessage {
927 id: stable_id("msg", &answer),
928 kind: String::from("message"),
929 role: String::from("assistant"),
930 content: vec![ResponseOutputContent {
931 kind: String::from("output_text"),
932 text: answer,
933 }],
934 thinking_steps: thinking_steps.clone(),
935 })];
936 if let Some(reasoning) = response_reasoning_item(prompt, &thinking_steps) {
937 output.push(reasoning);
938 }
939
940 ResponseObject {
941 id: stable_id("resp", prompt),
942 object: String::from("response"),
943 created_at: response_timestamp(),
944 status: String::from("completed"),
945 model,
946 output,
947 usage: ResponseUsage {
948 input_tokens,
949 output_tokens,
950 total_tokens: input_tokens.saturating_add(output_tokens),
951 },
952 evidence_links: symbolic_answer.evidence_links,
953 thinking_steps,
954 }
955}
956
957fn response_reasoning_item(
958 prompt: &str,
959 thinking_steps: &[ThinkingStep],
960) -> Option<ResponseOutputItem> {
961 let text = render_thinking_steps(thinking_steps);
962 if text.is_empty() {
963 return None;
964 }
965 Some(ResponseOutputItem::Reasoning(ResponseReasoningItem {
966 id: stable_id("rs", prompt),
967 kind: String::from("reasoning"),
968 summary: vec![ResponseReasoningSummaryText {
969 kind: String::from("summary_text"),
970 text,
971 }],
972 }))
973}