openai-agents 0.2.8__py3-none-any.whl → 0.6.8__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 (96) hide show
  1. agents/__init__.py +105 -4
  2. agents/_debug.py +15 -4
  3. agents/_run_impl.py +1203 -96
  4. agents/agent.py +164 -19
  5. agents/apply_diff.py +329 -0
  6. agents/editor.py +47 -0
  7. agents/exceptions.py +35 -0
  8. agents/extensions/experimental/__init__.py +6 -0
  9. agents/extensions/experimental/codex/__init__.py +92 -0
  10. agents/extensions/experimental/codex/codex.py +89 -0
  11. agents/extensions/experimental/codex/codex_options.py +35 -0
  12. agents/extensions/experimental/codex/codex_tool.py +1142 -0
  13. agents/extensions/experimental/codex/events.py +162 -0
  14. agents/extensions/experimental/codex/exec.py +263 -0
  15. agents/extensions/experimental/codex/items.py +245 -0
  16. agents/extensions/experimental/codex/output_schema_file.py +50 -0
  17. agents/extensions/experimental/codex/payloads.py +31 -0
  18. agents/extensions/experimental/codex/thread.py +214 -0
  19. agents/extensions/experimental/codex/thread_options.py +54 -0
  20. agents/extensions/experimental/codex/turn_options.py +36 -0
  21. agents/extensions/handoff_filters.py +13 -1
  22. agents/extensions/memory/__init__.py +120 -0
  23. agents/extensions/memory/advanced_sqlite_session.py +1285 -0
  24. agents/extensions/memory/async_sqlite_session.py +239 -0
  25. agents/extensions/memory/dapr_session.py +423 -0
  26. agents/extensions/memory/encrypt_session.py +185 -0
  27. agents/extensions/memory/redis_session.py +261 -0
  28. agents/extensions/memory/sqlalchemy_session.py +334 -0
  29. agents/extensions/models/litellm_model.py +449 -36
  30. agents/extensions/models/litellm_provider.py +3 -1
  31. agents/function_schema.py +47 -5
  32. agents/guardrail.py +16 -2
  33. agents/{handoffs.py → handoffs/__init__.py} +89 -47
  34. agents/handoffs/history.py +268 -0
  35. agents/items.py +237 -11
  36. agents/lifecycle.py +75 -14
  37. agents/mcp/server.py +280 -37
  38. agents/mcp/util.py +24 -3
  39. agents/memory/__init__.py +22 -2
  40. agents/memory/openai_conversations_session.py +91 -0
  41. agents/memory/openai_responses_compaction_session.py +249 -0
  42. agents/memory/session.py +19 -261
  43. agents/memory/sqlite_session.py +275 -0
  44. agents/memory/util.py +20 -0
  45. agents/model_settings.py +14 -3
  46. agents/models/__init__.py +13 -0
  47. agents/models/chatcmpl_converter.py +303 -50
  48. agents/models/chatcmpl_helpers.py +63 -0
  49. agents/models/chatcmpl_stream_handler.py +290 -68
  50. agents/models/default_models.py +58 -0
  51. agents/models/interface.py +4 -0
  52. agents/models/openai_chatcompletions.py +103 -49
  53. agents/models/openai_provider.py +10 -4
  54. agents/models/openai_responses.py +162 -46
  55. agents/realtime/__init__.py +4 -0
  56. agents/realtime/_util.py +14 -3
  57. agents/realtime/agent.py +7 -0
  58. agents/realtime/audio_formats.py +53 -0
  59. agents/realtime/config.py +78 -10
  60. agents/realtime/events.py +18 -0
  61. agents/realtime/handoffs.py +2 -2
  62. agents/realtime/items.py +17 -1
  63. agents/realtime/model.py +13 -0
  64. agents/realtime/model_events.py +12 -0
  65. agents/realtime/model_inputs.py +18 -1
  66. agents/realtime/openai_realtime.py +696 -150
  67. agents/realtime/session.py +243 -23
  68. agents/repl.py +7 -3
  69. agents/result.py +197 -38
  70. agents/run.py +949 -168
  71. agents/run_context.py +13 -2
  72. agents/stream_events.py +1 -0
  73. agents/strict_schema.py +14 -0
  74. agents/tool.py +413 -15
  75. agents/tool_context.py +22 -1
  76. agents/tool_guardrails.py +279 -0
  77. agents/tracing/__init__.py +2 -0
  78. agents/tracing/config.py +9 -0
  79. agents/tracing/create.py +4 -0
  80. agents/tracing/processor_interface.py +84 -11
  81. agents/tracing/processors.py +65 -54
  82. agents/tracing/provider.py +64 -7
  83. agents/tracing/spans.py +105 -0
  84. agents/tracing/traces.py +116 -16
  85. agents/usage.py +134 -12
  86. agents/util/_json.py +19 -1
  87. agents/util/_transforms.py +12 -2
  88. agents/voice/input.py +5 -4
  89. agents/voice/models/openai_stt.py +17 -9
  90. agents/voice/pipeline.py +2 -0
  91. agents/voice/pipeline_config.py +4 -0
  92. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/METADATA +44 -19
  93. openai_agents-0.6.8.dist-info/RECORD +134 -0
  94. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/WHEEL +1 -1
  95. openai_agents-0.2.8.dist-info/RECORD +0 -103
  96. {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,162 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass, field
5
+ from typing import Any, Union, cast
6
+
7
+ from typing_extensions import Literal, TypeAlias
8
+
9
+ from .items import ThreadItem, coerce_thread_item
10
+ from .payloads import _DictLike
11
+
12
+ # Event payloads emitted by the Codex CLI JSONL stream.
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ThreadStartedEvent(_DictLike):
17
+ thread_id: str
18
+ type: Literal["thread.started"] = field(default="thread.started", init=False)
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class TurnStartedEvent(_DictLike):
23
+ type: Literal["turn.started"] = field(default="turn.started", init=False)
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Usage(_DictLike):
28
+ input_tokens: int
29
+ cached_input_tokens: int
30
+ output_tokens: int
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class TurnCompletedEvent(_DictLike):
35
+ usage: Usage | None = None
36
+ type: Literal["turn.completed"] = field(default="turn.completed", init=False)
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class ThreadError(_DictLike):
41
+ message: str
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class TurnFailedEvent(_DictLike):
46
+ error: ThreadError
47
+ type: Literal["turn.failed"] = field(default="turn.failed", init=False)
48
+
49
+
50
+ @dataclass(frozen=True)
51
+ class ItemStartedEvent(_DictLike):
52
+ item: ThreadItem
53
+ type: Literal["item.started"] = field(default="item.started", init=False)
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class ItemUpdatedEvent(_DictLike):
58
+ item: ThreadItem
59
+ type: Literal["item.updated"] = field(default="item.updated", init=False)
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class ItemCompletedEvent(_DictLike):
64
+ item: ThreadItem
65
+ type: Literal["item.completed"] = field(default="item.completed", init=False)
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class ThreadErrorEvent(_DictLike):
70
+ message: str
71
+ type: Literal["error"] = field(default="error", init=False)
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class _UnknownThreadEvent(_DictLike):
76
+ type: str
77
+ payload: Mapping[str, Any] = field(default_factory=dict)
78
+
79
+
80
+ ThreadEvent: TypeAlias = Union[
81
+ ThreadStartedEvent,
82
+ TurnStartedEvent,
83
+ TurnCompletedEvent,
84
+ TurnFailedEvent,
85
+ ItemStartedEvent,
86
+ ItemUpdatedEvent,
87
+ ItemCompletedEvent,
88
+ ThreadErrorEvent,
89
+ _UnknownThreadEvent,
90
+ ]
91
+
92
+
93
+ def _coerce_thread_error(raw: ThreadError | Mapping[str, Any]) -> ThreadError:
94
+ if isinstance(raw, ThreadError):
95
+ return raw
96
+ if not isinstance(raw, Mapping):
97
+ raise TypeError("ThreadError must be a mapping.")
98
+ return ThreadError(message=cast(str, raw.get("message", "")))
99
+
100
+
101
+ def coerce_usage(raw: Usage | Mapping[str, Any]) -> Usage:
102
+ if isinstance(raw, Usage):
103
+ return raw
104
+ if not isinstance(raw, Mapping):
105
+ raise TypeError("Usage must be a mapping.")
106
+ return Usage(
107
+ input_tokens=cast(int, raw["input_tokens"]),
108
+ cached_input_tokens=cast(int, raw["cached_input_tokens"]),
109
+ output_tokens=cast(int, raw["output_tokens"]),
110
+ )
111
+
112
+
113
+ def coerce_thread_event(raw: ThreadEvent | Mapping[str, Any]) -> ThreadEvent:
114
+ if isinstance(raw, _DictLike):
115
+ return raw
116
+ if not isinstance(raw, Mapping):
117
+ raise TypeError("Thread event payload must be a mapping.")
118
+
119
+ event_type = raw.get("type")
120
+ if event_type == "thread.started":
121
+ return ThreadStartedEvent(thread_id=cast(str, raw["thread_id"]))
122
+ if event_type == "turn.started":
123
+ return TurnStartedEvent()
124
+ if event_type == "turn.completed":
125
+ usage_raw = raw.get("usage")
126
+ usage = coerce_usage(cast(Mapping[str, Any], usage_raw)) if usage_raw is not None else None
127
+ return TurnCompletedEvent(usage=usage)
128
+ if event_type == "turn.failed":
129
+ error_raw = raw.get("error", {})
130
+ error = _coerce_thread_error(cast(Mapping[str, Any], error_raw))
131
+ return TurnFailedEvent(error=error)
132
+ if event_type == "item.started":
133
+ item_raw = raw.get("item")
134
+ item = (
135
+ coerce_thread_item(cast(Union[ThreadItem, Mapping[str, Any]], item_raw))
136
+ if item_raw is not None
137
+ else coerce_thread_item({"type": "unknown"})
138
+ )
139
+ return ItemStartedEvent(item=item)
140
+ if event_type == "item.updated":
141
+ item_raw = raw.get("item")
142
+ item = (
143
+ coerce_thread_item(cast(Union[ThreadItem, Mapping[str, Any]], item_raw))
144
+ if item_raw is not None
145
+ else coerce_thread_item({"type": "unknown"})
146
+ )
147
+ return ItemUpdatedEvent(item=item)
148
+ if event_type == "item.completed":
149
+ item_raw = raw.get("item")
150
+ item = (
151
+ coerce_thread_item(cast(Union[ThreadItem, Mapping[str, Any]], item_raw))
152
+ if item_raw is not None
153
+ else coerce_thread_item({"type": "unknown"})
154
+ )
155
+ return ItemCompletedEvent(item=item)
156
+ if event_type == "error":
157
+ return ThreadErrorEvent(message=cast(str, raw.get("message", "")))
158
+
159
+ return _UnknownThreadEvent(
160
+ type=cast(str, event_type) if event_type is not None else "unknown",
161
+ payload=dict(raw),
162
+ )
@@ -0,0 +1,263 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import os
6
+ import platform
7
+ import shutil
8
+ import sys
9
+ from collections.abc import AsyncGenerator
10
+ from dataclasses import dataclass
11
+ from pathlib import Path
12
+
13
+ from .thread_options import ApprovalMode, ModelReasoningEffort, SandboxMode, WebSearchMode
14
+
15
+ _INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
16
+ _TYPESCRIPT_SDK_ORIGINATOR = "codex_sdk_ts"
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class CodexExecArgs:
21
+ input: str
22
+ base_url: str | None = None
23
+ api_key: str | None = None
24
+ thread_id: str | None = None
25
+ images: list[str] | None = None
26
+ model: str | None = None
27
+ sandbox_mode: SandboxMode | None = None
28
+ working_directory: str | None = None
29
+ additional_directories: list[str] | None = None
30
+ skip_git_repo_check: bool | None = None
31
+ output_schema_file: str | None = None
32
+ model_reasoning_effort: ModelReasoningEffort | None = None
33
+ signal: asyncio.Event | None = None
34
+ idle_timeout_seconds: float | None = None
35
+ network_access_enabled: bool | None = None
36
+ web_search_mode: WebSearchMode | None = None
37
+ web_search_enabled: bool | None = None
38
+ approval_policy: ApprovalMode | None = None
39
+
40
+
41
+ class CodexExec:
42
+ def __init__(
43
+ self,
44
+ *,
45
+ executable_path: str | None = None,
46
+ env: dict[str, str] | None = None,
47
+ ) -> None:
48
+ self._executable_path = executable_path or find_codex_path()
49
+ self._env_override = env
50
+
51
+ async def run(self, args: CodexExecArgs) -> AsyncGenerator[str, None]:
52
+ # Build the CLI args for `codex exec --experimental-json`.
53
+ command_args: list[str] = ["exec", "--experimental-json"]
54
+
55
+ if args.model:
56
+ command_args.extend(["--model", args.model])
57
+
58
+ if args.sandbox_mode:
59
+ command_args.extend(["--sandbox", args.sandbox_mode])
60
+
61
+ if args.working_directory:
62
+ command_args.extend(["--cd", args.working_directory])
63
+
64
+ if args.additional_directories:
65
+ for directory in args.additional_directories:
66
+ command_args.extend(["--add-dir", directory])
67
+
68
+ if args.skip_git_repo_check:
69
+ command_args.append("--skip-git-repo-check")
70
+
71
+ if args.output_schema_file:
72
+ command_args.extend(["--output-schema", args.output_schema_file])
73
+
74
+ if args.model_reasoning_effort:
75
+ command_args.extend(
76
+ ["--config", f'model_reasoning_effort="{args.model_reasoning_effort}"']
77
+ )
78
+
79
+ if args.network_access_enabled is not None:
80
+ command_args.extend(
81
+ [
82
+ "--config",
83
+ f"sandbox_workspace_write.network_access={str(args.network_access_enabled).lower()}",
84
+ ]
85
+ )
86
+
87
+ if args.web_search_mode:
88
+ command_args.extend(["--config", f'web_search="{args.web_search_mode}"'])
89
+ elif args.web_search_enabled is True:
90
+ command_args.extend(["--config", 'web_search="live"'])
91
+ elif args.web_search_enabled is False:
92
+ command_args.extend(["--config", 'web_search="disabled"'])
93
+
94
+ if args.approval_policy:
95
+ command_args.extend(["--config", f'approval_policy="{args.approval_policy}"'])
96
+
97
+ if args.thread_id:
98
+ command_args.extend(["resume", args.thread_id])
99
+
100
+ if args.images:
101
+ for image in args.images:
102
+ command_args.extend(["--image", image])
103
+
104
+ # Codex CLI expects a prompt argument; "-" tells it to read from stdin.
105
+ command_args.append("-")
106
+
107
+ env = self._build_env(args)
108
+
109
+ process = await asyncio.create_subprocess_exec(
110
+ self._executable_path,
111
+ *command_args,
112
+ stdin=asyncio.subprocess.PIPE,
113
+ stdout=asyncio.subprocess.PIPE,
114
+ stderr=asyncio.subprocess.PIPE,
115
+ env=env,
116
+ )
117
+
118
+ stderr_chunks: list[bytes] = []
119
+
120
+ async def _drain_stderr() -> None:
121
+ # Preserve stderr for error reporting without blocking stdout reads.
122
+ if process.stderr is None:
123
+ return
124
+ while True:
125
+ chunk = await process.stderr.read(1024)
126
+ if not chunk:
127
+ break
128
+ stderr_chunks.append(chunk)
129
+
130
+ stderr_task = asyncio.create_task(_drain_stderr())
131
+
132
+ if process.stdin is None:
133
+ process.kill()
134
+ raise RuntimeError("Codex subprocess has no stdin")
135
+
136
+ process.stdin.write(args.input.encode("utf-8"))
137
+ await process.stdin.drain()
138
+ process.stdin.close()
139
+
140
+ if process.stdout is None:
141
+ process.kill()
142
+ raise RuntimeError("Codex subprocess has no stdout")
143
+ stdout = process.stdout
144
+
145
+ cancel_task: asyncio.Task[None] | None = None
146
+ if args.signal is not None:
147
+ # Mirror AbortSignal semantics by terminating the subprocess.
148
+ cancel_task = asyncio.create_task(_watch_signal(args.signal, process))
149
+
150
+ async def _read_stdout_line() -> bytes:
151
+ if args.idle_timeout_seconds is None:
152
+ return await stdout.readline()
153
+
154
+ read_task: asyncio.Task[bytes] = asyncio.create_task(stdout.readline())
155
+ done, _ = await asyncio.wait(
156
+ {read_task}, timeout=args.idle_timeout_seconds, return_when=asyncio.FIRST_COMPLETED
157
+ )
158
+ if read_task in done:
159
+ return read_task.result()
160
+
161
+ if args.signal is not None:
162
+ args.signal.set()
163
+ if process.returncode is None:
164
+ process.terminate()
165
+
166
+ read_task.cancel()
167
+ with contextlib.suppress(asyncio.CancelledError, asyncio.TimeoutError):
168
+ await asyncio.wait_for(read_task, timeout=1)
169
+
170
+ raise RuntimeError(f"Codex stream idle for {args.idle_timeout_seconds} seconds.")
171
+
172
+ try:
173
+ while True:
174
+ line = await _read_stdout_line()
175
+ if not line:
176
+ break
177
+ yield line.decode("utf-8").rstrip("\n")
178
+
179
+ await process.wait()
180
+ if cancel_task is not None:
181
+ cancel_task.cancel()
182
+ with contextlib.suppress(asyncio.CancelledError):
183
+ await cancel_task
184
+
185
+ if process.returncode not in (0, None):
186
+ await stderr_task
187
+ stderr_text = b"".join(stderr_chunks).decode("utf-8")
188
+ raise RuntimeError(
189
+ f"Codex exec exited with code {process.returncode}: {stderr_text}"
190
+ )
191
+ finally:
192
+ if cancel_task is not None and not cancel_task.done():
193
+ cancel_task.cancel()
194
+ await stderr_task
195
+ if process.returncode is None:
196
+ process.kill()
197
+
198
+ def _build_env(self, args: CodexExecArgs) -> dict[str, str]:
199
+ # Respect env overrides when provided; otherwise copy from os.environ.
200
+ env: dict[str, str] = {}
201
+ if self._env_override is not None:
202
+ env.update(self._env_override)
203
+ else:
204
+ env.update({key: value for key, value in os.environ.items() if value is not None})
205
+
206
+ # Preserve originator metadata used by the CLI.
207
+ if _INTERNAL_ORIGINATOR_ENV not in env:
208
+ env[_INTERNAL_ORIGINATOR_ENV] = _TYPESCRIPT_SDK_ORIGINATOR
209
+
210
+ if args.base_url:
211
+ env["OPENAI_BASE_URL"] = args.base_url
212
+ if args.api_key:
213
+ env["CODEX_API_KEY"] = args.api_key
214
+
215
+ return env
216
+
217
+
218
+ async def _watch_signal(signal: asyncio.Event, process: asyncio.subprocess.Process) -> None:
219
+ await signal.wait()
220
+ if process.returncode is None:
221
+ process.terminate()
222
+
223
+
224
+ def _platform_target_triple() -> str:
225
+ # Map the running platform to the vendor layout used in Codex releases.
226
+ system = sys.platform
227
+ arch = platform.machine().lower()
228
+
229
+ if system.startswith("linux"):
230
+ if arch in {"x86_64", "amd64"}:
231
+ return "x86_64-unknown-linux-musl"
232
+ if arch in {"aarch64", "arm64"}:
233
+ return "aarch64-unknown-linux-musl"
234
+ if system == "darwin":
235
+ if arch in {"x86_64", "amd64"}:
236
+ return "x86_64-apple-darwin"
237
+ if arch in {"arm64", "aarch64"}:
238
+ return "aarch64-apple-darwin"
239
+ if system in {"win32", "cygwin"}:
240
+ if arch in {"x86_64", "amd64"}:
241
+ return "x86_64-pc-windows-msvc"
242
+ if arch in {"arm64", "aarch64"}:
243
+ return "aarch64-pc-windows-msvc"
244
+
245
+ raise RuntimeError(f"Unsupported platform: {system} ({arch})")
246
+
247
+
248
+ def find_codex_path() -> str:
249
+ # Resolution order: CODEX_PATH env, PATH lookup, bundled vendor binary.
250
+ path_override = os.environ.get("CODEX_PATH")
251
+ if path_override:
252
+ return path_override
253
+
254
+ which_path = shutil.which("codex")
255
+ if which_path:
256
+ return which_path
257
+
258
+ target_triple = _platform_target_triple()
259
+ vendor_root = Path(__file__).resolve().parent.parent.parent / "vendor"
260
+ arch_root = vendor_root / target_triple
261
+ binary_name = "codex.exe" if sys.platform.startswith("win") else "codex"
262
+ binary_path = arch_root / "codex" / binary_name
263
+ return str(binary_path)
@@ -0,0 +1,245 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING, Any, Optional, Union, cast
6
+
7
+ from typing_extensions import Literal, TypeAlias, TypeGuard
8
+
9
+ from .payloads import _DictLike
10
+
11
+ # Item payloads are emitted inside item.* events from the Codex CLI JSONL stream.
12
+
13
+ if TYPE_CHECKING:
14
+ from mcp.types import ContentBlock as McpContentBlock
15
+ else:
16
+ McpContentBlock = Any # type: ignore[assignment]
17
+
18
+ CommandExecutionStatus = Literal["in_progress", "completed", "failed"]
19
+ PatchChangeKind = Literal["add", "delete", "update"]
20
+ PatchApplyStatus = Literal["completed", "failed"]
21
+ McpToolCallStatus = Literal["in_progress", "completed", "failed"]
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class CommandExecutionItem(_DictLike):
26
+ id: str
27
+ command: str
28
+ status: CommandExecutionStatus
29
+ aggregated_output: str = ""
30
+ exit_code: int | None = None
31
+ type: Literal["command_execution"] = field(default="command_execution", init=False)
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class FileUpdateChange(_DictLike):
36
+ path: str
37
+ kind: PatchChangeKind
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class FileChangeItem(_DictLike):
42
+ id: str
43
+ changes: list[FileUpdateChange]
44
+ status: PatchApplyStatus
45
+ type: Literal["file_change"] = field(default="file_change", init=False)
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class McpToolCallResult(_DictLike):
50
+ content: list[McpContentBlock]
51
+ structured_content: Any
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class McpToolCallError(_DictLike):
56
+ message: str
57
+
58
+
59
+ @dataclass(frozen=True)
60
+ class McpToolCallItem(_DictLike):
61
+ id: str
62
+ server: str
63
+ tool: str
64
+ arguments: Any
65
+ status: McpToolCallStatus
66
+ result: McpToolCallResult | None = None
67
+ error: McpToolCallError | None = None
68
+ type: Literal["mcp_tool_call"] = field(default="mcp_tool_call", init=False)
69
+
70
+
71
+ @dataclass(frozen=True)
72
+ class AgentMessageItem(_DictLike):
73
+ id: str
74
+ text: str
75
+ type: Literal["agent_message"] = field(default="agent_message", init=False)
76
+
77
+
78
+ @dataclass(frozen=True)
79
+ class ReasoningItem(_DictLike):
80
+ id: str
81
+ text: str
82
+ type: Literal["reasoning"] = field(default="reasoning", init=False)
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class WebSearchItem(_DictLike):
87
+ id: str
88
+ query: str
89
+ type: Literal["web_search"] = field(default="web_search", init=False)
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class ErrorItem(_DictLike):
94
+ id: str
95
+ message: str
96
+ type: Literal["error"] = field(default="error", init=False)
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class TodoItem(_DictLike):
101
+ text: str
102
+ completed: bool
103
+
104
+
105
+ @dataclass(frozen=True)
106
+ class TodoListItem(_DictLike):
107
+ id: str
108
+ items: list[TodoItem]
109
+ type: Literal["todo_list"] = field(default="todo_list", init=False)
110
+
111
+
112
+ @dataclass(frozen=True)
113
+ class _UnknownThreadItem(_DictLike):
114
+ type: str
115
+ payload: Mapping[str, Any] = field(default_factory=dict)
116
+ id: str | None = None
117
+
118
+
119
+ ThreadItem: TypeAlias = Union[
120
+ AgentMessageItem,
121
+ ReasoningItem,
122
+ CommandExecutionItem,
123
+ FileChangeItem,
124
+ McpToolCallItem,
125
+ WebSearchItem,
126
+ TodoListItem,
127
+ ErrorItem,
128
+ _UnknownThreadItem,
129
+ ]
130
+
131
+
132
+ def is_agent_message_item(item: ThreadItem) -> TypeGuard[AgentMessageItem]:
133
+ return isinstance(item, AgentMessageItem)
134
+
135
+
136
+ def _coerce_file_update_change(
137
+ raw: FileUpdateChange | Mapping[str, Any],
138
+ ) -> FileUpdateChange:
139
+ if isinstance(raw, FileUpdateChange):
140
+ return raw
141
+ if not isinstance(raw, Mapping):
142
+ raise TypeError("FileUpdateChange must be a mapping.")
143
+ return FileUpdateChange(
144
+ path=cast(str, raw["path"]),
145
+ kind=cast(PatchChangeKind, raw["kind"]),
146
+ )
147
+
148
+
149
+ def _coerce_mcp_tool_call_result(
150
+ raw: McpToolCallResult | Mapping[str, Any],
151
+ ) -> McpToolCallResult:
152
+ if isinstance(raw, McpToolCallResult):
153
+ return raw
154
+ if not isinstance(raw, Mapping):
155
+ raise TypeError("McpToolCallResult must be a mapping.")
156
+ content = cast(list[McpContentBlock], raw.get("content", []))
157
+ return McpToolCallResult(
158
+ content=content,
159
+ structured_content=raw.get("structured_content"),
160
+ )
161
+
162
+
163
+ def _coerce_mcp_tool_call_error(
164
+ raw: McpToolCallError | Mapping[str, Any],
165
+ ) -> McpToolCallError:
166
+ if isinstance(raw, McpToolCallError):
167
+ return raw
168
+ if not isinstance(raw, Mapping):
169
+ raise TypeError("McpToolCallError must be a mapping.")
170
+ return McpToolCallError(message=cast(str, raw.get("message", "")))
171
+
172
+
173
+ def coerce_thread_item(raw: ThreadItem | Mapping[str, Any]) -> ThreadItem:
174
+ if isinstance(raw, _DictLike):
175
+ return raw
176
+ if not isinstance(raw, Mapping):
177
+ raise TypeError("Thread item payload must be a mapping.")
178
+
179
+ item_type = raw.get("type")
180
+ if item_type == "command_execution":
181
+ return CommandExecutionItem(
182
+ id=cast(str, raw["id"]),
183
+ command=cast(str, raw["command"]),
184
+ aggregated_output=cast(str, raw.get("aggregated_output", "")),
185
+ status=cast(CommandExecutionStatus, raw["status"]),
186
+ exit_code=cast(Optional[int], raw.get("exit_code")),
187
+ )
188
+ if item_type == "file_change":
189
+ changes = [_coerce_file_update_change(change) for change in raw.get("changes", [])]
190
+ return FileChangeItem(
191
+ id=cast(str, raw["id"]),
192
+ changes=changes,
193
+ status=cast(PatchApplyStatus, raw["status"]),
194
+ )
195
+ if item_type == "mcp_tool_call":
196
+ result_raw = raw.get("result")
197
+ error_raw = raw.get("error")
198
+ result = None
199
+ error = None
200
+ if result_raw is not None:
201
+ result = _coerce_mcp_tool_call_result(cast(Mapping[str, Any], result_raw))
202
+ if error_raw is not None:
203
+ error = _coerce_mcp_tool_call_error(cast(Mapping[str, Any], error_raw))
204
+ return McpToolCallItem(
205
+ id=cast(str, raw["id"]),
206
+ server=cast(str, raw["server"]),
207
+ tool=cast(str, raw["tool"]),
208
+ arguments=raw.get("arguments"),
209
+ status=cast(McpToolCallStatus, raw["status"]),
210
+ result=result,
211
+ error=error,
212
+ )
213
+ if item_type == "agent_message":
214
+ return AgentMessageItem(
215
+ id=cast(str, raw["id"]),
216
+ text=cast(str, raw.get("text", "")),
217
+ )
218
+ if item_type == "reasoning":
219
+ return ReasoningItem(
220
+ id=cast(str, raw["id"]),
221
+ text=cast(str, raw.get("text", "")),
222
+ )
223
+ if item_type == "web_search":
224
+ return WebSearchItem(
225
+ id=cast(str, raw["id"]),
226
+ query=cast(str, raw.get("query", "")),
227
+ )
228
+ if item_type == "todo_list":
229
+ items_raw = raw.get("items", [])
230
+ items = [
231
+ TodoItem(text=cast(str, item.get("text", "")), completed=bool(item.get("completed")))
232
+ for item in cast(list[Mapping[str, Any]], items_raw)
233
+ ]
234
+ return TodoListItem(id=cast(str, raw["id"]), items=items)
235
+ if item_type == "error":
236
+ return ErrorItem(
237
+ id=cast(str, raw.get("id", "")),
238
+ message=cast(str, raw.get("message", "")),
239
+ )
240
+
241
+ return _UnknownThreadItem(
242
+ type=cast(str, item_type) if item_type is not None else "unknown",
243
+ payload=dict(raw),
244
+ id=cast(Optional[str], raw.get("id")),
245
+ )