diff --git a/ds4_server.c b/ds4_server.c index 45a53469c..1e04168e0 100644 --- a/ds4_server.c +++ b/ds4_server.c @@ -3472,7 +3472,9 @@ static bool parse_responses_content_array(const char **p, char **out) { * render_chat_prompt_text can wrap them in . */ static bool parse_responses_input(const char **p, chat_msgs *msgs, buf *loaded_tool_schemas, - tool_schema_orders *orders) { + tool_schema_orders *orders, + bool *last_was_compaction) { + if (last_was_compaction) *last_was_compaction = false; json_ws(p); if (**p != '[') return false; (*p)++; @@ -3720,6 +3722,7 @@ static bool parse_responses_input(const char **p, chat_msgs *msgs, !strcmp(t, "tool_search_call") || !strcmp(t, "image_generation_call"); bool is_bookkeeping = !strcmp(t, "compaction") || !strcmp(t, "context_compaction"); + if (last_was_compaction) *last_was_compaction = is_bookkeeping; if (!consumes_reasoning && !is_bookkeeping && pending_reasoning.len) { chat_msg flush_msg = {0}; flush_msg.role = xstrdup("assistant"); @@ -4021,6 +4024,7 @@ static bool parse_responses_request(ds4_engine *e, server *s, const char *body, const char *p = body; bool got_input = false; bool tool_choice_none = false; + bool last_was_compaction = false; bool got_thinking = false; bool thinking_enabled = true; ds4_think_mode reasoning_effort = DS4_THINK_HIGH; @@ -4058,7 +4062,7 @@ static bool parse_responses_request(ds4_engine *e, server *s, const char *body, msg.content = plain; chat_msgs_push(&msgs, msg); } else if (!parse_responses_input(&p, &msgs, &loaded_tool_schemas, - &r->tool_orders)) { + &r->tool_orders, &last_was_compaction)) { free(key); goto bad; } @@ -4237,8 +4241,12 @@ static bool parse_responses_request(ds4_engine *e, server *s, const char *body, buf_append(&combined_tool_schemas, loaded_tool_schemas.ptr, loaded_tool_schemas.len); } + /* Compaction requests must not prime the model for tools at all: with + * schemas still in the prompt the model keeps emitting DSML stanzas, and + * with has_tools disabled nothing parses them, so they leak raw into the + * summary text. Drop the schemas too, like the tool-less chat path. */ const char *active_tool_schemas = - (!tool_choice_none && combined_tool_schemas.len) ? + (!tool_choice_none && !last_was_compaction && combined_tool_schemas.len) ? combined_tool_schemas.ptr : NULL; r->has_tools = active_tool_schemas && active_tool_schemas[0]; if (!got_thinking && model_alias_disables_thinking(r->model)) thinking_enabled = false; @@ -13371,7 +13379,7 @@ static void test_responses_input_tool_search_output_loads_tools(void) { chat_msgs msgs = {0}; buf loaded = {0}; tool_schema_orders orders = {0}; - TEST_ASSERT(parse_responses_input(&p, &msgs, &loaded, &orders)); + TEST_ASSERT(parse_responses_input(&p, &msgs, &loaded, &orders, NULL)); TEST_ASSERT(loaded.ptr && strstr(loaded.ptr, "\"name\":\"mcp__perplexity__perplexity_search\"")); const tool_schema_order *order = tool_schema_orders_find(&orders, "mcp__perplexity__perplexity_search"); @@ -13396,7 +13404,7 @@ static void test_responses_input_tool_search_output_rejects_bad_tools(void) { chat_msgs msgs = {0}; buf loaded = {0}; tool_schema_orders orders = {0}; - TEST_ASSERT(!parse_responses_input(&p, &msgs, &loaded, &orders)); + TEST_ASSERT(!parse_responses_input(&p, &msgs, &loaded, &orders, NULL)); buf_free(&loaded); tool_schema_orders_free(&orders); chat_msgs_free(&msgs); @@ -13419,7 +13427,7 @@ static void test_responses_input_function_call_namespace_round_trips_to_dsml(voi "\"arguments\":{\"query\":\"deepseek\"}}]"; const char *input_p = input_json; chat_msgs msgs = {0}; - TEST_ASSERT(parse_responses_input(&input_p, &msgs, NULL, NULL)); + TEST_ASSERT(parse_responses_input(&input_p, &msgs, NULL, NULL, NULL)); TEST_ASSERT(msgs.len == 1); TEST_ASSERT(msgs.v[0].calls.len == 1); TEST_ASSERT(!strcmp(msgs.v[0].calls.v[0].name, @@ -13437,6 +13445,31 @@ static void test_responses_input_function_call_namespace_round_trips_to_dsml(voi tool_schema_orders_free(&orders); } +static void test_responses_input_compaction_flag_tracks_last_item(void) { + const char *ends_with_compaction = + "[{\"type\":\"message\",\"role\":\"user\",\"content\":\"hi\"}," + "{\"type\":\"compaction\"}]"; + const char *p = ends_with_compaction; + chat_msgs msgs = {0}; + bool last_was_compaction = false; + TEST_ASSERT(parse_responses_input(&p, &msgs, NULL, NULL, &last_was_compaction)); + TEST_ASSERT(last_was_compaction); + chat_msgs_free(&msgs); + + /* A compaction item earlier in the history must not disable tools for + * this request: only the last item decides. Regression test for the + * latch that kept tools off for every turn after the first compaction. */ + const char *compaction_then_user = + "[{\"type\":\"compaction\"}," + "{\"type\":\"message\",\"role\":\"user\",\"content\":\"continue\"}]"; + p = compaction_then_user; + memset(&msgs, 0, sizeof(msgs)); + last_was_compaction = false; + TEST_ASSERT(parse_responses_input(&p, &msgs, NULL, NULL, &last_was_compaction)); + TEST_ASSERT(!last_was_compaction); + chat_msgs_free(&msgs); +} + static void test_responses_output_sends_tool_search_call_item(void) { tool_calls calls = {0}; tool_call tc = {0}; @@ -17433,6 +17466,7 @@ static void ds4_server_unit_tests_run(void) { test_responses_input_tool_search_output_loads_tools(); test_responses_input_tool_search_output_rejects_bad_tools(); test_responses_input_function_call_namespace_round_trips_to_dsml(); + test_responses_input_compaction_flag_tracks_last_item(); test_responses_output_sends_tool_search_call_item(); test_dsml_tool_args_preserve_call_order(); test_openai_tool_args_preserve_call_order();