agentex-sdk 0.6.0__py3-none-any.whl → 0.6.2__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.
Files changed (43) hide show
  1. agentex/_client.py +15 -1
  2. agentex/_version.py +1 -1
  3. agentex/lib/adk/providers/_modules/litellm.py +1 -1
  4. agentex/lib/adk/providers/_modules/openai.py +16 -1
  5. agentex/lib/adk/providers/_modules/sgp.py +1 -1
  6. agentex/lib/adk/providers/_modules/sync_provider.py +32 -24
  7. agentex/lib/cli/commands/init.py +7 -18
  8. agentex/lib/cli/templates/default/README.md.j2 +1 -1
  9. agentex/lib/cli/templates/default/dev.ipynb.j2 +1 -1
  10. agentex/lib/cli/templates/default/manifest.yaml.j2 +1 -4
  11. agentex/lib/cli/templates/default/project/acp.py.j2 +3 -3
  12. agentex/lib/cli/templates/default/test_agent.py.j2 +1 -1
  13. agentex/lib/cli/templates/sync/manifest.yaml.j2 +0 -3
  14. agentex/lib/cli/templates/temporal/README.md.j2 +1 -1
  15. agentex/lib/cli/templates/temporal/dev.ipynb.j2 +1 -1
  16. agentex/lib/cli/templates/temporal/manifest.yaml.j2 +2 -5
  17. agentex/lib/cli/templates/temporal/project/acp.py.j2 +1 -1
  18. agentex/lib/cli/templates/temporal/test_agent.py.j2 +1 -1
  19. agentex/lib/core/temporal/plugins/openai_agents/models/temporal_streaming_model.py +66 -0
  20. agentex/lib/core/temporal/plugins/openai_agents/models/temporal_tracing_model.py +94 -17
  21. agentex/lib/environment_variables.py +1 -1
  22. agentex/lib/sdk/config/agent_config.py +1 -1
  23. agentex/lib/sdk/fastacp/base/base_acp_server.py +4 -4
  24. agentex/lib/sdk/fastacp/fastacp.py +30 -16
  25. agentex/lib/sdk/fastacp/impl/{agentic_base_acp.py → async_base_acp.py} +10 -8
  26. agentex/lib/sdk/fastacp/tests/README.md +3 -3
  27. agentex/lib/sdk/fastacp/tests/conftest.py +4 -4
  28. agentex/lib/sdk/fastacp/tests/test_fastacp_factory.py +99 -72
  29. agentex/lib/sdk/fastacp/tests/test_integration.py +24 -24
  30. agentex/lib/types/fastacp.py +8 -5
  31. agentex/lib/utils/dev_tools/async_messages.py +1 -1
  32. agentex/resources/__init__.py +14 -0
  33. agentex/resources/deployment_history.py +272 -0
  34. agentex/types/__init__.py +3 -0
  35. agentex/types/agent.py +4 -1
  36. agentex/types/deployment_history.py +33 -0
  37. agentex/types/deployment_history_list_params.py +18 -0
  38. agentex/types/deployment_history_list_response.py +10 -0
  39. {agentex_sdk-0.6.0.dist-info → agentex_sdk-0.6.2.dist-info}/METADATA +1 -1
  40. {agentex_sdk-0.6.0.dist-info → agentex_sdk-0.6.2.dist-info}/RECORD +43 -39
  41. {agentex_sdk-0.6.0.dist-info → agentex_sdk-0.6.2.dist-info}/WHEEL +0 -0
  42. {agentex_sdk-0.6.0.dist-info → agentex_sdk-0.6.2.dist-info}/entry_points.txt +0 -0
  43. {agentex_sdk-0.6.0.dist-info → agentex_sdk-0.6.2.dist-info}/licenses/LICENSE +0 -0
agentex/_client.py CHANGED
@@ -21,7 +21,7 @@ from ._types import (
21
21
  )
22
22
  from ._utils import is_given, get_async_library
23
23
  from ._version import __version__
24
- from .resources import spans, tasks, agents, events, states, tracker
24
+ from .resources import spans, tasks, agents, events, states, tracker, deployment_history
25
25
  from ._streaming import Stream as Stream, AsyncStream as AsyncStream
26
26
  from ._exceptions import APIStatusError
27
27
  from ._base_client import (
@@ -57,6 +57,7 @@ class Agentex(SyncAPIClient):
57
57
  states: states.StatesResource
58
58
  events: events.EventsResource
59
59
  tracker: tracker.TrackerResource
60
+ deployment_history: deployment_history.DeploymentHistoryResource
60
61
  with_raw_response: AgentexWithRawResponse
61
62
  with_streaming_response: AgentexWithStreamedResponse
62
63
 
@@ -141,6 +142,7 @@ class Agentex(SyncAPIClient):
141
142
  self.states = states.StatesResource(self)
142
143
  self.events = events.EventsResource(self)
143
144
  self.tracker = tracker.TrackerResource(self)
145
+ self.deployment_history = deployment_history.DeploymentHistoryResource(self)
144
146
  self.with_raw_response = AgentexWithRawResponse(self)
145
147
  self.with_streaming_response = AgentexWithStreamedResponse(self)
146
148
 
@@ -261,6 +263,7 @@ class AsyncAgentex(AsyncAPIClient):
261
263
  states: states.AsyncStatesResource
262
264
  events: events.AsyncEventsResource
263
265
  tracker: tracker.AsyncTrackerResource
266
+ deployment_history: deployment_history.AsyncDeploymentHistoryResource
264
267
  with_raw_response: AsyncAgentexWithRawResponse
265
268
  with_streaming_response: AsyncAgentexWithStreamedResponse
266
269
 
@@ -345,6 +348,7 @@ class AsyncAgentex(AsyncAPIClient):
345
348
  self.states = states.AsyncStatesResource(self)
346
349
  self.events = events.AsyncEventsResource(self)
347
350
  self.tracker = tracker.AsyncTrackerResource(self)
351
+ self.deployment_history = deployment_history.AsyncDeploymentHistoryResource(self)
348
352
  self.with_raw_response = AsyncAgentexWithRawResponse(self)
349
353
  self.with_streaming_response = AsyncAgentexWithStreamedResponse(self)
350
354
 
@@ -466,6 +470,7 @@ class AgentexWithRawResponse:
466
470
  self.states = states.StatesResourceWithRawResponse(client.states)
467
471
  self.events = events.EventsResourceWithRawResponse(client.events)
468
472
  self.tracker = tracker.TrackerResourceWithRawResponse(client.tracker)
473
+ self.deployment_history = deployment_history.DeploymentHistoryResourceWithRawResponse(client.deployment_history)
469
474
 
470
475
 
471
476
  class AsyncAgentexWithRawResponse:
@@ -477,6 +482,9 @@ class AsyncAgentexWithRawResponse:
477
482
  self.states = states.AsyncStatesResourceWithRawResponse(client.states)
478
483
  self.events = events.AsyncEventsResourceWithRawResponse(client.events)
479
484
  self.tracker = tracker.AsyncTrackerResourceWithRawResponse(client.tracker)
485
+ self.deployment_history = deployment_history.AsyncDeploymentHistoryResourceWithRawResponse(
486
+ client.deployment_history
487
+ )
480
488
 
481
489
 
482
490
  class AgentexWithStreamedResponse:
@@ -488,6 +496,9 @@ class AgentexWithStreamedResponse:
488
496
  self.states = states.StatesResourceWithStreamingResponse(client.states)
489
497
  self.events = events.EventsResourceWithStreamingResponse(client.events)
490
498
  self.tracker = tracker.TrackerResourceWithStreamingResponse(client.tracker)
499
+ self.deployment_history = deployment_history.DeploymentHistoryResourceWithStreamingResponse(
500
+ client.deployment_history
501
+ )
491
502
 
492
503
 
493
504
  class AsyncAgentexWithStreamedResponse:
@@ -499,6 +510,9 @@ class AsyncAgentexWithStreamedResponse:
499
510
  self.states = states.AsyncStatesResourceWithStreamingResponse(client.states)
500
511
  self.events = events.AsyncEventsResourceWithStreamingResponse(client.events)
501
512
  self.tracker = tracker.AsyncTrackerResourceWithStreamingResponse(client.tracker)
513
+ self.deployment_history = deployment_history.AsyncDeploymentHistoryResourceWithStreamingResponse(
514
+ client.deployment_history
515
+ )
502
516
 
503
517
 
504
518
  Client = Agentex
agentex/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "agentex"
4
- __version__ = "0.6.0" # x-release-please-version
4
+ __version__ = "0.6.2" # x-release-please-version
@@ -32,7 +32,7 @@ DEFAULT_RETRY_POLICY = RetryPolicy(maximum_attempts=1)
32
32
  class LiteLLMModule:
33
33
  """
34
34
  Module for managing LiteLLM agent operations in Agentex.
35
- Provides high-level methods for chat completion, streaming, agentic streaming.
35
+ Provides high-level methods for chat completion, streaming.
36
36
  """
37
37
 
38
38
  def __init__(
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import sys
3
4
  from typing import Any, Literal
4
5
  from datetime import timedelta
5
6
 
@@ -12,6 +13,12 @@ from temporalio.common import RetryPolicy
12
13
  from agents.agent_output import AgentOutputSchemaBase
13
14
  from agents.model_settings import ModelSettings
14
15
 
16
+ # Use warnings.deprecated in Python 3.13+, typing_extensions.deprecated for older versions
17
+ if sys.version_info >= (3, 13):
18
+ from warnings import deprecated
19
+ else:
20
+ from typing_extensions import deprecated
21
+
15
22
  from agentex.lib.utils.logging import make_logger
16
23
  from agentex.lib.utils.temporal import in_temporal_workflow
17
24
  from agentex.lib.core.tracing.tracer import AsyncTracer
@@ -383,6 +390,10 @@ class OpenAIModule:
383
390
  previous_response_id=previous_response_id,
384
391
  )
385
392
 
393
+ @deprecated(
394
+ "Use the OpenAI Agents SDK integration with Temporal instead. "
395
+ "See examples in tutorials/10_agentic/10_temporal/ for migration guidance."
396
+ )
386
397
  async def run_agent_streamed_auto_send(
387
398
  self,
388
399
  task_id: str,
@@ -413,6 +424,10 @@ class OpenAIModule:
413
424
  """
414
425
  Run an agent with streaming enabled and automatic TaskMessage creation.
415
426
 
427
+ .. deprecated::
428
+ Use the OpenAI Agents SDK integration with Temporal instead.
429
+ See examples in tutorials/10_agentic/10_temporal/ for migration guidance.
430
+
416
431
  Args:
417
432
  task_id: The ID of the task to run the agent for.
418
433
  input_list: List of input data for the agent.
@@ -494,4 +509,4 @@ class OpenAIModule:
494
509
  output_guardrails=output_guardrails,
495
510
  max_turns=max_turns,
496
511
  previous_response_id=previous_response_id,
497
- )
512
+ )
@@ -25,7 +25,7 @@ DEFAULT_RETRY_POLICY = RetryPolicy(maximum_attempts=1)
25
25
  class SGPModule:
26
26
  """
27
27
  Module for managing SGP agent operations in Agentex.
28
- Provides high-level methods for chat completion, streaming, agentic streaming, and message classification.
28
+ Provides high-level methods for chat completion, streaming, and message classification.
29
29
  """
30
30
 
31
31
  def __init__(
@@ -164,18 +164,22 @@ class SyncStreamingModel(Model):
164
164
  output_items = response_output if isinstance(response_output, list) else [response_output]
165
165
 
166
166
  for item in output_items:
167
- item_dict = _serialize_item(item)
168
- if item_dict:
169
- new_items.append(item_dict)
170
-
171
- # Extract final_output from message type if available
172
- if item_dict.get('type') == 'message' and not final_output:
173
- content = item_dict.get('content', [])
174
- if content and isinstance(content, list):
175
- for content_part in content:
176
- if isinstance(content_part, dict) and 'text' in content_part:
177
- final_output = content_part['text']
178
- break
167
+ try:
168
+ item_dict = _serialize_item(item)
169
+ if item_dict:
170
+ new_items.append(item_dict)
171
+
172
+ # Extract final_output from message type if available
173
+ if item_dict.get('type') == 'message' and not final_output:
174
+ content = item_dict.get('content', [])
175
+ if content and isinstance(content, list):
176
+ for content_part in content:
177
+ if isinstance(content_part, dict) and 'text' in content_part:
178
+ final_output = content_part['text']
179
+ break
180
+ except Exception as e:
181
+ logger.warning(f"Failed to serialize item in get_response: {e}")
182
+ continue
179
183
 
180
184
  span.output = {
181
185
  "new_items": new_items,
@@ -275,18 +279,22 @@ class SyncStreamingModel(Model):
275
279
  if event_type == 'response.output_item.done':
276
280
  item = getattr(event, 'item', None)
277
281
  if item is not None:
278
- item_dict = _serialize_item(item)
279
- if item_dict:
280
- new_items.append(item_dict)
281
-
282
- # Update final_response_text from message type if available
283
- if item_dict.get('type') == 'message':
284
- content = item_dict.get('content', [])
285
- if content and isinstance(content, list):
286
- for content_part in content:
287
- if isinstance(content_part, dict) and 'text' in content_part:
288
- final_response_text = content_part['text']
289
- break
282
+ try:
283
+ item_dict = _serialize_item(item)
284
+ if item_dict:
285
+ new_items.append(item_dict)
286
+
287
+ # Update final_response_text from message type if available
288
+ if item_dict.get('type') == 'message':
289
+ content = item_dict.get('content', [])
290
+ if content and isinstance(content, list):
291
+ for content_part in content:
292
+ if isinstance(content_part, dict) and 'text' in content_part:
293
+ final_response_text = content_part['text']
294
+ break
295
+ except Exception as e:
296
+ logger.warning(f"Failed to serialize item in stream_response: {e}")
297
+ continue
290
298
 
291
299
  yield event
292
300
 
@@ -125,16 +125,16 @@ def init():
125
125
  table.add_column("Template", style="cyan", no_wrap=True)
126
126
  table.add_column("Description", style="white")
127
127
  table.add_row(
128
- "[bold cyan]Agentic - ACP Only[/bold cyan]",
129
- "A simple synchronous agent that handles tasks directly. Best for straightforward agents that don't need long-running operations.",
128
+ "[bold cyan]Async - ACP Only[/bold cyan]",
129
+ "Asynchronous, non-blocking agent that can process multiple concurrent requests. Best for straightforward asynchronous agents that don't need durable execution. Good for asynchronous workflows, stateful applications, and multi-step analysis.",
130
130
  )
131
131
  table.add_row(
132
- "[bold cyan]Agentic - Temporal[/bold cyan]",
133
- "An asynchronous agent powered by Temporal workflows. Best for agents that need to handle long-running tasks, retries, or complex state management.",
132
+ "[bold cyan]Async - Temporal[/bold cyan]",
133
+ "Asynchronous, non-blocking agent with durable execution for all steps. Best for production-grade agents that require complex multi-step tool calls, human-in-the-loop approvals, and long-running processes that require transactional reliability.",
134
134
  )
135
135
  table.add_row(
136
136
  "[bold cyan]Sync ACP[/bold cyan]",
137
- "A synchronous agent that handles tasks directly. The difference is that this Sync ACP will be required to respond with the results in the same call as the input.Best for straightforward agents that don't need long-running operations.",
137
+ "Synchronous agent that processes one request per task with a simple request-response pattern. Best for low-latency use cases, FAQ bots, translation services, and data lookups.",
138
138
  )
139
139
  console.print()
140
140
  console.print(table)
@@ -151,8 +151,8 @@ def init():
151
151
  template_type = questionary.select(
152
152
  "What type of template would you like to create?",
153
153
  choices=[
154
- {"name": "Agentic - ACP Only", "value": TemplateType.DEFAULT},
155
- {"name": "Agentic - Temporal", "value": TemplateType.TEMPORAL},
154
+ {"name": "Async - ACP Only", "value": TemplateType.DEFAULT},
155
+ {"name": "Async - Temporal", "value": TemplateType.TEMPORAL},
156
156
  {"name": "Sync ACP", "value": TemplateType.SYNC},
157
157
  ],
158
158
  ).ask()
@@ -184,16 +184,6 @@ def init():
184
184
  ).ask()
185
185
  if not description:
186
186
  return
187
-
188
- agent_input_type = questionary.select(
189
- "What type of input will your agent handle?",
190
- choices=[
191
- {"name": "Text Input", "value": "text"},
192
- {"name": "Structured Input", "value": "json"},
193
- ],
194
- ).ask()
195
- if not agent_input_type:
196
- return
197
187
 
198
188
  use_uv = questionary.select(
199
189
  "Would you like to use uv for package management?",
@@ -206,7 +196,6 @@ def init():
206
196
  answers = {
207
197
  "template_type": template_type,
208
198
  "project_path": project_path,
209
- "agent_input_type": agent_input_type,
210
199
  "agent_name": agent_name,
211
200
  "agent_directory_name": agent_directory_name,
212
201
  "description": description,
@@ -95,7 +95,7 @@ The notebook includes:
95
95
  - **Async message subscription**: Subscribe to server-side events to receive agent responses
96
96
  - **Rich message display**: Beautiful formatting with timestamps and author information
97
97
 
98
- The notebook automatically uses your agent name (`{{ agent_name }}`) and demonstrates the agentic ACP workflow: create task → send event → subscribe to responses.
98
+ The notebook automatically uses your agent name (`{{ agent_name }}`) and demonstrates the async ACP workflow: create task → send event → subscribe to responses.
99
99
 
100
100
  ### 3. Manage Dependencies
101
101
 
@@ -29,7 +29,7 @@
29
29
  "metadata": {},
30
30
  "outputs": [],
31
31
  "source": [
32
- "# (REQUIRED) Create a new task. For Agentic agents, you must create a task for messages to be associated with.\n",
32
+ "# (REQUIRED) Create a new task. For Async agents, you must create a task for messages to be associated with.\n",
33
33
  "import uuid\n",
34
34
  "\n",
35
35
  "rpc_response = client.agents.create_task(\n",
@@ -57,7 +57,7 @@ local_development:
57
57
  # Agent Configuration
58
58
  # -----------------
59
59
  agent:
60
- acp_type: agentic
60
+ acp_type: async
61
61
 
62
62
  # Unique name for your agent
63
63
  # Used for task routing and monitoring
@@ -67,9 +67,6 @@ agent:
67
67
  # Helps with documentation and discovery
68
68
  description: {{ description }}
69
69
 
70
- # Type of input the agent will handle: text or json
71
- agent_input_type: {{ agent_input_type }}
72
-
73
70
  # Temporal workflow configuration
74
71
  # Set enabled: true to use Temporal workflows for long-running tasks
75
72
  temporal:
@@ -1,12 +1,12 @@
1
1
  from agentex.lib.sdk.fastacp.fastacp import FastACP
2
- from agentex.lib.types.fastacp import AgenticACPConfig
2
+ from agentex.lib.types.fastacp import AsyncACPConfig
3
3
  from agentex.lib.types.acp import SendEventParams, CancelTaskParams, CreateTaskParams
4
4
 
5
5
 
6
6
  # Create an ACP server
7
7
  acp = FastACP.create(
8
- acp_type="agentic",
9
- config=AgenticACPConfig(type="base")
8
+ acp_type="async",
9
+ config=AsyncACPConfig(type="base")
10
10
  )
11
11
 
12
12
 
@@ -24,7 +24,7 @@ from agentex import AsyncAgentex
24
24
  from agentex.types import TaskMessage
25
25
  from agentex.types.agent_rpc_params import ParamsCreateTaskRequest
26
26
  from agentex.types.text_content_param import TextContentParam
27
- from test_utils.agentic import (
27
+ from test_utils.async_utils import (
28
28
  poll_for_agent_response,
29
29
  send_event_and_poll_yielding,
30
30
  stream_agent_response,
@@ -66,9 +66,6 @@ agent:
66
66
  # Helps with documentation and discovery
67
67
  description: {{ description }}
68
68
 
69
- # Type of input the agent will handle: text or json
70
- agent_input_type: {{ agent_input_type }}
71
-
72
69
  # Temporal workflow configuration
73
70
  # Set enabled: true to use Temporal workflows for long-running tasks
74
71
  temporal:
@@ -101,7 +101,7 @@ The notebook includes:
101
101
  - **Async message subscription**: Subscribe to server-side events to receive agent responses
102
102
  - **Rich message display**: Beautiful formatting with timestamps and author information
103
103
 
104
- The notebook automatically uses your agent name (`{{ agent_name }}`) and demonstrates the agentic ACP workflow: create task → send event → subscribe to responses.
104
+ The notebook automatically uses your agent name (`{{ agent_name }}`) and demonstrates the async ACP workflow: create task → send event → subscribe to responses.
105
105
 
106
106
  ### 3. Develop Temporal Workflows
107
107
  - Edit `workflow.py` to define your agent's async workflow logic
@@ -29,7 +29,7 @@
29
29
  "metadata": {},
30
30
  "outputs": [],
31
31
  "source": [
32
- "# (REQUIRED) Create a new task. For Agentic agents, you must create a task for messages to be associated with.\n",
32
+ "# (REQUIRED) Create a new task. For Async agents, you must create a task for messages to be associated with.\n",
33
33
  "import uuid\n",
34
34
  "\n",
35
35
  "rpc_response = client.agents.create_task(\n",
@@ -64,8 +64,8 @@ local_development:
64
64
  # Agent Configuration
65
65
  # -----------------
66
66
  agent:
67
- # Type of agent - either sync or agentic
68
- acp_type: agentic
67
+ # Type of agent - either sync or async
68
+ acp_type: async
69
69
 
70
70
  # Unique name for your agent
71
71
  # Used for task routing and monitoring
@@ -75,9 +75,6 @@ agent:
75
75
  # Helps with documentation and discovery
76
76
  description: {{ description }}
77
77
 
78
- # Type of input the agent will handle: text or json
79
- agent_input_type: {{ agent_input_type }}
80
-
81
78
  # Temporal workflow configuration
82
79
  # This enables your agent to run as a Temporal workflow for long-running tasks
83
80
  temporal:
@@ -39,7 +39,7 @@ from agentex.lib.types.fastacp import TemporalACPConfig
39
39
 
40
40
  # Create the ACP server
41
41
  acp = FastACP.create(
42
- acp_type="agentic",
42
+ acp_type="async",
43
43
  config=TemporalACPConfig(
44
44
  # When deployed to the cluster, the Temporal address will automatically be set to the cluster address
45
45
  # For local development, we set the address manually to talk to the local Temporal service set up via docker compose
@@ -24,7 +24,7 @@ from agentex import AsyncAgentex
24
24
  from agentex.types import TaskMessage
25
25
  from agentex.types.agent_rpc_params import ParamsCreateTaskRequest
26
26
  from agentex.types.text_content_param import TextContentParam
27
- from test_utils.agentic import (
27
+ from test_utils.async_utils import (
28
28
  poll_for_agent_response,
29
29
  send_event_and_poll_yielding,
30
30
  stream_agent_response,
@@ -62,6 +62,43 @@ from agentex.lib.core.temporal.plugins.openai_agents.interceptors.context_interc
62
62
  # Create logger for this module
63
63
  logger = logging.getLogger("agentex.temporal.streaming")
64
64
 
65
+
66
+ def _serialize_item(item: Any) -> dict[str, Any]:
67
+ """
68
+ Universal serializer for any item type from OpenAI Agents SDK.
69
+
70
+ Uses model_dump() for Pydantic models, otherwise extracts attributes manually.
71
+ Filters out internal Pydantic fields that can't be serialized.
72
+ """
73
+ if hasattr(item, 'model_dump'):
74
+ # Pydantic model - use model_dump for proper serialization
75
+ try:
76
+ return item.model_dump(mode='json', exclude_unset=True)
77
+ except Exception:
78
+ # Fallback to dict conversion
79
+ return dict(item) if hasattr(item, '__iter__') else {}
80
+ else:
81
+ # Not a Pydantic model - extract attributes manually
82
+ item_dict = {}
83
+ for attr_name in dir(item):
84
+ if not attr_name.startswith('_') and attr_name not in ('model_fields', 'model_config', 'model_computed_fields'):
85
+ try:
86
+ attr_value = getattr(item, attr_name, None)
87
+ # Skip methods and None values
88
+ if attr_value is not None and not callable(attr_value):
89
+ # Convert to JSON-serializable format
90
+ if hasattr(attr_value, 'model_dump'):
91
+ item_dict[attr_name] = attr_value.model_dump()
92
+ elif isinstance(attr_value, (str, int, float, bool, list, dict)):
93
+ item_dict[attr_name] = attr_value
94
+ else:
95
+ item_dict[attr_name] = str(attr_value)
96
+ except Exception:
97
+ # Skip attributes that can't be accessed
98
+ pass
99
+ return item_dict
100
+
101
+
65
102
  class TemporalStreamingModel(Model):
66
103
  """Custom model implementation with streaming support."""
67
104
 
@@ -739,6 +776,35 @@ class TemporalStreamingModel(Model):
739
776
  output_tokens_details=OutputTokensDetails(reasoning_tokens=len(''.join(reasoning_contents)) // 4), # Approximate
740
777
  )
741
778
 
779
+ # Serialize response output items for span tracing
780
+ new_items = []
781
+ final_output = None
782
+
783
+ for item in response_output:
784
+ try:
785
+ item_dict = _serialize_item(item)
786
+ if item_dict:
787
+ new_items.append(item_dict)
788
+
789
+ # Extract final_output from message type if available
790
+ if item_dict.get('type') == 'message' and not final_output:
791
+ content = item_dict.get('content', [])
792
+ if content and isinstance(content, list):
793
+ for content_part in content:
794
+ if isinstance(content_part, dict) and 'text' in content_part:
795
+ final_output = content_part['text']
796
+ break
797
+ except Exception as e:
798
+ logger.warning(f"Failed to serialize item in temporal_streaming_model: {e}")
799
+ continue
800
+
801
+ # Set span output with structured data
802
+ if span:
803
+ span.output = {
804
+ "new_items": new_items,
805
+ "final_output": final_output,
806
+ }
807
+
742
808
  # Return the response
743
809
  return ModelResponse(
744
810
  output=response_output,
@@ -7,9 +7,10 @@ context interceptor to access task_id, trace_id, and parent_span_id.
7
7
  The key innovation is that these are thin wrappers around the standard OpenAI models,
8
8
  avoiding code duplication while adding tracing capabilities.
9
9
  """
10
+ from __future__ import annotations
10
11
 
11
12
  import logging
12
- from typing import List, Union, Optional, override
13
+ from typing import Any, List, Union, Optional, override
13
14
 
14
15
  from agents import (
15
16
  Tool,
@@ -41,6 +42,42 @@ from agentex.lib.core.temporal.plugins.openai_agents.interceptors.context_interc
41
42
  logger = logging.getLogger("agentex.temporal.tracing")
42
43
 
43
44
 
45
+ def _serialize_item(item: Any) -> dict[str, Any]:
46
+ """
47
+ Universal serializer for any item type from OpenAI Agents SDK.
48
+
49
+ Uses model_dump() for Pydantic models, otherwise extracts attributes manually.
50
+ Filters out internal Pydantic fields that can't be serialized.
51
+ """
52
+ if hasattr(item, 'model_dump'):
53
+ # Pydantic model - use model_dump for proper serialization
54
+ try:
55
+ return item.model_dump(mode='json', exclude_unset=True)
56
+ except Exception:
57
+ # Fallback to dict conversion
58
+ return dict(item) if hasattr(item, '__iter__') else {}
59
+ else:
60
+ # Not a Pydantic model - extract attributes manually
61
+ item_dict = {}
62
+ for attr_name in dir(item):
63
+ if not attr_name.startswith('_') and attr_name not in ('model_fields', 'model_config', 'model_computed_fields'):
64
+ try:
65
+ attr_value = getattr(item, attr_name, None)
66
+ # Skip methods and None values
67
+ if attr_value is not None and not callable(attr_value):
68
+ # Convert to JSON-serializable format
69
+ if hasattr(attr_value, 'model_dump'):
70
+ item_dict[attr_name] = attr_value.model_dump()
71
+ elif isinstance(attr_value, (str, int, float, bool, list, dict)):
72
+ item_dict[attr_name] = attr_value
73
+ else:
74
+ item_dict[attr_name] = str(attr_value)
75
+ except Exception:
76
+ # Skip attributes that can't be accessed
77
+ pass
78
+ return item_dict
79
+
80
+
44
81
  class TemporalTracingModelProvider(OpenAIProvider):
45
82
  """Model provider that returns OpenAI models wrapped with AgentEx tracing.
46
83
 
@@ -171,15 +208,35 @@ class TemporalTracingResponsesModel(Model):
171
208
  **kwargs,
172
209
  )
173
210
 
174
- # Add response info to span output
211
+ # Serialize response output items for span tracing
212
+ new_items = []
213
+ final_output = None
214
+
215
+ if hasattr(response, 'output') and response.output:
216
+ response_output = response.output if isinstance(response.output, list) else [response.output]
217
+
218
+ for item in response_output:
219
+ try:
220
+ item_dict = _serialize_item(item)
221
+ if item_dict:
222
+ new_items.append(item_dict)
223
+
224
+ # Extract final_output from message type if available
225
+ if item_dict.get('type') == 'message' and not final_output:
226
+ content = item_dict.get('content', [])
227
+ if content and isinstance(content, list):
228
+ for content_part in content:
229
+ if isinstance(content_part, dict) and 'text' in content_part:
230
+ final_output = content_part['text']
231
+ break
232
+ except Exception as e:
233
+ logger.warning(f"Failed to serialize item in temporal tracing model: {e}")
234
+ continue
235
+
236
+ # Set span output with structured data
175
237
  span.output = { # type: ignore[attr-defined]
176
- "response_id": getattr(response, "id", None),
177
- "model_used": getattr(response, "model", None),
178
- "usage": {
179
- "input_tokens": response.usage.input_tokens if response.usage else None,
180
- "output_tokens": response.usage.output_tokens if response.usage else None,
181
- "total_tokens": response.usage.total_tokens if response.usage else None,
182
- } if response.usage else None,
238
+ "new_items": new_items,
239
+ "final_output": final_output,
183
240
  }
184
241
 
185
242
  return response
@@ -284,15 +341,35 @@ class TemporalTracingChatCompletionsModel(Model):
284
341
  **kwargs,
285
342
  )
286
343
 
287
- # Add response info to span output
344
+ # Serialize response output items for span tracing
345
+ new_items = []
346
+ final_output = None
347
+
348
+ if hasattr(response, 'output') and response.output:
349
+ response_output = response.output if isinstance(response.output, list) else [response.output]
350
+
351
+ for item in response_output:
352
+ try:
353
+ item_dict = _serialize_item(item)
354
+ if item_dict:
355
+ new_items.append(item_dict)
356
+
357
+ # Extract final_output from message type if available
358
+ if item_dict.get('type') == 'message' and not final_output:
359
+ content = item_dict.get('content', [])
360
+ if content and isinstance(content, list):
361
+ for content_part in content:
362
+ if isinstance(content_part, dict) and 'text' in content_part:
363
+ final_output = content_part['text']
364
+ break
365
+ except Exception as e:
366
+ logger.warning(f"Failed to serialize item in temporal tracing model: {e}")
367
+ continue
368
+
369
+ # Set span output with structured data
288
370
  span.output = { # type: ignore[attr-defined]
289
- "response_id": getattr(response, "id", None),
290
- "model_used": getattr(response, "model", None),
291
- "usage": {
292
- "input_tokens": response.usage.input_tokens if response.usage else None,
293
- "output_tokens": response.usage.output_tokens if response.usage else None,
294
- "total_tokens": response.usage.total_tokens if response.usage else None,
295
- } if response.usage else None,
371
+ "new_items": new_items,
372
+ "final_output": final_output,
296
373
  }
297
374
 
298
375
  return response