Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -413,10 +413,18 @@ def _serialize_function_result(content: Content) -> dict[str, Any]:
f"TypeSafe function results support text items only; received unsupported {item.type!r} content."
)
items.append({"type": "text", "text": item.text or ""})
if items:
result = "\n".join(item["text"] for item in items)
elif isinstance(content.result, str):
result = content.result
else:
raise ChatClientInvalidRequestException(
"TypeSafe function results require canonical text items or a string result."
)
return {
"type": "function_result",
"call_id": content.call_id,
"result": content.result,
"result": result,
**({"items": items} if items else {}),
}

Expand Down Expand Up @@ -454,8 +462,15 @@ def _get_current_turn_function_result_texts(messages: Sequence[Message]) -> list
if message.role == "user":
break
for content in reversed(message.contents):
if content.type == "function_result" and isinstance(content.result, str):
results.append(RawTypeSafeChatClient._unwrap_function_result_text(content.result))
if content.type != "function_result":
continue
if content.items:
result = "\n".join(item.text or "" for item in content.items if item.type == "text")
elif isinstance(content.result, str):
result = content.result
else:
continue
results.append(RawTypeSafeChatClient._unwrap_function_result_text(result))
results.reverse()
return results

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,11 +146,25 @@ def compile_tool_call_plan(

compiled: list[_CompiledTool] = []
unsupported: dict[str, str] = {}
compiled_question_count = 0
for index, tool in enumerate(selected_tools):
try:
compiled.append(_compile_tool(tool, index, list((previous_calls or {}).get(tool.name, ()))))
compiled_tool = _compile_tool(tool, index, list((previous_calls or {}).get(tool.name, ())))
except _UnsupportedToolSchema as exc:
unsupported[tool.name] = str(exc)
continue
compiled.append(compiled_tool)
if len(compiled) > MAX_ROUTABLE_TOOLS:
raise ChatClientInvalidRequestException(
f"TypeSafe supports at most {MAX_ROUTABLE_TOOLS} routable tools per request. "
"Use tool_choice.allowed_tools to narrow MCP or local tools."
)
compiled_question_count += len(compiled_tool.questions)
if compiled_question_count > MAX_INTERNAL_QUESTIONS:
raise ChatClientInvalidRequestException(
f"TypeSafe tool schemas generated more than {MAX_INTERNAL_QUESTIONS} internal questions. "
"Narrow the available tools."
)

if unsupported:
for name, reason in unsupported.items():
Expand All @@ -164,11 +178,6 @@ def compile_tool_call_plan(

if not compiled:
return None
if len(compiled) > MAX_ROUTABLE_TOOLS:
raise ChatClientInvalidRequestException(
f"TypeSafe supports at most {MAX_ROUTABLE_TOOLS} routable tools per request. "
"Use tool_choice.allowed_tools to narrow MCP or local tools."
)

required_tool = compiled[0] if mode == "required" and len(compiled) == 1 else None
route_question_id: str | None = None
Expand Down
47 changes: 47 additions & 0 deletions python/packages/typesafe/tests/typesafe/test_chat_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,53 @@ def test_text_function_result_items_are_serialized() -> None:
}


def test_function_result_uses_canonical_items_over_stale_result() -> None:
result = Content(
"function_result",
call_id="call-1",
result="stale",
items=[Content.from_text("canonical")],
)

state = RawTypeSafeChatClient._build_state( # pyright: ignore[reportPrivateUsage]
[Message("user", ["run"]), Message("tool", [result])],
instructions=None,
)

assert state["messages"][1]["contents"][0] == {
"type": "function_result",
"call_id": "call-1",
"result": "canonical",
"items": [{"type": "text", "text": "canonical"}],
}


def test_terminal_text_uses_canonical_function_result_items() -> None:
result = Content(
"function_result",
call_id="call-1",
result="stale",
items=[Content.from_text("canonical")],
)

text = RawTypeSafeChatClient._build_terminal_text( # pyright: ignore[reportPrivateUsage]
[Message("user", ["run"]), Message("tool", [result])],
make_response(),
)

assert text == "canonical"


def test_function_result_without_canonical_text_is_rejected() -> None:
result = Content("function_result", call_id="call-1", result={"value": 1})

with pytest.raises(ChatClientInvalidRequestException, match="canonical text items or a string result"):
RawTypeSafeChatClient._build_state( # pyright: ignore[reportPrivateUsage]
[Message("user", ["run"]), Message("tool", [result])],
instructions=None,
)


def test_simple_mcp_result_wrapper_is_unwrapped() -> None:
result = RawTypeSafeChatClient._get_current_turn_function_result_texts( # pyright: ignore[reportPrivateUsage]
[
Expand Down
21 changes: 21 additions & 0 deletions python/packages/typesafe/tests/typesafe/test_tool_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,27 @@ def test_routable_tool_limit_is_enforced() -> None:
compile_tool_call_plan(tools, tool_mode=None, user_question_ids=set())


def test_routable_tool_limit_stops_compiling_later_tools() -> None:
class CountingFunctionTool(FunctionTool):
compiled_count = 0

def parameters(self) -> dict[str, Any]:
type(self).compiled_count += 1
if type(self).compiled_count > MAX_ROUTABLE_TOOLS + 1:
raise AssertionError("tool compilation continued after the routable limit")
return super().parameters()

tools = [
CountingFunctionTool(name=f"tool_{index}", func=lambda: None, input_model={})
for index in range(MAX_ROUTABLE_TOOLS + 2)
]

with pytest.raises(ChatClientInvalidRequestException, match="at most"):
compile_tool_call_plan(tools, tool_mode=None, user_question_ids=set())

assert CountingFunctionTool.compiled_count == MAX_ROUTABLE_TOOLS + 1


def test_tool_property_limit_is_enforced_before_compilation() -> None:
properties = {f"field_{index}": {"type": "boolean"} for index in range(MAX_TOOL_PROPERTIES + 1)}

Expand Down
Loading