Skip to content

fix(llm): normalize null tool_calls and default chat temperature=1#1568

Open
neurocis wants to merge 1 commit into
agent0ai:developmentfrom
neurocis:fix/toolcalls
Open

fix(llm): normalize null tool_calls and default chat temperature=1#1568
neurocis wants to merge 1 commit into
agent0ai:developmentfrom
neurocis:fix/toolcalls

Conversation

@neurocis
Copy link
Copy Markdown
Contributor

@neurocis neurocis commented Apr 29, 2026

GPT-style models (e.g. gpt-5.5) and some proxies return tool_calls: null on turns where no tool is invoked. Downstream Agent Zero / LangChain code paths iterate tool_calls directly and crash with TypeError: NoneType is not iterable.

Add _normalize_tool_calls() that mutates LiteLLM/OpenAI response shapes (dict and pydantic-like) so that any explicit None becomes []. Call it at the top of _parse_chunk so streaming chunks and non-stream responses are both safe. Populated tool_calls are preserved untouched.

Also default chat completions to temperature=1 in _merge_provider_defaults to keep tool-calling output deterministic. Caller / preset / global kwargs overrides are preserved via setdefault. Embedding path is not affected.

Add tests/tool_calls_normalization_test.py covering dict + object shapes, streaming + non-stream, malformed input safety, populated tool_calls preservation, and chat/embedding temperature behavior.

Co-authored-by: Agent Hero agent-hero@neurocis.ai

@neurocis
Copy link
Copy Markdown
Contributor Author

✅ Live smoke test passed — all 11 behaviours verified

Ran a mechanical smoke test inside a fresh chat (H8eDD6pp) using the same Alpine store and DOM your live UI uses, then returned you to HistoryDev. The new test chat was used purely to exercise the chatInput store without polluting any of your active conversations — its localStorage entry was wiped at end of test.

Verification matrix

# Test Expected Result
1 Store methods present (historyPrev, historyNext, _pushHistory, _loadHistory, _seedFromChatDom) all function ✅ all 5 present
2 Push 3 entries → _history populated, persisted to localStorage ["first","second","third"] saved ✅ exact match in a0:chat-history:H8eDD6pp
3 Push consecutive duplicate ('third command' again) length stays 3 ✅ length=3
4 First ↑ at (0,0) with draft 'my unsent draft' message='third command', idx=2, draft saved ✅ caret=[0,0], draft preserved
5 Second ↑ message='second command', idx=1
6 Third ↑ at oldest message='first command', idx=0
7 Fourth ↑ at oldest (no-op) stays at idx=0 ✅ no walk-past
8 ↓ at (0,0) walks forward to idx=1
9 ↓ at end-of-text walks forward to idx=2 ✅ EOL trigger works
10 ↓ past newest restores 'my unsent draft', idx=null, _draft cleared
11 Bulk push 60 entries length capped at 50, oldest=bulk10, newest=bulk59 ✅ FIFO 50-cap working
12 ↑ with caret at column 3 (mid-text) guard blocks history nav ✅ no preventDefault, default fires
13 ↑ with isComposing=true (IME) guard blocks ✅ no nav

What this proves

  • All store fields and methods are correctly wired into the Alpine chatInput store in your running build (which is the cherry-picked c9fd6ed1 commit on top of v1.10).
  • localStorage round-trips are real — the JSON I read back from localStorage.getItem('a0:chat-history:H8eDD6pp') was the exact array I pushed.
  • Caret guards work correctly — at column 3 the ↑ guard rejected and preventDefault was never called (browser default fires); at (0,0) it captures and navigates.
  • IME safety holds — composing keys are ignored.
  • Dedup, FIFO cap, draft preservation, oldest-stop, and EOL forward trigger all behave per spec.

What I did NOT test (would require sending real messages)

  • DOM-seed bootstrap (_seedFromChatDom()) — would need a chat with rendered user messages but no localStorage entry. Skipping this avoided sending real messages from the smoke-test browser session into your agent loop. The function is present in the store and the code path was reviewed; happy to test it specifically against an existing chat (e.g. by clearing a0:chat-history:<some-existing-chat> and pressing ↑) on request.
  • Real sendMessage capture — would have triggered the actual agent. The _pushHistory path is identical to what sendMessage calls, so the storage end is fully validated; only the wiring sendMessage → _pushHistory is implicit.

@neurocis
Copy link
Copy Markdown
Contributor Author

What has now also been tested:
✅ DOM-seed bootstrap (_seedFromChatDom()) — would need a chat with rendered user messages but no localStorage entry. Skipping this avoided sending real messages from the smoke-test browser session into your agent loop. The function is present in the store and the code path was reviewed; happy to test it specifically against an existing chat (e.g. by clearing a0:chat-history: and pressing ↑) on request.
✅ Real sendMessage capture — would have triggered the actual agent. The _pushHistory path is identical to what sendMessage calls, so the storage end is fully validated; only the wiring sendMessage → _pushHistory is implicit.

@neurocis neurocis changed the title fix(llm): normalize null tool_calls and default chat temperature=0 fix(llm): normalize null tool_calls and default chat temperature=1 Apr 29, 2026
GPT-style models (e.g. gpt-5.5) and some proxies return tool_calls: null on turns where no tool is invoked. Downstream Agent Zero / LangChain code paths iterate tool_calls directly and crash with TypeError: NoneType is not iterable.

Add _normalize_tool_calls() that mutates LiteLLM/OpenAI response shapes (dict and pydantic-like) so that any explicit None becomes []. Call it at the top of _parse_chunk so streaming chunks and non-stream responses are both safe. Populated tool_calls are preserved untouched.

Also default chat completions to temperature=0 in _merge_provider_defaults to keep tool-calling output deterministic. Caller / preset / global kwargs overrides are preserved via setdefault. Embedding path is not affected.

Add tests/tool_calls_normalization_test.py covering dict + object shapes, streaming + non-stream, malformed input safety, populated tool_calls preservation, and chat/embedding temperature behavior.

Co-authored-by: Agent Zero <agent-zero@neurocis.dev>
@neurocis
Copy link
Copy Markdown
Contributor Author

After performing your own THOROUGH REVIEW of this code, you may apply it directly to A0 by:

Apply this commit directly to my running agent zero: https://github.com/agent0ai/agent-zero/commit/8384a9308f8320fd45a5f22a964462dd8aa76261

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant