Skip to content

Commit 5ecac03

Browse files
committed
bugfix: 修复codeexecute 执行和tool冲突的问题
- 如果执行codeexecute, 再次执行 tool,会导致tool的数据丢失
1 parent 532bbcf commit 5ecac03

8 files changed

Lines changed: 48 additions & 59 deletions

tests/code_executors/container/test_container_code_executor.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -321,11 +321,12 @@ async def test_exec_run_exception_propagates(self, mock_cc_cls, mock_ctx):
321321
class TestCodeBlockDelimiter:
322322

323323
@patch("trpc_agent_sdk.code_executors.container._container_code_executor.ContainerClient")
324-
def test_returns_correct_delimiter(self, mock_cc_cls):
324+
def test_returns_default_delimiters(self, mock_cc_cls):
325325
mock_cc_cls.return_value = Mock()
326326
executor = ContainerCodeExecutor(image="img")
327-
delim = executor.code_block_delimiter()
327+
delims = executor.code_block_delimiters
328328

329-
assert isinstance(delim, CodeBlockDelimiter)
330-
assert delim.start == "```tool_code\n"
331-
assert delim.end == "\n```"
329+
assert isinstance(delims, list)
330+
assert all(isinstance(d, CodeBlockDelimiter) for d in delims)
331+
assert delims[0].start == "```tool_code\n"
332+
assert delims[0].end == "\n```"

tests/code_executors/local/test_unsafe_local_code_executor.py

Lines changed: 10 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -62,27 +62,22 @@ def test_custom_clean_temp_files(self):
6262
executor = UnsafeLocalCodeExecutor(clean_temp_files=False)
6363
assert executor.clean_temp_files is False
6464

65-
def test_custom_delimiter(self):
66-
delim = CodeBlockDelimiter(start="<<<", end=">>>")
67-
executor = UnsafeLocalCodeExecutor(delimiter=delim)
68-
assert executor.delimiter.start == "<<<"
69-
assert executor.delimiter.end == ">>>"
70-
7165

7266
class TestCodeBlockDelimiter:
73-
"""Tests for code_block_delimiter method."""
67+
"""Tests for code block delimiter configuration."""
7468

75-
def test_returns_default_delimiter(self):
69+
def test_returns_default_delimiters(self):
7670
executor = UnsafeLocalCodeExecutor()
77-
delimiter = executor.code_block_delimiter()
78-
assert isinstance(delimiter, CodeBlockDelimiter)
79-
assert delimiter.start == "```"
80-
assert delimiter.end == "```"
71+
delimiters = executor.code_block_delimiters
72+
assert isinstance(delimiters, list)
73+
assert all(isinstance(d, CodeBlockDelimiter) for d in delimiters)
74+
assert delimiters[0].start == "```tool_code\n"
75+
assert delimiters[0].end == "\n```"
8176

82-
def test_returns_custom_delimiter(self):
77+
def test_returns_custom_delimiters(self):
8378
custom = CodeBlockDelimiter(start="---", end="---")
84-
executor = UnsafeLocalCodeExecutor(delimiter=custom)
85-
assert executor.code_block_delimiter() == custom
79+
executor = UnsafeLocalCodeExecutor(code_block_delimiters=[custom])
80+
assert executor.code_block_delimiters == [custom]
8681

8782

8883
class TestPrepareWorkDir:

tests/code_executors/test_container_container_code_executor.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -193,16 +193,17 @@ async def test_execute_code_exception_handling(self, mock_container_client_class
193193
await executor.execute_code(self.mock_ctx, code_input)
194194

195195
@patch('trpc_agent_sdk.code_executors.container._container_code_executor.ContainerClient')
196-
def test_code_block_delimiter(self, mock_container_client_class):
197-
"""Test code_block_delimiter method."""
196+
def test_code_block_delimiters(self, mock_container_client_class):
197+
"""Test default code_block_delimiters value."""
198198
mock_container_client = Mock()
199199
mock_container_client_class.return_value = mock_container_client
200200

201201
executor = ContainerCodeExecutor(image="python:3-slim")
202-
delimiter = executor.code_block_delimiter()
202+
delimiters = executor.code_block_delimiters
203203

204-
assert delimiter.start == "```tool_code\n"
205-
assert delimiter.end == "\n```"
204+
assert isinstance(delimiters, list)
205+
assert delimiters[0].start == "```tool_code\n"
206+
assert delimiters[0].end == "\n```"
206207

207208
@patch('trpc_agent_sdk.code_executors.container._container_code_executor.ContainerClient')
208209
async def test_execute_code_empty_language_defaults_to_python(self, mock_container_client_class):

tests/code_executors/test_local_unsafe_local_code_executor.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,13 @@ def test_init_with_custom_values(self):
5959
assert executor.timeout == 30.0
6060
assert executor.clean_temp_files is False
6161

62-
def test_code_block_delimiter(self):
63-
"""Test code_block_delimiter method."""
62+
def test_code_block_delimiters(self):
63+
"""Test default code_block_delimiters value."""
6464
executor = UnsafeLocalCodeExecutor()
65-
delimiter = executor.code_block_delimiter()
65+
delimiters = executor.code_block_delimiters
6666

67-
assert isinstance(delimiter, CodeBlockDelimiter)
67+
assert isinstance(delimiters, list)
68+
assert all(isinstance(delimiter, CodeBlockDelimiter) for delimiter in delimiters)
6869

6970
@patch('trpc_agent_sdk.code_executors.local._unsafe_local_code_executor.async_execute_command')
7071
async def test_execute_code_python(self, mock_async_execute):

trpc_agent_sdk/agents/core/_code_execution_processor.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -248,19 +248,16 @@ async def _run_post_processor(
248248
if not llm_response or not llm_response.content:
249249
return
250250

251-
# For container and unsafe local executors, we handle execution in post-processing
252-
if isinstance(code_executor, (ContainerCodeExecutor, UnsafeLocalCodeExecutor)):
253-
# Continue with post-processing for these executors
254-
pass
255-
256251
code_executor_context = CodeExecutorContext(invocation_context.session.state)
257252
# Skip if the error count exceeds the max retry attempts.
258253
if code_executor_context.get_error_count(invocation_context.invocation_id) >= code_executor.error_retry_attempts:
259254
return
260255

261-
# [Step 1] Extract code from the model predict response and truncate the
262-
# content to the part with the first code block.
263-
response_content = llm_response.content
256+
# [Step 1] Extract code from a cloned response content.
257+
# IMPORTANT: extract_code_and_truncate_content mutates the input Content in place.
258+
# Clone first to avoid mutating shared references that are later used by
259+
# telemetry tracing and session persistence.
260+
response_content = copy.deepcopy(llm_response.content)
264261
code_blocks = CodeExecutionUtils.extract_code_and_truncate_content(response_content,
265262
code_executor.code_block_delimiters,
266263
code_executor.ignore_codes)
@@ -302,9 +299,20 @@ async def _run_post_processor(
302299
code_execution_result)
303300
yield result_event
304301

305-
# [Step 3] Skip processing the original model response
306-
# to continue code generation loop.
307-
llm_response.content = None
302+
# [Step 3] Skip processing original textual/code response to continue the
303+
# code generation loop, but preserve function_call parts for downstream
304+
# session consistency (pairing function_call with function_response).
305+
function_call_parts = []
306+
if llm_response.content and llm_response.content.parts:
307+
function_call_parts = [part for part in llm_response.content.parts if part.function_call]
308+
309+
if function_call_parts:
310+
llm_response.content = Content(
311+
role=llm_response.content.role if llm_response.content else "model",
312+
parts=function_call_parts,
313+
)
314+
else:
315+
llm_response.content = None
308316

309317

310318
def _extract_and_replace_inline_files(

trpc_agent_sdk/code_executors/_base_code_executor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,10 @@ async def execute_code(
100100
The code execution result.
101101
"""
102102

103-
@abc.abstractmethod
104-
def code_block_delimiter(self) -> CodeBlockDelimiter:
103+
def code_block_delimiter(self) -> list[CodeBlockDelimiter]:
105104
"""Return the code block delimiter used by this executor.
106105
107106
Returns:
108107
CodeBlockDelimiter instance
109108
"""
109+
return self.code_block_delimiters

trpc_agent_sdk/code_executors/container/_container_code_executor.py

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -149,12 +149,3 @@ async def execute_code(
149149
output = "".join(all_output)
150150
err_str = "".join(all_errors)
151151
return create_code_execution_result(stdout=output, stderr=err_str)
152-
153-
@override
154-
def code_block_delimiter(self) -> CodeBlockDelimiter:
155-
"""Return the code block delimiter used by this executor.
156-
157-
Returns:
158-
CodeBlockDelimiter instance
159-
"""
160-
return CodeBlockDelimiter(start="```tool_code\n", end="\n```")

trpc_agent_sdk/code_executors/local/_unsafe_local_code_executor.py

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,6 @@ class UnsafeLocalCodeExecutor(BaseCodeExecutor):
4848
clean_temp_files: bool = Field(default=True,
4949
description="Whether to clean temporary files after the code execution.")
5050

51-
delimiter: CodeBlockDelimiter = Field(default_factory=CodeBlockDelimiter,
52-
description="The delimiter for the code execution.")
53-
5451
def __init__(self, **data):
5552
"""Initialize the UnsafeLocalCodeExecutor."""
5653
if "stateful" in data and data["stateful"]:
@@ -97,11 +94,6 @@ async def execute_code(self, invocation_context: InvocationContext,
9794
return create_code_execution_result(stdout="\n".join(output_parts) if output_parts else "",
9895
stderr="\n".join(error_parts) if error_parts else "")
9996

100-
@override
101-
def code_block_delimiter(self) -> CodeBlockDelimiter:
102-
"""Return the code block delimiter used by this executor."""
103-
return self.delimiter
104-
10597
def _prepare_work_dir(self, execution_id: str) -> tuple[Path, bool]:
10698
"""Prepare working directory for execution.
10799

0 commit comments

Comments
 (0)