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.
- agents/__init__.py +105 -4
- agents/_debug.py +15 -4
- agents/_run_impl.py +1203 -96
- agents/agent.py +164 -19
- agents/apply_diff.py +329 -0
- agents/editor.py +47 -0
- agents/exceptions.py +35 -0
- agents/extensions/experimental/__init__.py +6 -0
- agents/extensions/experimental/codex/__init__.py +92 -0
- agents/extensions/experimental/codex/codex.py +89 -0
- agents/extensions/experimental/codex/codex_options.py +35 -0
- agents/extensions/experimental/codex/codex_tool.py +1142 -0
- agents/extensions/experimental/codex/events.py +162 -0
- agents/extensions/experimental/codex/exec.py +263 -0
- agents/extensions/experimental/codex/items.py +245 -0
- agents/extensions/experimental/codex/output_schema_file.py +50 -0
- agents/extensions/experimental/codex/payloads.py +31 -0
- agents/extensions/experimental/codex/thread.py +214 -0
- agents/extensions/experimental/codex/thread_options.py +54 -0
- agents/extensions/experimental/codex/turn_options.py +36 -0
- agents/extensions/handoff_filters.py +13 -1
- agents/extensions/memory/__init__.py +120 -0
- agents/extensions/memory/advanced_sqlite_session.py +1285 -0
- agents/extensions/memory/async_sqlite_session.py +239 -0
- agents/extensions/memory/dapr_session.py +423 -0
- agents/extensions/memory/encrypt_session.py +185 -0
- agents/extensions/memory/redis_session.py +261 -0
- agents/extensions/memory/sqlalchemy_session.py +334 -0
- agents/extensions/models/litellm_model.py +449 -36
- agents/extensions/models/litellm_provider.py +3 -1
- agents/function_schema.py +47 -5
- agents/guardrail.py +16 -2
- agents/{handoffs.py → handoffs/__init__.py} +89 -47
- agents/handoffs/history.py +268 -0
- agents/items.py +237 -11
- agents/lifecycle.py +75 -14
- agents/mcp/server.py +280 -37
- agents/mcp/util.py +24 -3
- agents/memory/__init__.py +22 -2
- agents/memory/openai_conversations_session.py +91 -0
- agents/memory/openai_responses_compaction_session.py +249 -0
- agents/memory/session.py +19 -261
- agents/memory/sqlite_session.py +275 -0
- agents/memory/util.py +20 -0
- agents/model_settings.py +14 -3
- agents/models/__init__.py +13 -0
- agents/models/chatcmpl_converter.py +303 -50
- agents/models/chatcmpl_helpers.py +63 -0
- agents/models/chatcmpl_stream_handler.py +290 -68
- agents/models/default_models.py +58 -0
- agents/models/interface.py +4 -0
- agents/models/openai_chatcompletions.py +103 -49
- agents/models/openai_provider.py +10 -4
- agents/models/openai_responses.py +162 -46
- agents/realtime/__init__.py +4 -0
- agents/realtime/_util.py +14 -3
- agents/realtime/agent.py +7 -0
- agents/realtime/audio_formats.py +53 -0
- agents/realtime/config.py +78 -10
- agents/realtime/events.py +18 -0
- agents/realtime/handoffs.py +2 -2
- agents/realtime/items.py +17 -1
- agents/realtime/model.py +13 -0
- agents/realtime/model_events.py +12 -0
- agents/realtime/model_inputs.py +18 -1
- agents/realtime/openai_realtime.py +696 -150
- agents/realtime/session.py +243 -23
- agents/repl.py +7 -3
- agents/result.py +197 -38
- agents/run.py +949 -168
- agents/run_context.py +13 -2
- agents/stream_events.py +1 -0
- agents/strict_schema.py +14 -0
- agents/tool.py +413 -15
- agents/tool_context.py +22 -1
- agents/tool_guardrails.py +279 -0
- agents/tracing/__init__.py +2 -0
- agents/tracing/config.py +9 -0
- agents/tracing/create.py +4 -0
- agents/tracing/processor_interface.py +84 -11
- agents/tracing/processors.py +65 -54
- agents/tracing/provider.py +64 -7
- agents/tracing/spans.py +105 -0
- agents/tracing/traces.py +116 -16
- agents/usage.py +134 -12
- agents/util/_json.py +19 -1
- agents/util/_transforms.py +12 -2
- agents/voice/input.py +5 -4
- agents/voice/models/openai_stt.py +17 -9
- agents/voice/pipeline.py +2 -0
- agents/voice/pipeline_config.py +4 -0
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/METADATA +44 -19
- openai_agents-0.6.8.dist-info/RECORD +134 -0
- {openai_agents-0.2.8.dist-info → openai_agents-0.6.8.dist-info}/WHEEL +1 -1
- openai_agents-0.2.8.dist-info/RECORD +0 -103
- {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
|
+
)
|