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