diff --git a/api/streaming.py b/api/streaming.py index 5b8f4108..e924dc10 100644 --- a/api/streaming.py +++ b/api/streaming.py @@ -7950,7 +7950,7 @@ def _extract_tool_calls_from_messages(messages, live_tool_calls=None): pending_names[tid] = part.get('name', '') pending_args[tid] = part.get('input', {}) pending_asst_idx[tid] = msg_idx - for tc in m.get('tool_calls', []): + for tc in m.get('tool_calls') or []: if not isinstance(tc, dict): continue tid = tc.get('id', '') or tc.get('call_id', '') diff --git a/tests/test_tool_call_persistence.py b/tests/test_tool_call_persistence.py index 050b2443..3f86c127 100644 --- a/tests/test_tool_call_persistence.py +++ b/tests/test_tool_call_persistence.py @@ -33,6 +33,34 @@ def test_extract_tool_calls_from_openai_message_linkage(): assert result[0]["snippet"] == "file.txt" +def test_extract_tool_calls_ignores_explicit_null_tool_calls_in_history(): + """Legacy assistant rows with tool_calls=None must not abort later turns.""" + messages = [ + {"role": "assistant", "content": "An old TickTick response", "tool_calls": None}, + {"role": "user", "content": "continue"}, + { + "role": "assistant", + "content": "", + "tool_calls": [{ + "id": "call-current", + "function": {"name": "terminal", "arguments": '{"command":"pwd"}'}, + }], + }, + { + "role": "tool", + "tool_call_id": "call-current", + "content": '{"output":"/workspace","exit_code":0}', + }, + ] + + result = _extract_tool_calls_from_messages(messages) + + assert len(result) == 1 + assert result[0]["name"] == "terminal" + assert result[0]["assistant_msg_idx"] == 2 + assert result[0]["snippet"] == "/workspace" + + def test_tool_result_snippet_allows_frontend_show_more_threshold_but_stays_bounded(): """Persisted snippets should be long enough for frontend Show more but capped.""" medium_output = "m" * 1200