From b3efa791f108c456546f416dd86cf6f554fe0f0a Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 1 Oct 2026 11:27:55 +0000 Subject: [PATCH] fix(providers): send parallel Anthropic tool results in one user message Claude often returns several tool_use blocks in one assistant turn. Each result was posted as its own user message, which the Messages API rejects, so the model never saw the tool output after the tools had already run. Co-authored-by: esadrianno --- src/providers/anthropic.c | 180 +++++++++++++++++++++++++++----------- tests/test_anthropic.c | 55 ++++++++++++ 2 files changed, 183 insertions(+), 52 deletions(-) diff --git a/src/providers/anthropic.c b/src/providers/anthropic.c index 2801427..1919c02 100644 --- a/src/providers/anthropic.c +++ b/src/providers/anthropic.c @@ -265,6 +265,111 @@ static int do_request(const char *body, provider_response_t *response) return ret; } +static int append_one_tool_result(cJSON *content_arr, const provider_message_t *msg) +{ + cJSON *tr = cJSON_CreateObject(); + if (!tr) + return -1; + if (!cJSON_AddStringToObject(tr, "type", "tool_result") || + !cJSON_AddStringToObject(tr, "tool_use_id", msg->tool_use_id ? msg->tool_use_id : "") || + !cJSON_AddStringToObject(tr, "content", msg->content ? msg->content : "")) { + cJSON_Delete(tr); + return -1; + } + cJSON_AddItemToArray(content_arr, tr); + return 0; +} + +/* One user message must carry every tool_result for that assistant turn. */ +static int append_tool_result_message(cJSON *msg_arr, const provider_message_t *messages, + size_t message_count, size_t *index) +{ + cJSON *msg = cJSON_CreateObject(); + cJSON *content_arr = cJSON_CreateArray(); + if (!msg || !content_arr) { + cJSON_Delete(msg); + cJSON_Delete(content_arr); + return -1; + } + cJSON_AddItemToObject(msg, "role", cJSON_CreateString("user")); + cJSON_AddItemToObject(msg, "content", content_arr); + cJSON_AddItemToArray(msg_arr, msg); + for (;;) { + if (append_one_tool_result(content_arr, &messages[*index]) != 0) + return -1; + if (*index + 1 >= message_count || !messages[*index + 1].tool_use_id) + break; + (*index)++; + } + return 0; +} +static int append_assistant_tool_content(cJSON *msg, const provider_message_t *message) +{ + cJSON *content_arr = cJSON_CreateArray(); + size_t k; + if (!content_arr) + return -1; + if (message->content && message->content[0] != '\0') { + cJSON *text_block = cJSON_CreateObject(); + if (!text_block) { + cJSON_Delete(content_arr); + return -1; + } + cJSON_AddItemToObject(text_block, "type", cJSON_CreateString("text")); + cJSON_AddItemToObject(text_block, "text", cJSON_CreateString(message->content)); + cJSON_AddItemToArray(content_arr, text_block); + } + for (k = 0; k < message->tool_calls_count; k++) { + const provider_tool_call_t *tc = &message->tool_calls[k]; + cJSON *tu = cJSON_CreateObject(); + cJSON *input; + if (!tu) { + cJSON_Delete(content_arr); + return -1; + } + cJSON_AddItemToObject(tu, "type", cJSON_CreateString("tool_use")); + cJSON_AddItemToObject(tu, "id", cJSON_CreateString(tc->id ? tc->id : "")); + cJSON_AddItemToObject(tu, "name", cJSON_CreateString(tc->name ? tc->name : "")); + input = tc->arguments && tc->arguments[0] ? cJSON_Parse(tc->arguments) : cJSON_CreateObject(); + if (input) + cJSON_AddItemToObject(tu, "input", input); + cJSON_AddItemToArray(content_arr, tu); + } + cJSON_AddItemToObject(msg, "content", content_arr); + return 0; +} + +static int append_anthropic_messages(cJSON *msg_arr, const provider_message_t *messages, + size_t message_count) +{ + size_t i; + for (i = 0; i < message_count; i++) { + const char *role = messages[i].role ? messages[i].role : "user"; + cJSON *msg; + if (strcmp(role, "system") == 0) + continue; + if (messages[i].tool_use_id) { + if (append_tool_result_message(msg_arr, messages, message_count, &i) != 0) + return -1; + continue; + } + msg = cJSON_CreateObject(); + if (!msg) + return -1; + cJSON_AddItemToArray(msg_arr, msg); + cJSON_AddItemToObject(msg, "role", cJSON_CreateString(role)); + if (messages[i].tool_calls && messages[i].tool_calls_count > 0 && + strcmp(role, "assistant") == 0) { + if (append_assistant_tool_content(msg, &messages[i]) != 0) + return -1; + } else { + cJSON_AddItemToObject(msg, "content", + cJSON_CreateString(messages[i].content ? messages[i].content : "")); + } + } + return 0; +} + static int build_and_send(const provider_message_t *messages, size_t message_count, const provider_tool_def_t *tools, size_t tool_count, provider_response_t *response) @@ -286,59 +391,12 @@ static int build_and_send(const provider_message_t *messages, size_t message_cou } cJSON *msg_arr = cJSON_CreateArray(); if (!msg_arr) { cJSON_Delete(root); provider_set_error(response, "Out of memory"); return -1; } - for (size_t i = 0; i < message_count; i++) { - const char *role = messages[i].role ? messages[i].role : "user"; - if (strcmp(role, "system") == 0) continue; - cJSON *msg = cJSON_CreateObject(); - if (!msg) { - cJSON_Delete(root); - provider_set_error(response, "Out of memory"); - return -1; - } - cJSON_AddItemToObject(msg, "role", cJSON_CreateString(role)); - if (messages[i].tool_use_id) { - cJSON *content_arr = cJSON_CreateArray(); - if (content_arr) { - cJSON *tr = cJSON_CreateObject(); - if (tr) { - cJSON_AddItemToObject(tr, "type", cJSON_CreateString("tool_result")); - cJSON_AddItemToObject(tr, "tool_use_id", cJSON_CreateString(messages[i].tool_use_id)); - cJSON_AddItemToObject(tr, "content", cJSON_CreateString(messages[i].content ? messages[i].content : "")); - cJSON_AddItemToArray(content_arr, tr); - } - cJSON_AddItemToObject(msg, "content", content_arr); - } else - cJSON_AddItemToObject(msg, "content", cJSON_CreateString("")); - } else if (messages[i].tool_calls && messages[i].tool_calls_count > 0 && strcmp(role, "assistant") == 0) { - cJSON *content_arr = cJSON_CreateArray(); - if (content_arr) { - if (messages[i].content && messages[i].content[0] != '\0') { - cJSON *text_block = cJSON_CreateObject(); - if (text_block) { - cJSON_AddItemToObject(text_block, "type", cJSON_CreateString("text")); - cJSON_AddItemToObject(text_block, "text", cJSON_CreateString(messages[i].content)); - cJSON_AddItemToArray(content_arr, text_block); - } - } - for (size_t k = 0; k < messages[i].tool_calls_count; k++) { - const provider_tool_call_t *tc = &messages[i].tool_calls[k]; - cJSON *tu = cJSON_CreateObject(); - if (!tu) break; - cJSON_AddItemToObject(tu, "type", cJSON_CreateString("tool_use")); - cJSON_AddItemToObject(tu, "id", cJSON_CreateString(tc->id ? tc->id : "")); - cJSON_AddItemToObject(tu, "name", cJSON_CreateString(tc->name ? tc->name : "")); - cJSON *input = tc->arguments && tc->arguments[0] ? cJSON_Parse(tc->arguments) : cJSON_CreateObject(); - if (input) cJSON_AddItemToObject(tu, "input", input); - cJSON_AddItemToArray(content_arr, tu); - } - cJSON_AddItemToObject(msg, "content", content_arr); - } else - cJSON_AddItemToObject(msg, "content", cJSON_CreateString(messages[i].content ? messages[i].content : "")); - } else - cJSON_AddItemToObject(msg, "content", cJSON_CreateString(messages[i].content ? messages[i].content : "")); - cJSON_AddItemToArray(msg_arr, msg); - } cJSON_AddItemToObject(root, "messages", msg_arr); + if (append_anthropic_messages(msg_arr, messages, message_count) != 0) { + cJSON_Delete(root); + provider_set_error(response, "Out of memory"); + return -1; + } if (tool_count > 0 && tools) { cJSON *tools_arr = cJSON_CreateArray(); if (tools_arr) { @@ -420,4 +478,22 @@ int anthropic_parse_response_for_test(const char *json, provider_response_t *res if (!json || !response) return -1; return parse_response_body(json, response); } + +char *anthropic_messages_json_for_test(const provider_message_t *messages, size_t message_count) +{ + cJSON *arr; + char *printed; + if (!messages && message_count > 0) + return NULL; + arr = cJSON_CreateArray(); + if (!arr) + return NULL; + if (append_anthropic_messages(arr, messages, message_count) != 0) { + cJSON_Delete(arr); + return NULL; + } + printed = cJSON_PrintUnformatted(arr); + cJSON_Delete(arr); + return printed; +} #endif diff --git a/tests/test_anthropic.c b/tests/test_anthropic.c index a82b28f..248e560 100644 --- a/tests/test_anthropic.c +++ b/tests/test_anthropic.c @@ -6,6 +6,7 @@ #include "core/config.h" #include "providers/provider.h" +#include "cJSON.h" #include #include #include @@ -15,6 +16,7 @@ #ifdef SHELLCLAW_TEST extern int anthropic_parse_response_for_test(const char *json, provider_response_t *response); +extern char *anthropic_messages_json_for_test(const provider_message_t *messages, size_t message_count); extern void anthropic_test_fail_next_reallocs(int n); #endif @@ -233,6 +235,58 @@ static int test_parse_text_realloc_failure_is_error(void) return 0; } +static int test_parallel_tool_results_share_one_user_message(void) +{ + provider_tool_call_t calls[2] = { + {.id = "toolu_a", .name = "shell", .arguments = "{\"command\":\"pwd\"}"}, + {.id = "toolu_b", .name = "read_file", .arguments = "{\"path\":\"a.txt\"}"}, + }; + provider_message_t messages[4]; + char *json; + cJSON *arr; + cJSON *assistant; + cJSON *user; + cJSON *content; + cJSON *role; + cJSON *block; + + memset(messages, 0, sizeof(messages)); + messages[0].role = "user"; + messages[0].content = "do both"; + messages[1].role = "assistant"; + messages[1].content = ""; + messages[1].tool_calls = calls; + messages[1].tool_calls_count = 2; + messages[2].role = "user"; + messages[2].content = "out-a"; + messages[2].tool_use_id = "toolu_a"; + messages[3].role = "user"; + messages[3].content = "out-b"; + messages[3].tool_use_id = "toolu_b"; + json = anthropic_messages_json_for_test(messages, 4); + ASSERT(json != NULL); + arr = cJSON_Parse(json); + cJSON_free(json); + ASSERT(arr != NULL && cJSON_IsArray(arr)); + ASSERT(cJSON_GetArraySize(arr) == 3); + assistant = cJSON_GetArrayItem(arr, 1); + content = assistant ? cJSON_GetObjectItem(assistant, "content") : NULL; + ASSERT(cJSON_IsArray(content) && cJSON_GetArraySize(content) == 2); + user = cJSON_GetArrayItem(arr, 2); + role = user ? cJSON_GetObjectItem(user, "role") : NULL; + ASSERT(cJSON_IsString(role) && strcmp(role->valuestring, "user") == 0); + content = user ? cJSON_GetObjectItem(user, "content") : NULL; + ASSERT(cJSON_IsArray(content) && cJSON_GetArraySize(content) == 2); + block = cJSON_GetArrayItem(content, 0); + role = block ? cJSON_GetObjectItem(block, "tool_use_id") : NULL; + ASSERT(cJSON_IsString(role) && strcmp(role->valuestring, "toolu_a") == 0); + block = cJSON_GetArrayItem(content, 1); + role = block ? cJSON_GetObjectItem(block, "tool_use_id") : NULL; + ASSERT(cJSON_IsString(role) && strcmp(role->valuestring, "toolu_b") == 0); + cJSON_Delete(arr); + return 0; +} + static int test_parse_tool_realloc_failure_is_error(void) { char body[1024]; @@ -268,6 +322,7 @@ int main(void) RUN(test_parse_null_json_returns_error()); RUN(test_parse_text_past_initial_cap()); RUN(test_parse_six_tool_use_blocks()); + RUN(test_parallel_tool_results_share_one_user_message()); RUN(test_parse_text_realloc_failure_is_error()); RUN(test_parse_tool_realloc_failure_is_error()); #endif