| 4445 | buf_puts(&qualified, namespace); |
| 4446 | buf_puts(&qualified, name); |
| 4447 | tc.name = buf_take(&qualified); |
| 4448 | } else { |
| 4449 | tc.name = xstrdup(name ? name : ""); |
| 4450 | } |
| 4451 | /* A Responses turn that has both message text and tool calls splits |
| 4452 | * them across separate output items; the chat template renders the |
| 4453 | * second assistant record without an `<|Assistant|>` prefix, leaving |
| 4454 | * the tool call bare. Merge into the previous assistant message |
| 4455 | * when nothing user-like / tool-output-like came between them. */ |
| 4456 | chat_msg *last = msgs->len ? &msgs->v[msgs->len - 1] : NULL; |
| 4457 | if (last && !strcmp(last->role, "assistant")) { |
| 4458 | if (pending_reasoning.len && (!last->reasoning || !last->reasoning[0])) { |
| 4459 | free(last->reasoning); |
| 4460 | last->reasoning = buf_take(&pending_reasoning); |
| 4461 | } |
| 4462 | tool_calls_push(&last->calls, tc); |
| 4463 | } else { |
| 4464 | chat_msg msg = {0}; |
| 4465 | msg.role = xstrdup("assistant"); |
| 4466 | msg.content = xstrdup(""); |
| 4467 | if (pending_reasoning.len) msg.reasoning = buf_take(&pending_reasoning); |
| 4468 | tool_calls_push(&msg.calls, tc); |
| 4469 | chat_msgs_push(msgs, msg); |
| 4470 | } |
| 4471 | } else if (!strcmp(t, "function_call_output") || !strcmp(t, "custom_tool_call_output")) { |
| 4472 | chat_msg msg = {0}; |
| 4473 | msg.role = xstrdup("tool"); |
| 4474 | msg.content = output ? output : xstrdup(""); |
| 4475 | output = NULL; |
| 4476 | msg.images = output_images; |
| 4477 | memset(&output_images, 0, sizeof(output_images)); |
| 4478 | if (call_id || item_id) { |
| 4479 | chat_msg_add_tool_call_id(&msg, call_id ? call_id : item_id); |
| 4480 | } |
| 4481 | chat_msgs_push(msgs, msg); |
| 4482 | } else if (!strcmp(t, "reasoning")) { |
| 4483 | /* Stash so it merges into the next assistant message. summary is the |
| 4484 | * short-form list, content is the verbose chain. Either can be empty. */ |
| 4485 | if (summary && summary[0]) { |
| 4486 | if (pending_reasoning.len) buf_putc(&pending_reasoning, '\n'); |
| 4487 | buf_puts(&pending_reasoning, summary); |
| 4488 | } |
| 4489 | if (content && content[0]) { |
| 4490 | if (pending_reasoning.len) buf_putc(&pending_reasoning, '\n'); |
| 4491 | buf_puts(&pending_reasoning, content); |
| 4492 | } |
| 4493 | } else if (!strcmp(t, "local_shell_call") || !strcmp(t, "web_search_call") || |
| 4494 | !strcmp(t, "tool_search_call") || !strcmp(t, "image_generation_call")) |
| 4495 | { |
| 4496 | /* Hosted-tool history isn't natively supported (DS4 doesn't register |
| 4497 | * these tools), but a Codex client may still replay them when the |
| 4498 | * model used them in a prior turn. Surface them as function_call |
| 4499 | * shaped history so the next prompt retains the action that ran. */ |
| 4500 | tool_call tc = {0}; |
| 4501 | tc.id = xstrdup(call_id ? call_id : item_id ? item_id : ""); |
| 4502 | if (!strcmp(t, "tool_search_call")) { |
| 4503 | tc.name = xstrdup("tool_search"); |
| 4504 | } else if (!strcmp(t, "local_shell_call")) { |