|
15 | 15 | from google.adk.agents.llm_agent import Agent |
16 | 16 | from google.adk.events.event import Event |
17 | 17 | from google.adk.events.event_actions import EventActions |
| 18 | +from google.adk.flows.llm_flows import _nl_planning |
18 | 19 | from google.adk.flows.llm_flows import contents |
19 | 20 | from google.adk.flows.llm_flows.contents import request_processor |
20 | 21 | from google.adk.flows.llm_flows.functions import REQUEST_CONFIRMATION_FUNCTION_CALL_NAME |
@@ -1001,6 +1002,122 @@ async def test_adk_function_call_ids_are_stripped_for_non_interactions_model(): |
1001 | 1002 | assert user_fr_part.function_response.id is None |
1002 | 1003 |
|
1003 | 1004 |
|
| 1005 | +@pytest.mark.asyncio |
| 1006 | +async def test_stripping_function_call_ids_does_not_mutate_session_events(): |
| 1007 | + """Stripping ``adk-`` ids must not mutate the session-owned events.""" |
| 1008 | + agent = Agent(model="gemini-2.5-flash", name="test_agent") |
| 1009 | + llm_request = LlmRequest(model="gemini-2.5-flash") |
| 1010 | + invocation_context = await testing_utils.create_invocation_context( |
| 1011 | + agent=agent |
| 1012 | + ) |
| 1013 | + |
| 1014 | + # Shared id so the history rearrange logic can pair call and response. |
| 1015 | + function_call_id = "adk-test-call-id" |
| 1016 | + fc_part = types.Part( |
| 1017 | + function_call=types.FunctionCall( |
| 1018 | + id=function_call_id, |
| 1019 | + name="test_tool", |
| 1020 | + args={"x": 1}, |
| 1021 | + ) |
| 1022 | + ) |
| 1023 | + fr_part = types.Part( |
| 1024 | + function_response=types.FunctionResponse( |
| 1025 | + id=function_call_id, |
| 1026 | + name="test_tool", |
| 1027 | + response={"result": 2}, |
| 1028 | + ) |
| 1029 | + ) |
| 1030 | + events = [ |
| 1031 | + Event( |
| 1032 | + invocation_id="inv1", |
| 1033 | + author="user", |
| 1034 | + content=types.UserContent("Call the tool"), |
| 1035 | + ), |
| 1036 | + Event( |
| 1037 | + invocation_id="inv2", |
| 1038 | + author="test_agent", |
| 1039 | + content=types.Content(role="model", parts=[fc_part]), |
| 1040 | + ), |
| 1041 | + Event( |
| 1042 | + invocation_id="inv3", |
| 1043 | + author="test_agent", |
| 1044 | + content=types.Content(role="user", parts=[fr_part]), |
| 1045 | + ), |
| 1046 | + ] |
| 1047 | + invocation_context.session.events = events |
| 1048 | + |
| 1049 | + async for _ in contents.request_processor.run_async( |
| 1050 | + invocation_context, llm_request |
| 1051 | + ): |
| 1052 | + pass |
| 1053 | + |
| 1054 | + model_fc_part = llm_request.contents[1].parts[0] |
| 1055 | + assert model_fc_part.function_call.id is None |
| 1056 | + user_fr_part = llm_request.contents[2].parts[0] |
| 1057 | + assert user_fr_part.function_response.id is None |
| 1058 | + |
| 1059 | + assert fc_part.function_call.id == function_call_id |
| 1060 | + assert fr_part.function_response.id == function_call_id |
| 1061 | + assert events[1].content.parts[0].function_call.id == function_call_id |
| 1062 | + assert events[2].content.parts[0].function_response.id == function_call_id |
| 1063 | + assert model_fc_part is not fc_part |
| 1064 | + assert user_fr_part is not fr_part |
| 1065 | + |
| 1066 | + |
| 1067 | +@pytest.mark.asyncio |
| 1068 | +async def test_downstream_part_mutation_does_not_corrupt_session_events(): |
| 1069 | + """Request parts must survive in-place mutation by later processors.""" |
| 1070 | + agent = Agent(model="gemini-2.5-flash", name="test_agent") |
| 1071 | + llm_request = LlmRequest(model="gemini-2.5-flash") |
| 1072 | + invocation_context = await testing_utils.create_invocation_context( |
| 1073 | + agent=agent |
| 1074 | + ) |
| 1075 | + |
| 1076 | + # A thought=True function-call part survives context filtering. |
| 1077 | + fc_part = types.Part( |
| 1078 | + function_call=types.FunctionCall(id="fc1", name="t", args={"x": 1}), |
| 1079 | + ) |
| 1080 | + fc_part.thought = True |
| 1081 | + events = [ |
| 1082 | + Event( |
| 1083 | + invocation_id="inv1", |
| 1084 | + author="user", |
| 1085 | + content=types.UserContent("Call the tool"), |
| 1086 | + ), |
| 1087 | + Event( |
| 1088 | + invocation_id="inv2", |
| 1089 | + author="test_agent", |
| 1090 | + content=types.Content(role="model", parts=[fc_part]), |
| 1091 | + ), |
| 1092 | + Event( |
| 1093 | + invocation_id="inv3", |
| 1094 | + author="test_agent", |
| 1095 | + content=types.Content( |
| 1096 | + role="user", |
| 1097 | + parts=[ |
| 1098 | + types.Part( |
| 1099 | + function_response=types.FunctionResponse( |
| 1100 | + id="fc1", name="t", response={"r": 2} |
| 1101 | + ) |
| 1102 | + ) |
| 1103 | + ], |
| 1104 | + ), |
| 1105 | + ), |
| 1106 | + ] |
| 1107 | + invocation_context.session.events = events |
| 1108 | + |
| 1109 | + async for _ in contents.request_processor.run_async( |
| 1110 | + invocation_context, llm_request |
| 1111 | + ): |
| 1112 | + pass |
| 1113 | + |
| 1114 | + # nl_planning clears thoughts in place on the request parts. |
| 1115 | + _nl_planning._remove_thought_from_request(llm_request) |
| 1116 | + |
| 1117 | + assert fc_part.thought is True |
| 1118 | + assert events[1].content.parts[0].thought is True |
| 1119 | + |
| 1120 | + |
1004 | 1121 | @pytest.mark.asyncio |
1005 | 1122 | async def test_adk_function_call_ids_preserved_for_interactions_model(): |
1006 | 1123 | """Test ADK generated ids are preserved for interactions requests.""" |
|
0 commit comments