[1.4.0] - 2026-10-26
Added
- Added
ToolCallError, which can be imported fromcontextgem: the exception that a tool function raises to tell the LLM why a call failed, e.g. that no record has the ID that the LLM passed. Its message is sent to the LLM as the result of the tool call, and the LLM can call the tool again (see "Changed"). See Errors in tool calls.
Changed
- An exception that a tool function raises, other than
ToolCallError, now stops the chat: it is raised fromchat()andchat_async()as it is, with a note that names the tool, no further requests are sent, and no fallback LLM is tried. Its message was sent to the LLM, as the result of the tool call, and the chat went on, so that a bug in a tool, e.g. an error of a database, went unnoticed but in the answer, and its message, which can hold anything, reached the LLM. To send an error to the LLM, raiseToolCallErrorwith a message written for it, e.g.raise ToolCallError(f"No order has the ID {order_id!r}"), where the function raised another exception for the LLM. The result of the tool call that the LLM receives for it has"error": "ToolCallError", where it had"error": "ToolExecutionError": change a stand-in for the LLM in your tests that checks it. - A sync tool function now runs in a worker thread, as
asyncio.to_thread()runs it, where it ran in the event loop of the chat and blocked it: inchat_async(), the other tasks of the event loop of your code now run while it does, and withparallel_tool_calls=True, the sync tool calls of a response now run at the same time, where they ran one after another. No event loop runs in its thread, so that it can use a library that refuses to run where one is running, e.g. the ORM of Django, which raisedSynchronousOnlyOperationin a tool. The function no longer runs in the thread of the code that callschat(): it does not see what that code keeps for its thread, e.g. an open database transaction, while it gets its context variables. A function that changes state that parallel tool calls share must be thread-safe. See Sync and async tool functions. - A message sent on a
ChatSessionwhile it answers another, e.g. from another task withasyncio.gather()or from another thread, now raisesValueError, before any request is sent. Such turns were answered at the same time: the turn sent second was also sent the message of the first, which had no answer yet, the history kept the two messages one after the other and then the two answers, and the tool calls of one turn were lost, or kept in the history with the answer of the other. Where two messages of a conversation can come at once, e.g. in a server, wait for the answer to one before sending the next, e.g. with a lock for each session: anasyncio.Lockwhere the messages come from the tasks of one event loop, athreading.Lockwhere they come from threads.
Fixed
- The description of a tool no longer splits a first sentence that goes on in the next line of the docstring: the first line was taken as the summary and the rest as the description, which were joined with a blank line, so that the LLM was sent e.g.
Looks up an order: its status and\n\ndelivery date.. The first paragraph is now kept together. Only the tool descriptions of such docstrings change. register_tool()now raisesTypeErrorfor a bound method or afunctools.partial, which cannot be registered, with what to do instead: a function that uses the object, e.g. a closure. A bound method raisedAttributeError('method' object has no attribute '__contextgem_tool__'), and a partial an error on its parameters or on the name of the tool.