lite-agent 0.5.0__py3-none-any.whl → 0.8.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of lite-agent might be problematic. Click here for more details.
- lite_agent/agent.py +188 -49
- lite_agent/chat_display.py +22 -14
- lite_agent/client.py +65 -3
- lite_agent/constants.py +30 -0
- lite_agent/message_transfers.py +3 -3
- lite_agent/processors/completion_event_processor.py +14 -20
- lite_agent/processors/response_event_processor.py +21 -15
- lite_agent/response_handlers/__init__.py +11 -0
- lite_agent/response_handlers/base.py +54 -0
- lite_agent/response_handlers/completion.py +78 -0
- lite_agent/response_handlers/responses.py +76 -0
- lite_agent/runner.py +312 -247
- lite_agent/types/__init__.py +2 -0
- lite_agent/types/messages.py +6 -5
- lite_agent/utils/__init__.py +0 -0
- lite_agent/utils/message_builder.py +211 -0
- lite_agent/utils/metrics.py +50 -0
- {lite_agent-0.5.0.dist-info → lite_agent-0.8.0.dist-info}/METADATA +2 -1
- lite_agent-0.8.0.dist-info/RECORD +31 -0
- lite_agent-0.5.0.dist-info/RECORD +0 -23
- {lite_agent-0.5.0.dist-info → lite_agent-0.8.0.dist-info}/WHEEL +0 -0
lite_agent/runner.py
CHANGED
|
@@ -1,59 +1,45 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import warnings
|
|
2
3
|
from collections.abc import AsyncGenerator, Sequence
|
|
3
4
|
from datetime import datetime, timedelta, timezone
|
|
4
5
|
from os import PathLike
|
|
5
6
|
from pathlib import Path
|
|
6
|
-
from typing import Any, Literal
|
|
7
|
+
from typing import Any, Literal, cast
|
|
7
8
|
|
|
8
9
|
from lite_agent.agent import Agent
|
|
10
|
+
from lite_agent.constants import CompletionMode, StreamIncludes, ToolName
|
|
9
11
|
from lite_agent.loggers import logger
|
|
10
12
|
from lite_agent.types import (
|
|
11
13
|
AgentChunk,
|
|
12
14
|
AgentChunkType,
|
|
13
|
-
AssistantMessageContent,
|
|
14
15
|
AssistantMessageMeta,
|
|
15
16
|
AssistantTextContent,
|
|
16
17
|
AssistantToolCall,
|
|
17
18
|
AssistantToolCallResult,
|
|
19
|
+
FlexibleInputMessage,
|
|
18
20
|
FlexibleRunnerMessage,
|
|
19
|
-
MessageDict,
|
|
20
21
|
MessageUsage,
|
|
21
22
|
NewAssistantMessage,
|
|
22
23
|
NewMessage,
|
|
23
24
|
NewSystemMessage,
|
|
24
|
-
# New structured message types
|
|
25
25
|
NewUserMessage,
|
|
26
26
|
ToolCall,
|
|
27
27
|
ToolCallFunction,
|
|
28
|
-
UserImageContent,
|
|
29
28
|
UserInput,
|
|
30
|
-
UserMessageContent,
|
|
31
29
|
UserTextContent,
|
|
32
30
|
)
|
|
33
|
-
from lite_agent.types.events import AssistantMessageEvent
|
|
34
|
-
|
|
35
|
-
DEFAULT_INCLUDES: tuple[AgentChunkType, ...] = (
|
|
36
|
-
"completion_raw",
|
|
37
|
-
"usage",
|
|
38
|
-
"function_call",
|
|
39
|
-
"function_call_output",
|
|
40
|
-
"content_delta",
|
|
41
|
-
"function_call_delta",
|
|
42
|
-
"assistant_message",
|
|
43
|
-
)
|
|
31
|
+
from lite_agent.types.events import AssistantMessageEvent, FunctionCallOutputEvent
|
|
32
|
+
from lite_agent.utils.message_builder import MessageBuilder
|
|
44
33
|
|
|
45
34
|
|
|
46
35
|
class Runner:
|
|
47
|
-
def __init__(self, agent: Agent, api: Literal["completion", "responses"] = "responses") -> None:
|
|
36
|
+
def __init__(self, agent: Agent, api: Literal["completion", "responses"] = "responses", *, streaming: bool = True) -> None:
|
|
48
37
|
self.agent = agent
|
|
49
|
-
self.messages: list[
|
|
38
|
+
self.messages: list[FlexibleRunnerMessage] = []
|
|
50
39
|
self.api = api
|
|
40
|
+
self.streaming = streaming
|
|
51
41
|
self._current_assistant_message: NewAssistantMessage | None = None
|
|
52
|
-
|
|
53
|
-
@property
|
|
54
|
-
def legacy_messages(self) -> list[NewMessage]:
|
|
55
|
-
"""Return messages in new format (legacy_messages is now an alias)."""
|
|
56
|
-
return self.messages
|
|
42
|
+
self.usage = MessageUsage(input_tokens=0, output_tokens=0, total_tokens=0)
|
|
57
43
|
|
|
58
44
|
def _start_assistant_message(self, content: str = "", meta: AssistantMessageMeta | None = None) -> None:
|
|
59
45
|
"""Start a new assistant message."""
|
|
@@ -66,7 +52,10 @@ class Runner:
|
|
|
66
52
|
"""Ensure current assistant message exists and return it."""
|
|
67
53
|
if self._current_assistant_message is None:
|
|
68
54
|
self._start_assistant_message()
|
|
69
|
-
|
|
55
|
+
if self._current_assistant_message is None:
|
|
56
|
+
msg = "Failed to create current assistant message"
|
|
57
|
+
raise RuntimeError(msg)
|
|
58
|
+
return self._current_assistant_message
|
|
70
59
|
|
|
71
60
|
def _add_to_current_assistant_message(self, content_item: AssistantTextContent | AssistantToolCall | AssistantToolCallResult) -> None:
|
|
72
61
|
"""Add content to the current assistant message."""
|
|
@@ -99,15 +88,20 @@ class Runner:
|
|
|
99
88
|
|
|
100
89
|
if self.messages and isinstance(self.messages[-1], NewAssistantMessage):
|
|
101
90
|
# Add to existing assistant message
|
|
102
|
-
self.messages[-1]
|
|
91
|
+
last_message = cast("NewAssistantMessage", self.messages[-1])
|
|
92
|
+
last_message.content.append(result)
|
|
103
93
|
else:
|
|
104
94
|
# Create new assistant message with just the tool result
|
|
105
95
|
assistant_message = NewAssistantMessage(content=[result])
|
|
106
96
|
self.messages.append(assistant_message)
|
|
107
97
|
|
|
98
|
+
# For completion API compatibility, create a separate assistant message
|
|
99
|
+
# Note: In the new architecture, we store everything as NewMessage format
|
|
100
|
+
# The conversion to completion format happens when sending to LLM
|
|
101
|
+
|
|
108
102
|
def _normalize_includes(self, includes: Sequence[AgentChunkType] | None) -> Sequence[AgentChunkType]:
|
|
109
103
|
"""Normalize includes parameter to default if None."""
|
|
110
|
-
return includes if includes is not None else DEFAULT_INCLUDES
|
|
104
|
+
return includes if includes is not None else StreamIncludes.DEFAULT_INCLUDES
|
|
111
105
|
|
|
112
106
|
def _normalize_record_path(self, record_to: PathLike | str | None) -> Path | None:
|
|
113
107
|
"""Normalize record_to parameter to Path object if provided."""
|
|
@@ -119,34 +113,68 @@ class Runner:
|
|
|
119
113
|
return
|
|
120
114
|
|
|
121
115
|
# Check for transfer_to_agent calls first
|
|
122
|
-
transfer_calls = [tc for tc in tool_calls if tc.function.name ==
|
|
116
|
+
transfer_calls = [tc for tc in tool_calls if tc.function.name == ToolName.TRANSFER_TO_AGENT]
|
|
123
117
|
if transfer_calls:
|
|
124
118
|
# Handle all transfer calls but only execute the first one
|
|
125
119
|
for i, tool_call in enumerate(transfer_calls):
|
|
126
120
|
if i == 0:
|
|
127
121
|
# Execute the first transfer
|
|
128
|
-
await self._handle_agent_transfer(tool_call)
|
|
122
|
+
call_id, output = await self._handle_agent_transfer(tool_call)
|
|
123
|
+
# Generate function_call_output event if in includes
|
|
124
|
+
if "function_call_output" in includes:
|
|
125
|
+
yield FunctionCallOutputEvent(
|
|
126
|
+
tool_call_id=call_id,
|
|
127
|
+
name=tool_call.function.name,
|
|
128
|
+
content=output,
|
|
129
|
+
execution_time_ms=0, # Transfer operations are typically fast
|
|
130
|
+
)
|
|
129
131
|
else:
|
|
130
132
|
# Add response for additional transfer calls without executing them
|
|
133
|
+
output = "Transfer already executed by previous call"
|
|
131
134
|
self._add_tool_call_result(
|
|
132
135
|
call_id=tool_call.id,
|
|
133
|
-
output=
|
|
136
|
+
output=output,
|
|
134
137
|
)
|
|
138
|
+
# Generate function_call_output event if in includes
|
|
139
|
+
if "function_call_output" in includes:
|
|
140
|
+
yield FunctionCallOutputEvent(
|
|
141
|
+
tool_call_id=tool_call.id,
|
|
142
|
+
name=tool_call.function.name,
|
|
143
|
+
content=output,
|
|
144
|
+
execution_time_ms=0,
|
|
145
|
+
)
|
|
135
146
|
return # Stop processing other tool calls after transfer
|
|
136
147
|
|
|
137
|
-
return_parent_calls = [tc for tc in tool_calls if tc.function.name ==
|
|
148
|
+
return_parent_calls = [tc for tc in tool_calls if tc.function.name == ToolName.TRANSFER_TO_PARENT]
|
|
138
149
|
if return_parent_calls:
|
|
139
150
|
# Handle multiple transfer_to_parent calls (only execute the first one)
|
|
140
151
|
for i, tool_call in enumerate(return_parent_calls):
|
|
141
152
|
if i == 0:
|
|
142
153
|
# Execute the first transfer
|
|
143
|
-
await self._handle_parent_transfer(tool_call)
|
|
154
|
+
call_id, output = await self._handle_parent_transfer(tool_call)
|
|
155
|
+
# Generate function_call_output event if in includes
|
|
156
|
+
if "function_call_output" in includes:
|
|
157
|
+
yield FunctionCallOutputEvent(
|
|
158
|
+
tool_call_id=call_id,
|
|
159
|
+
name=tool_call.function.name,
|
|
160
|
+
content=output,
|
|
161
|
+
execution_time_ms=0, # Transfer operations are typically fast
|
|
162
|
+
)
|
|
144
163
|
else:
|
|
145
164
|
# Add response for additional transfer calls without executing them
|
|
165
|
+
output = "Transfer already executed by previous call"
|
|
146
166
|
self._add_tool_call_result(
|
|
147
167
|
call_id=tool_call.id,
|
|
148
|
-
output=
|
|
168
|
+
output=output,
|
|
149
169
|
)
|
|
170
|
+
# Generate function_call_output event if in includes
|
|
171
|
+
if "function_call_output" in includes:
|
|
172
|
+
yield FunctionCallOutputEvent(
|
|
173
|
+
tool_call_id=tool_call.id,
|
|
174
|
+
name=tool_call.function.name,
|
|
175
|
+
content=output,
|
|
176
|
+
execution_time_ms=0,
|
|
177
|
+
)
|
|
150
178
|
return # Stop processing other tool calls after transfer
|
|
151
179
|
|
|
152
180
|
async for tool_call_chunk in self.agent.handle_tool_calls(tool_calls, context=context):
|
|
@@ -162,23 +190,46 @@ class Runner:
|
|
|
162
190
|
output=tool_call_chunk.content,
|
|
163
191
|
execution_time_ms=tool_call_chunk.execution_time_ms,
|
|
164
192
|
)
|
|
165
|
-
self.messages[-1]
|
|
193
|
+
last_message = cast("NewAssistantMessage", self.messages[-1])
|
|
194
|
+
last_message.content.append(tool_result)
|
|
195
|
+
|
|
196
|
+
# Note: For completion API compatibility, the conversion happens when sending to LLM
|
|
166
197
|
|
|
167
198
|
async def _collect_all_chunks(self, stream: AsyncGenerator[AgentChunk, None]) -> list[AgentChunk]:
|
|
168
199
|
"""Collect all chunks from an async generator into a list."""
|
|
169
200
|
return [chunk async for chunk in stream]
|
|
170
201
|
|
|
171
|
-
def run(
|
|
202
|
+
def run(
|
|
172
203
|
self,
|
|
173
|
-
user_input: UserInput,
|
|
204
|
+
user_input: UserInput | None = None,
|
|
174
205
|
max_steps: int = 20,
|
|
175
206
|
includes: Sequence[AgentChunkType] | None = None,
|
|
176
207
|
context: "Any | None" = None, # noqa: ANN401
|
|
177
208
|
record_to: PathLike | str | None = None,
|
|
178
209
|
agent_kwargs: dict[str, Any] | None = None,
|
|
179
210
|
) -> AsyncGenerator[AgentChunk, None]:
|
|
180
|
-
"""Run the agent and return a RunResponse object that can be asynchronously iterated for each chunk.
|
|
211
|
+
"""Run the agent and return a RunResponse object that can be asynchronously iterated for each chunk.
|
|
212
|
+
|
|
213
|
+
If user_input is None, the method will continue execution from the current state,
|
|
214
|
+
equivalent to calling the continue methods.
|
|
215
|
+
"""
|
|
216
|
+
logger.debug(f"Runner.run called with streaming={self.streaming}, api={self.api}")
|
|
181
217
|
includes = self._normalize_includes(includes)
|
|
218
|
+
|
|
219
|
+
# If no user input provided, use continue logic
|
|
220
|
+
if user_input is None:
|
|
221
|
+
logger.debug("No user input provided, using continue logic")
|
|
222
|
+
return self._run_continue_stream(max_steps, includes, self._normalize_record_path(record_to), context)
|
|
223
|
+
|
|
224
|
+
# Cancel any pending tool calls before processing new user input
|
|
225
|
+
# and yield cancellation events if they should be included
|
|
226
|
+
cancellation_events = self._cancel_pending_tool_calls()
|
|
227
|
+
|
|
228
|
+
# We need to handle this differently since run() is not async
|
|
229
|
+
# Store cancellation events to be yielded by _run
|
|
230
|
+
self._pending_cancellation_events = cancellation_events
|
|
231
|
+
|
|
232
|
+
# Process user input
|
|
182
233
|
match user_input:
|
|
183
234
|
case str():
|
|
184
235
|
self.messages.append(NewUserMessage(content=[UserTextContent(text=user_input)]))
|
|
@@ -189,6 +240,7 @@ class Runner:
|
|
|
189
240
|
case _:
|
|
190
241
|
# Handle single message (BaseModel, TypedDict, or dict)
|
|
191
242
|
self.append_message(user_input) # type: ignore[arg-type]
|
|
243
|
+
logger.debug("Messages prepared, calling _run")
|
|
192
244
|
return self._run(max_steps, includes, self._normalize_record_path(record_to), context=context, agent_kwargs=agent_kwargs)
|
|
193
245
|
|
|
194
246
|
async def _run(
|
|
@@ -201,21 +253,31 @@ class Runner:
|
|
|
201
253
|
) -> AsyncGenerator[AgentChunk, None]:
|
|
202
254
|
"""Run the agent and return a RunResponse object that can be asynchronously iterated for each chunk."""
|
|
203
255
|
logger.debug(f"Running agent with messages: {self.messages}")
|
|
256
|
+
|
|
257
|
+
# First, yield any pending cancellation events
|
|
258
|
+
if hasattr(self, "_pending_cancellation_events"):
|
|
259
|
+
for cancellation_event in self._pending_cancellation_events:
|
|
260
|
+
if "function_call_output" in includes:
|
|
261
|
+
yield cancellation_event
|
|
262
|
+
# Clear the pending events after yielding
|
|
263
|
+
delattr(self, "_pending_cancellation_events")
|
|
264
|
+
|
|
204
265
|
steps = 0
|
|
205
266
|
finish_reason = None
|
|
206
267
|
|
|
207
268
|
# Determine completion condition based on agent configuration
|
|
208
|
-
completion_condition = getattr(self.agent, "completion_condition",
|
|
269
|
+
completion_condition = getattr(self.agent, "completion_condition", CompletionMode.STOP)
|
|
209
270
|
|
|
210
271
|
def is_finish() -> bool:
|
|
211
|
-
if completion_condition ==
|
|
272
|
+
if completion_condition == CompletionMode.CALL:
|
|
212
273
|
# Check if wait_for_user was called in the last assistant message
|
|
213
274
|
if self.messages and isinstance(self.messages[-1], NewAssistantMessage):
|
|
214
|
-
|
|
215
|
-
|
|
275
|
+
last_message = self.messages[-1]
|
|
276
|
+
for content_item in last_message.content:
|
|
277
|
+
if isinstance(content_item, AssistantToolCallResult) and self._get_tool_call_name_by_id(content_item.call_id) == ToolName.WAIT_FOR_USER:
|
|
216
278
|
return True
|
|
217
279
|
return False
|
|
218
|
-
return finish_reason ==
|
|
280
|
+
return finish_reason == CompletionMode.STOP
|
|
219
281
|
|
|
220
282
|
while not is_finish() and steps < max_steps:
|
|
221
283
|
logger.debug(f"Step {steps}: finish_reason={finish_reason}, is_finish()={is_finish()}")
|
|
@@ -226,43 +288,52 @@ class Runner:
|
|
|
226
288
|
if agent_kwargs:
|
|
227
289
|
reasoning = agent_kwargs.get("reasoning")
|
|
228
290
|
|
|
291
|
+
logger.debug(f"Using API: {self.api}, streaming: {self.streaming}")
|
|
229
292
|
match self.api:
|
|
230
293
|
case "completion":
|
|
294
|
+
logger.debug("Calling agent.completion")
|
|
231
295
|
resp = await self.agent.completion(
|
|
232
296
|
self.messages,
|
|
233
297
|
record_to_file=record_to,
|
|
234
298
|
reasoning=reasoning,
|
|
299
|
+
streaming=self.streaming,
|
|
235
300
|
)
|
|
236
301
|
case "responses":
|
|
302
|
+
logger.debug("Calling agent.responses")
|
|
237
303
|
resp = await self.agent.responses(
|
|
238
304
|
self.messages,
|
|
239
305
|
record_to_file=record_to,
|
|
240
306
|
reasoning=reasoning,
|
|
307
|
+
streaming=self.streaming,
|
|
241
308
|
)
|
|
242
309
|
case _:
|
|
243
310
|
msg = f"Unknown API type: {self.api}"
|
|
244
311
|
raise ValueError(msg)
|
|
312
|
+
logger.debug(f"Received response from agent: {type(resp)}")
|
|
245
313
|
async for chunk in resp:
|
|
246
314
|
match chunk.type:
|
|
247
315
|
case "assistant_message":
|
|
248
316
|
# Start or update assistant message in new format
|
|
249
|
-
meta = AssistantMessageMeta(
|
|
250
|
-
sent_at=chunk.message.meta.sent_at,
|
|
251
|
-
latency_ms=getattr(chunk.message.meta, "latency_ms", None),
|
|
252
|
-
total_time_ms=getattr(chunk.message.meta, "output_time_ms", None),
|
|
253
|
-
)
|
|
254
317
|
# If we already have a current assistant message, just update its metadata
|
|
255
318
|
if self._current_assistant_message is not None:
|
|
256
|
-
|
|
319
|
+
# Preserve all existing metadata and only update specific fields
|
|
320
|
+
original_meta = self._current_assistant_message.meta
|
|
321
|
+
original_meta.sent_at = chunk.message.meta.sent_at
|
|
322
|
+
if hasattr(chunk.message.meta, "latency_ms"):
|
|
323
|
+
original_meta.latency_ms = chunk.message.meta.latency_ms
|
|
324
|
+
if hasattr(chunk.message.meta, "output_time_ms"):
|
|
325
|
+
original_meta.total_time_ms = chunk.message.meta.output_time_ms
|
|
326
|
+
# Preserve other metadata fields like model, usage, etc.
|
|
327
|
+
for attr in ["model", "usage", "input_tokens", "output_tokens"]:
|
|
328
|
+
if hasattr(chunk.message.meta, attr):
|
|
329
|
+
setattr(original_meta, attr, getattr(chunk.message.meta, attr))
|
|
257
330
|
else:
|
|
258
|
-
#
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
break
|
|
265
|
-
self._start_assistant_message(text_content, meta)
|
|
331
|
+
# For non-streaming mode, directly use the complete message from the response handler
|
|
332
|
+
self._current_assistant_message = chunk.message
|
|
333
|
+
|
|
334
|
+
# If model is None, try to get it from agent client
|
|
335
|
+
if self._current_assistant_message is not None and self._current_assistant_message.meta.model is None and hasattr(self.agent.client, "model"):
|
|
336
|
+
self._current_assistant_message.meta.model = self.agent.client.model
|
|
266
337
|
# Only yield assistant_message chunk if it's in includes and has content
|
|
267
338
|
if chunk.type in includes and self._current_assistant_message is not None:
|
|
268
339
|
# Create a new chunk with the current assistant message content
|
|
@@ -289,28 +360,45 @@ class Runner:
|
|
|
289
360
|
if chunk.type in includes:
|
|
290
361
|
yield chunk
|
|
291
362
|
case "usage":
|
|
292
|
-
# Update the last assistant message with usage data and output_time_ms
|
|
363
|
+
# Update the current or last assistant message with usage data and output_time_ms
|
|
293
364
|
usage_time = datetime.now(timezone.utc)
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
365
|
+
|
|
366
|
+
# Always accumulate usage in runner first
|
|
367
|
+
self.usage.input_tokens = (self.usage.input_tokens or 0) + (chunk.usage.input_tokens or 0)
|
|
368
|
+
self.usage.output_tokens = (self.usage.output_tokens or 0) + (chunk.usage.output_tokens or 0)
|
|
369
|
+
self.usage.total_tokens = (self.usage.total_tokens or 0) + (chunk.usage.input_tokens or 0) + (chunk.usage.output_tokens or 0)
|
|
370
|
+
|
|
371
|
+
# Try to find the assistant message to update
|
|
372
|
+
target_message = None
|
|
373
|
+
|
|
374
|
+
# First check if we have a current assistant message
|
|
375
|
+
if self._current_assistant_message is not None:
|
|
376
|
+
target_message = self._current_assistant_message
|
|
377
|
+
else:
|
|
378
|
+
# Otherwise, look for the last assistant message in the list
|
|
379
|
+
for i in range(len(self.messages) - 1, -1, -1):
|
|
380
|
+
current_message = self.messages[i]
|
|
381
|
+
if isinstance(current_message, NewAssistantMessage):
|
|
382
|
+
target_message = current_message
|
|
383
|
+
break
|
|
384
|
+
|
|
385
|
+
# Update the target message with usage information
|
|
386
|
+
if target_message is not None:
|
|
387
|
+
if target_message.meta.usage is None:
|
|
388
|
+
target_message.meta.usage = MessageUsage()
|
|
389
|
+
target_message.meta.usage.input_tokens = chunk.usage.input_tokens
|
|
390
|
+
target_message.meta.usage.output_tokens = chunk.usage.output_tokens
|
|
391
|
+
target_message.meta.usage.total_tokens = (chunk.usage.input_tokens or 0) + (chunk.usage.output_tokens or 0)
|
|
392
|
+
|
|
393
|
+
# Calculate output_time_ms if latency_ms is available
|
|
394
|
+
if target_message.meta.latency_ms is not None:
|
|
395
|
+
# We need to calculate from first output to usage time
|
|
396
|
+
# We'll calculate: usage_time - (sent_at - latency_ms)
|
|
397
|
+
# This gives us the time from first output to usage completion
|
|
398
|
+
# sent_at is when the message was completed, so sent_at - latency_ms approximates first output time
|
|
399
|
+
first_output_time_approx = target_message.meta.sent_at - timedelta(milliseconds=target_message.meta.latency_ms)
|
|
400
|
+
output_time_ms = int((usage_time - first_output_time_approx).total_seconds() * 1000)
|
|
401
|
+
target_message.meta.total_time_ms = max(0, output_time_ms)
|
|
314
402
|
# Always yield usage chunk if it's in includes
|
|
315
403
|
if chunk.type in includes:
|
|
316
404
|
yield chunk
|
|
@@ -333,7 +421,7 @@ class Runner:
|
|
|
333
421
|
yield tool_chunk
|
|
334
422
|
finish_reason = "tool_calls"
|
|
335
423
|
else:
|
|
336
|
-
finish_reason =
|
|
424
|
+
finish_reason = CompletionMode.STOP
|
|
337
425
|
steps += 1
|
|
338
426
|
|
|
339
427
|
async def has_require_confirm_tools(self):
|
|
@@ -350,6 +438,12 @@ class Runner:
|
|
|
350
438
|
includes: list[AgentChunkType] | None = None,
|
|
351
439
|
record_to: PathLike | str | None = None,
|
|
352
440
|
) -> list[AgentChunk]:
|
|
441
|
+
"""Deprecated: Use run_until_complete(None) instead."""
|
|
442
|
+
warnings.warn(
|
|
443
|
+
"run_continue_until_complete is deprecated. Use run_until_complete(None) instead.",
|
|
444
|
+
DeprecationWarning,
|
|
445
|
+
stacklevel=2,
|
|
446
|
+
)
|
|
353
447
|
resp = self.run_continue_stream(max_steps, includes, record_to=record_to)
|
|
354
448
|
return await self._collect_all_chunks(resp)
|
|
355
449
|
|
|
@@ -360,6 +454,12 @@ class Runner:
|
|
|
360
454
|
record_to: PathLike | str | None = None,
|
|
361
455
|
context: "Any | None" = None, # noqa: ANN401
|
|
362
456
|
) -> AsyncGenerator[AgentChunk, None]:
|
|
457
|
+
"""Deprecated: Use run(None) instead."""
|
|
458
|
+
warnings.warn(
|
|
459
|
+
"run_continue_stream is deprecated. Use run(None) instead.",
|
|
460
|
+
DeprecationWarning,
|
|
461
|
+
stacklevel=2,
|
|
462
|
+
)
|
|
363
463
|
return self._run_continue_stream(max_steps, includes, record_to=record_to, context=context)
|
|
364
464
|
|
|
365
465
|
async def _run_continue_stream(
|
|
@@ -394,7 +494,7 @@ class Runner:
|
|
|
394
494
|
|
|
395
495
|
async def run_until_complete(
|
|
396
496
|
self,
|
|
397
|
-
user_input: UserInput,
|
|
497
|
+
user_input: UserInput | None = None,
|
|
398
498
|
max_steps: int = 20,
|
|
399
499
|
includes: list[AgentChunkType] | None = None,
|
|
400
500
|
record_to: PathLike | str | None = None,
|
|
@@ -412,11 +512,12 @@ class Runner:
|
|
|
412
512
|
tool_results = set()
|
|
413
513
|
tool_call_names = {}
|
|
414
514
|
|
|
415
|
-
|
|
416
|
-
|
|
515
|
+
last_message = self.messages[-1]
|
|
516
|
+
for content_item in last_message.content:
|
|
517
|
+
if isinstance(content_item, AssistantToolCall):
|
|
417
518
|
tool_calls[content_item.call_id] = content_item
|
|
418
519
|
tool_call_names[content_item.call_id] = content_item.name
|
|
419
|
-
elif content_item
|
|
520
|
+
elif isinstance(content_item, AssistantToolCallResult):
|
|
420
521
|
tool_results.add(content_item.call_id)
|
|
421
522
|
|
|
422
523
|
# Return pending tool calls and tool call names map
|
|
@@ -433,6 +534,38 @@ class Runner:
|
|
|
433
534
|
_, tool_call_names = self._analyze_last_assistant_message()
|
|
434
535
|
return tool_call_names.get(call_id)
|
|
435
536
|
|
|
537
|
+
def _cancel_pending_tool_calls(self) -> list[FunctionCallOutputEvent]:
|
|
538
|
+
"""Cancel all pending tool calls by adding cancellation results.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
List of FunctionCallOutputEvent for each cancelled tool call
|
|
542
|
+
"""
|
|
543
|
+
pending_tool_calls = self._find_pending_tool_calls()
|
|
544
|
+
if not pending_tool_calls:
|
|
545
|
+
return []
|
|
546
|
+
|
|
547
|
+
logger.debug(f"Cancelling {len(pending_tool_calls)} pending tool calls due to new user input")
|
|
548
|
+
|
|
549
|
+
cancellation_events = []
|
|
550
|
+
for tool_call in pending_tool_calls:
|
|
551
|
+
output = "Operation cancelled by user - new input provided"
|
|
552
|
+
self._add_tool_call_result(
|
|
553
|
+
call_id=tool_call.call_id,
|
|
554
|
+
output=output,
|
|
555
|
+
execution_time_ms=0,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
# Create cancellation event
|
|
559
|
+
cancellation_event = FunctionCallOutputEvent(
|
|
560
|
+
tool_call_id=tool_call.call_id,
|
|
561
|
+
name=tool_call.name,
|
|
562
|
+
content=output,
|
|
563
|
+
execution_time_ms=0,
|
|
564
|
+
)
|
|
565
|
+
cancellation_events.append(cancellation_event)
|
|
566
|
+
|
|
567
|
+
return cancellation_events
|
|
568
|
+
|
|
436
569
|
def _convert_tool_calls_to_tool_calls(self, tool_calls: list[AssistantToolCall]) -> list[ToolCall]:
|
|
437
570
|
"""Convert AssistantToolCall objects to ToolCall objects for compatibility."""
|
|
438
571
|
return [
|
|
@@ -448,7 +581,7 @@ class Runner:
|
|
|
448
581
|
for i, tc in enumerate(tool_calls)
|
|
449
582
|
]
|
|
450
583
|
|
|
451
|
-
def set_chat_history(self, messages: Sequence[
|
|
584
|
+
def set_chat_history(self, messages: Sequence[FlexibleInputMessage], root_agent: Agent | None = None) -> None:
|
|
452
585
|
"""Set the entire chat history and track the current agent based on function calls.
|
|
453
586
|
|
|
454
587
|
This method analyzes the message history to determine which agent should be active
|
|
@@ -465,17 +598,54 @@ class Runner:
|
|
|
465
598
|
current_agent = root_agent if root_agent is not None else self.agent
|
|
466
599
|
|
|
467
600
|
# Add each message and track agent transfers
|
|
468
|
-
for
|
|
469
|
-
|
|
470
|
-
|
|
601
|
+
for input_message in messages:
|
|
602
|
+
# Store length before adding to get the added message
|
|
603
|
+
prev_length = len(self.messages)
|
|
604
|
+
self.append_message(input_message)
|
|
605
|
+
|
|
606
|
+
# Track transfers using the converted message (now in self.messages)
|
|
607
|
+
if len(self.messages) > prev_length:
|
|
608
|
+
converted_message = self.messages[-1] # Get the last added message
|
|
609
|
+
current_agent = self._track_agent_transfer_in_message(converted_message, current_agent)
|
|
471
610
|
|
|
472
611
|
# Set the current agent based on the tracked transfers
|
|
473
612
|
self.agent = current_agent
|
|
474
613
|
logger.info(f"Chat history set with {len(self.messages)} messages. Current agent: {self.agent.name}")
|
|
475
614
|
|
|
476
|
-
def
|
|
615
|
+
def get_messages(self) -> list[NewMessage]:
|
|
616
|
+
"""Get the messages as NewMessage objects.
|
|
617
|
+
|
|
618
|
+
Only returns NewMessage objects, filtering out any dict or other legacy formats.
|
|
619
|
+
"""
|
|
620
|
+
return [msg for msg in self.messages if isinstance(msg, NewMessage)]
|
|
621
|
+
|
|
622
|
+
def get_dict_messages(self) -> list[dict[str, Any]]:
|
|
477
623
|
"""Get the messages in JSONL format."""
|
|
478
|
-
|
|
624
|
+
result = []
|
|
625
|
+
for msg in self.messages:
|
|
626
|
+
if hasattr(msg, "model_dump"):
|
|
627
|
+
result.append(msg.model_dump(mode="json"))
|
|
628
|
+
elif isinstance(msg, dict):
|
|
629
|
+
result.append(msg)
|
|
630
|
+
else:
|
|
631
|
+
# Fallback for any other message types
|
|
632
|
+
result.append(dict(msg))
|
|
633
|
+
return result
|
|
634
|
+
|
|
635
|
+
def add_user_message(self, text: str) -> None:
|
|
636
|
+
"""Convenience method to add a user text message."""
|
|
637
|
+
message = NewUserMessage(content=[UserTextContent(text=text)])
|
|
638
|
+
self.append_message(message)
|
|
639
|
+
|
|
640
|
+
def add_assistant_message(self, text: str) -> None:
|
|
641
|
+
"""Convenience method to add an assistant text message."""
|
|
642
|
+
message = NewAssistantMessage(content=[AssistantTextContent(text=text)])
|
|
643
|
+
self.append_message(message)
|
|
644
|
+
|
|
645
|
+
def add_system_message(self, content: str) -> None:
|
|
646
|
+
"""Convenience method to add a system message."""
|
|
647
|
+
message = NewSystemMessage(content=content)
|
|
648
|
+
self.append_message(message)
|
|
479
649
|
|
|
480
650
|
def _track_agent_transfer_in_message(self, message: FlexibleRunnerMessage, current_agent: Agent) -> Agent:
|
|
481
651
|
"""Track agent transfers in a single message.
|
|
@@ -487,8 +657,6 @@ class Runner:
|
|
|
487
657
|
Returns:
|
|
488
658
|
The agent that should be active after processing this message
|
|
489
659
|
"""
|
|
490
|
-
if isinstance(message, dict):
|
|
491
|
-
return self._track_transfer_from_dict_message(message, current_agent)
|
|
492
660
|
if isinstance(message, NewAssistantMessage):
|
|
493
661
|
return self._track_transfer_from_new_assistant_message(message, current_agent)
|
|
494
662
|
|
|
@@ -498,28 +666,13 @@ class Runner:
|
|
|
498
666
|
"""Track transfers from NewAssistantMessage objects."""
|
|
499
667
|
for content_item in message.content:
|
|
500
668
|
if content_item.type == "tool_call":
|
|
501
|
-
if content_item.name ==
|
|
669
|
+
if content_item.name == ToolName.TRANSFER_TO_AGENT:
|
|
502
670
|
arguments = content_item.arguments if isinstance(content_item.arguments, str) else str(content_item.arguments)
|
|
503
671
|
return self._handle_transfer_to_agent_tracking(arguments, current_agent)
|
|
504
|
-
if content_item.name ==
|
|
672
|
+
if content_item.name == ToolName.TRANSFER_TO_PARENT:
|
|
505
673
|
return self._handle_transfer_to_parent_tracking(current_agent)
|
|
506
674
|
return current_agent
|
|
507
675
|
|
|
508
|
-
def _track_transfer_from_dict_message(self, message: dict[str, Any] | MessageDict, current_agent: Agent) -> Agent:
|
|
509
|
-
"""Track transfers from dictionary-format messages."""
|
|
510
|
-
message_type = message.get("type")
|
|
511
|
-
if message_type != "function_call":
|
|
512
|
-
return current_agent
|
|
513
|
-
|
|
514
|
-
function_name = message.get("name", "")
|
|
515
|
-
if function_name == "transfer_to_agent":
|
|
516
|
-
return self._handle_transfer_to_agent_tracking(message.get("arguments", ""), current_agent)
|
|
517
|
-
|
|
518
|
-
if function_name == "transfer_to_parent":
|
|
519
|
-
return self._handle_transfer_to_parent_tracking(current_agent)
|
|
520
|
-
|
|
521
|
-
return current_agent
|
|
522
|
-
|
|
523
676
|
def _handle_transfer_to_agent_tracking(self, arguments: str | dict, current_agent: Agent) -> Agent:
|
|
524
677
|
"""Handle transfer_to_agent function call tracking."""
|
|
525
678
|
try:
|
|
@@ -575,145 +728,39 @@ class Runner:
|
|
|
575
728
|
|
|
576
729
|
return None
|
|
577
730
|
|
|
578
|
-
def append_message(self, message:
|
|
731
|
+
def append_message(self, message: FlexibleInputMessage) -> None:
|
|
732
|
+
"""Append a message to the conversation history.
|
|
733
|
+
|
|
734
|
+
Accepts both NewMessage format and dict format (which will be converted internally).
|
|
735
|
+
"""
|
|
579
736
|
if isinstance(message, NewMessage):
|
|
580
|
-
# Already in new format
|
|
581
737
|
self.messages.append(message)
|
|
582
738
|
elif isinstance(message, dict):
|
|
583
|
-
#
|
|
584
|
-
|
|
585
|
-
role = message.get("role")
|
|
586
|
-
|
|
739
|
+
# Convert dict to NewMessage using MessageBuilder
|
|
740
|
+
role = message.get("role", "").lower()
|
|
587
741
|
if role == "user":
|
|
588
|
-
|
|
589
|
-
if isinstance(content, str):
|
|
590
|
-
user_message = NewUserMessage(content=[UserTextContent(text=content)])
|
|
591
|
-
elif isinstance(content, list):
|
|
592
|
-
# Handle complex content array
|
|
593
|
-
user_content_items: list[UserMessageContent] = []
|
|
594
|
-
for item in content:
|
|
595
|
-
if isinstance(item, dict):
|
|
596
|
-
item_type = item.get("type")
|
|
597
|
-
if item_type in {"input_text", "text"}:
|
|
598
|
-
user_content_items.append(UserTextContent(text=item.get("text", "")))
|
|
599
|
-
elif item_type in {"input_image", "image_url"}:
|
|
600
|
-
if item_type == "image_url":
|
|
601
|
-
# Handle completion API format
|
|
602
|
-
image_url = item.get("image_url", {})
|
|
603
|
-
url = image_url.get("url", "") if isinstance(image_url, dict) else str(image_url)
|
|
604
|
-
user_content_items.append(UserImageContent(image_url=url))
|
|
605
|
-
else:
|
|
606
|
-
# Handle response API format
|
|
607
|
-
user_content_items.append(
|
|
608
|
-
UserImageContent(
|
|
609
|
-
image_url=item.get("image_url"),
|
|
610
|
-
file_id=item.get("file_id"),
|
|
611
|
-
detail=item.get("detail", "auto"),
|
|
612
|
-
),
|
|
613
|
-
)
|
|
614
|
-
elif hasattr(item, "type"):
|
|
615
|
-
# Handle Pydantic models
|
|
616
|
-
if item.type == "input_text":
|
|
617
|
-
user_content_items.append(UserTextContent(text=item.text))
|
|
618
|
-
elif item.type == "input_image":
|
|
619
|
-
user_content_items.append(
|
|
620
|
-
UserImageContent(
|
|
621
|
-
image_url=getattr(item, "image_url", None),
|
|
622
|
-
file_id=getattr(item, "file_id", None),
|
|
623
|
-
detail=getattr(item, "detail", "auto"),
|
|
624
|
-
),
|
|
625
|
-
)
|
|
626
|
-
else:
|
|
627
|
-
# Fallback: convert to text
|
|
628
|
-
user_content_items.append(UserTextContent(text=str(item)))
|
|
629
|
-
|
|
630
|
-
user_message = NewUserMessage(content=user_content_items)
|
|
631
|
-
else:
|
|
632
|
-
# Handle non-string, non-list content
|
|
633
|
-
user_message = NewUserMessage(content=[UserTextContent(text=str(content))])
|
|
634
|
-
self.messages.append(user_message)
|
|
635
|
-
elif role == "system":
|
|
636
|
-
content = message.get("content", "")
|
|
637
|
-
system_message = NewSystemMessage(content=str(content))
|
|
638
|
-
self.messages.append(system_message)
|
|
742
|
+
converted_message = MessageBuilder.build_user_message_from_dict(message)
|
|
639
743
|
elif role == "assistant":
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
# Handle tool calls if present
|
|
644
|
-
if "tool_calls" in message:
|
|
645
|
-
for tool_call in message.get("tool_calls", []):
|
|
646
|
-
try:
|
|
647
|
-
arguments = json.loads(tool_call["function"]["arguments"]) if isinstance(tool_call["function"]["arguments"], str) else tool_call["function"]["arguments"]
|
|
648
|
-
except (json.JSONDecodeError, TypeError):
|
|
649
|
-
arguments = tool_call["function"]["arguments"]
|
|
650
|
-
|
|
651
|
-
assistant_content_items.append(
|
|
652
|
-
AssistantToolCall(
|
|
653
|
-
call_id=tool_call["id"],
|
|
654
|
-
name=tool_call["function"]["name"],
|
|
655
|
-
arguments=arguments,
|
|
656
|
-
),
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
assistant_message = NewAssistantMessage(content=assistant_content_items)
|
|
660
|
-
self.messages.append(assistant_message)
|
|
661
|
-
elif message_type == "function_call":
|
|
662
|
-
# Handle function_call directly like AgentFunctionToolCallMessage
|
|
663
|
-
# Type guard: ensure we have the right message type
|
|
664
|
-
if "call_id" in message and "name" in message and "arguments" in message:
|
|
665
|
-
function_call_msg = message # Type should be FunctionCallDict now
|
|
666
|
-
if self.messages and isinstance(self.messages[-1], NewAssistantMessage):
|
|
667
|
-
tool_call = AssistantToolCall(
|
|
668
|
-
call_id=function_call_msg["call_id"], # type: ignore
|
|
669
|
-
name=function_call_msg["name"], # type: ignore
|
|
670
|
-
arguments=function_call_msg["arguments"], # type: ignore
|
|
671
|
-
)
|
|
672
|
-
self.messages[-1].content.append(tool_call)
|
|
673
|
-
else:
|
|
674
|
-
assistant_message = NewAssistantMessage(
|
|
675
|
-
content=[
|
|
676
|
-
AssistantToolCall(
|
|
677
|
-
call_id=function_call_msg["call_id"], # type: ignore
|
|
678
|
-
name=function_call_msg["name"], # type: ignore
|
|
679
|
-
arguments=function_call_msg["arguments"], # type: ignore
|
|
680
|
-
),
|
|
681
|
-
],
|
|
682
|
-
)
|
|
683
|
-
self.messages.append(assistant_message)
|
|
684
|
-
elif message_type == "function_call_output":
|
|
685
|
-
# Handle function_call_output directly like AgentFunctionCallOutput
|
|
686
|
-
# Type guard: ensure we have the right message type
|
|
687
|
-
if "call_id" in message and "output" in message:
|
|
688
|
-
function_output_msg = message # Type should be FunctionCallOutputDict now
|
|
689
|
-
if self.messages and isinstance(self.messages[-1], NewAssistantMessage):
|
|
690
|
-
tool_result = AssistantToolCallResult(
|
|
691
|
-
call_id=function_output_msg["call_id"], # type: ignore
|
|
692
|
-
output=function_output_msg["output"], # type: ignore
|
|
693
|
-
)
|
|
694
|
-
self.messages[-1].content.append(tool_result)
|
|
695
|
-
else:
|
|
696
|
-
assistant_message = NewAssistantMessage(
|
|
697
|
-
content=[
|
|
698
|
-
AssistantToolCallResult(
|
|
699
|
-
call_id=function_output_msg["call_id"], # type: ignore
|
|
700
|
-
output=function_output_msg["output"], # type: ignore
|
|
701
|
-
),
|
|
702
|
-
],
|
|
703
|
-
)
|
|
704
|
-
self.messages.append(assistant_message)
|
|
744
|
+
converted_message = MessageBuilder.build_assistant_message_from_dict(message)
|
|
745
|
+
elif role == "system":
|
|
746
|
+
converted_message = MessageBuilder.build_system_message_from_dict(message)
|
|
705
747
|
else:
|
|
706
|
-
msg = "
|
|
748
|
+
msg = f"Unsupported message role: {role}. Must be 'user', 'assistant', or 'system'."
|
|
707
749
|
raise ValueError(msg)
|
|
750
|
+
|
|
751
|
+
self.messages.append(converted_message)
|
|
708
752
|
else:
|
|
709
|
-
msg = f"Unsupported message type: {type(message)}"
|
|
753
|
+
msg = f"Unsupported message type: {type(message)}. Supports NewMessage types and dict."
|
|
710
754
|
raise TypeError(msg)
|
|
711
755
|
|
|
712
|
-
async def _handle_agent_transfer(self, tool_call: ToolCall) ->
|
|
756
|
+
async def _handle_agent_transfer(self, tool_call: ToolCall) -> tuple[str, str]:
|
|
713
757
|
"""Handle agent transfer when transfer_to_agent tool is called.
|
|
714
758
|
|
|
715
759
|
Args:
|
|
716
760
|
tool_call: The transfer_to_agent tool call
|
|
761
|
+
|
|
762
|
+
Returns:
|
|
763
|
+
Tuple of (call_id, output) for the tool call result
|
|
717
764
|
"""
|
|
718
765
|
|
|
719
766
|
# Parse the arguments to get the target agent name
|
|
@@ -722,31 +769,34 @@ class Runner:
|
|
|
722
769
|
target_agent_name = arguments.get("name")
|
|
723
770
|
except (json.JSONDecodeError, KeyError):
|
|
724
771
|
logger.error("Failed to parse transfer_to_agent arguments: %s", tool_call.function.arguments)
|
|
772
|
+
output = "Failed to parse transfer arguments"
|
|
725
773
|
# Add error result to messages
|
|
726
774
|
self._add_tool_call_result(
|
|
727
775
|
call_id=tool_call.id,
|
|
728
|
-
output=
|
|
776
|
+
output=output,
|
|
729
777
|
)
|
|
730
|
-
return
|
|
778
|
+
return tool_call.id, output
|
|
731
779
|
|
|
732
780
|
if not target_agent_name:
|
|
733
781
|
logger.error("No target agent name provided in transfer_to_agent call")
|
|
782
|
+
output = "No target agent name provided"
|
|
734
783
|
# Add error result to messages
|
|
735
784
|
self._add_tool_call_result(
|
|
736
785
|
call_id=tool_call.id,
|
|
737
|
-
output=
|
|
786
|
+
output=output,
|
|
738
787
|
)
|
|
739
|
-
return
|
|
788
|
+
return tool_call.id, output
|
|
740
789
|
|
|
741
790
|
# Find the target agent in handoffs
|
|
742
791
|
if not self.agent.handoffs:
|
|
743
792
|
logger.error("Current agent has no handoffs configured")
|
|
793
|
+
output = "Current agent has no handoffs configured"
|
|
744
794
|
# Add error result to messages
|
|
745
795
|
self._add_tool_call_result(
|
|
746
796
|
call_id=tool_call.id,
|
|
747
|
-
output=
|
|
797
|
+
output=output,
|
|
748
798
|
)
|
|
749
|
-
return
|
|
799
|
+
return tool_call.id, output
|
|
750
800
|
|
|
751
801
|
target_agent = None
|
|
752
802
|
for agent in self.agent.handoffs:
|
|
@@ -756,12 +806,13 @@ class Runner:
|
|
|
756
806
|
|
|
757
807
|
if not target_agent:
|
|
758
808
|
logger.error("Target agent '%s' not found in handoffs", target_agent_name)
|
|
809
|
+
output = f"Target agent '{target_agent_name}' not found in handoffs"
|
|
759
810
|
# Add error result to messages
|
|
760
811
|
self._add_tool_call_result(
|
|
761
812
|
call_id=tool_call.id,
|
|
762
|
-
output=
|
|
813
|
+
output=output,
|
|
763
814
|
)
|
|
764
|
-
return
|
|
815
|
+
return tool_call.id, output
|
|
765
816
|
|
|
766
817
|
# Execute the transfer tool call to get the result
|
|
767
818
|
try:
|
|
@@ -770,40 +821,49 @@ class Runner:
|
|
|
770
821
|
tool_call.function.arguments or "",
|
|
771
822
|
)
|
|
772
823
|
|
|
824
|
+
output = str(result)
|
|
773
825
|
# Add the tool call result to messages
|
|
774
826
|
self._add_tool_call_result(
|
|
775
827
|
call_id=tool_call.id,
|
|
776
|
-
output=
|
|
828
|
+
output=output,
|
|
777
829
|
)
|
|
778
830
|
|
|
779
831
|
# Switch to the target agent
|
|
780
832
|
logger.info("Transferring conversation from %s to %s", self.agent.name, target_agent_name)
|
|
781
833
|
self.agent = target_agent
|
|
782
834
|
|
|
835
|
+
return tool_call.id, output
|
|
836
|
+
|
|
783
837
|
except Exception as e:
|
|
784
838
|
logger.exception("Failed to execute transfer_to_agent tool call")
|
|
839
|
+
output = f"Transfer failed: {e!s}"
|
|
785
840
|
# Add error result to messages
|
|
786
841
|
self._add_tool_call_result(
|
|
787
842
|
call_id=tool_call.id,
|
|
788
|
-
output=
|
|
843
|
+
output=output,
|
|
789
844
|
)
|
|
845
|
+
return tool_call.id, output
|
|
790
846
|
|
|
791
|
-
async def _handle_parent_transfer(self, tool_call: ToolCall) ->
|
|
847
|
+
async def _handle_parent_transfer(self, tool_call: ToolCall) -> tuple[str, str]:
|
|
792
848
|
"""Handle parent transfer when transfer_to_parent tool is called.
|
|
793
849
|
|
|
794
850
|
Args:
|
|
795
851
|
tool_call: The transfer_to_parent tool call
|
|
852
|
+
|
|
853
|
+
Returns:
|
|
854
|
+
Tuple of (call_id, output) for the tool call result
|
|
796
855
|
"""
|
|
797
856
|
|
|
798
857
|
# Check if current agent has a parent
|
|
799
858
|
if not self.agent.parent:
|
|
800
859
|
logger.error("Current agent has no parent to transfer back to.")
|
|
860
|
+
output = "Current agent has no parent to transfer back to"
|
|
801
861
|
# Add error result to messages
|
|
802
862
|
self._add_tool_call_result(
|
|
803
863
|
call_id=tool_call.id,
|
|
804
|
-
output=
|
|
864
|
+
output=output,
|
|
805
865
|
)
|
|
806
|
-
return
|
|
866
|
+
return tool_call.id, output
|
|
807
867
|
|
|
808
868
|
# Execute the transfer tool call to get the result
|
|
809
869
|
try:
|
|
@@ -812,20 +872,25 @@ class Runner:
|
|
|
812
872
|
tool_call.function.arguments or "",
|
|
813
873
|
)
|
|
814
874
|
|
|
875
|
+
output = str(result)
|
|
815
876
|
# Add the tool call result to messages
|
|
816
877
|
self._add_tool_call_result(
|
|
817
878
|
call_id=tool_call.id,
|
|
818
|
-
output=
|
|
879
|
+
output=output,
|
|
819
880
|
)
|
|
820
881
|
|
|
821
882
|
# Switch to the parent agent
|
|
822
883
|
logger.info("Transferring conversation from %s back to parent %s", self.agent.name, self.agent.parent.name)
|
|
823
884
|
self.agent = self.agent.parent
|
|
824
885
|
|
|
886
|
+
return tool_call.id, output
|
|
887
|
+
|
|
825
888
|
except Exception as e:
|
|
826
889
|
logger.exception("Failed to execute transfer_to_parent tool call")
|
|
890
|
+
output = f"Transfer to parent failed: {e!s}"
|
|
827
891
|
# Add error result to messages
|
|
828
892
|
self._add_tool_call_result(
|
|
829
893
|
call_id=tool_call.id,
|
|
830
|
-
output=
|
|
894
|
+
output=output,
|
|
831
895
|
)
|
|
896
|
+
return tool_call.id, output
|