mirror of
https://github.com/openai/codex.git
synced 2026-05-02 12:21:26 +03:00
temp
This commit is contained in:
39
sdk/python/src/codex_sdk/__init__.py
Normal file
39
sdk/python/src/codex_sdk/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
||||
from .codex import Codex
|
||||
from .options import CodexOptions, ThreadOptions, TurnOptions
|
||||
from .thread import Input, StreamedTurn, Thread, ThreadRunError, TurnResult
|
||||
from .types import (
|
||||
AgentMessageItem,
|
||||
CommandExecutionItem,
|
||||
ErrorItem,
|
||||
FileChangeItem,
|
||||
McpToolCallItem,
|
||||
ReasoningItem,
|
||||
ThreadEvent,
|
||||
ThreadItem,
|
||||
TodoListItem,
|
||||
Usage,
|
||||
WebSearchItem,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Codex",
|
||||
"CodexOptions",
|
||||
"ThreadOptions",
|
||||
"TurnOptions",
|
||||
"Thread",
|
||||
"ThreadRunError",
|
||||
"TurnResult",
|
||||
"StreamedTurn",
|
||||
"Input",
|
||||
"ThreadEvent",
|
||||
"ThreadItem",
|
||||
"Usage",
|
||||
"AgentMessageItem",
|
||||
"ReasoningItem",
|
||||
"CommandExecutionItem",
|
||||
"FileChangeItem",
|
||||
"McpToolCallItem",
|
||||
"WebSearchItem",
|
||||
"TodoListItem",
|
||||
"ErrorItem",
|
||||
]
|
||||
BIN
sdk/python/src/codex_sdk/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
sdk/python/src/codex_sdk/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
sdk/python/src/codex_sdk/__pycache__/codex.cpython-312.pyc
Normal file
BIN
sdk/python/src/codex_sdk/__pycache__/codex.cpython-312.pyc
Normal file
Binary file not shown.
BIN
sdk/python/src/codex_sdk/__pycache__/exec.cpython-312.pyc
Normal file
BIN
sdk/python/src/codex_sdk/__pycache__/exec.cpython-312.pyc
Normal file
Binary file not shown.
BIN
sdk/python/src/codex_sdk/__pycache__/options.cpython-312.pyc
Normal file
BIN
sdk/python/src/codex_sdk/__pycache__/options.cpython-312.pyc
Normal file
Binary file not shown.
BIN
sdk/python/src/codex_sdk/__pycache__/schema_file.cpython-312.pyc
Normal file
BIN
sdk/python/src/codex_sdk/__pycache__/schema_file.cpython-312.pyc
Normal file
Binary file not shown.
BIN
sdk/python/src/codex_sdk/__pycache__/thread.cpython-312.pyc
Normal file
BIN
sdk/python/src/codex_sdk/__pycache__/thread.cpython-312.pyc
Normal file
Binary file not shown.
BIN
sdk/python/src/codex_sdk/__pycache__/types.cpython-312.pyc
Normal file
BIN
sdk/python/src/codex_sdk/__pycache__/types.cpython-312.pyc
Normal file
Binary file not shown.
22
sdk/python/src/codex_sdk/codex.py
Normal file
22
sdk/python/src/codex_sdk/codex.py
Normal file
@@ -0,0 +1,22 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from .exec import CodexExec
|
||||
from .options import CodexOptions, ThreadOptions
|
||||
from .thread import Thread
|
||||
|
||||
|
||||
class Codex:
|
||||
"""Main entry point for interacting with the Codex agent."""
|
||||
|
||||
def __init__(self, options: Optional[CodexOptions] = None) -> None:
|
||||
opts = options or CodexOptions()
|
||||
self._options = opts
|
||||
self._exec = CodexExec(opts.codex_path_override, opts.env)
|
||||
|
||||
def start_thread(self, options: Optional[ThreadOptions] = None) -> Thread:
|
||||
return Thread(self._exec, self._options, options or ThreadOptions(), None)
|
||||
|
||||
def resume_thread(self, thread_id: str, options: Optional[ThreadOptions] = None) -> Thread:
|
||||
return Thread(self._exec, self._options, options or ThreadOptions(), thread_id)
|
||||
196
sdk/python/src/codex_sdk/exec.py
Normal file
196
sdk/python/src/codex_sdk/exec.py
Normal file
@@ -0,0 +1,196 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Dict, Generator, List, Optional
|
||||
|
||||
from .options import CancellationEvent
|
||||
|
||||
INTERNAL_ORIGINATOR_ENV = "CODEX_INTERNAL_ORIGINATOR_OVERRIDE"
|
||||
PYTHON_SDK_ORIGINATOR = "codex_sdk_py"
|
||||
|
||||
|
||||
class CancelledError(Exception):
|
||||
"""Raised when a turn is cancelled before completion."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodexExecArgs:
|
||||
input: str
|
||||
base_url: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
thread_id: Optional[str] = None
|
||||
images: Optional[List[str]] = None
|
||||
model: Optional[str] = None
|
||||
sandbox_mode: Optional[str] = None
|
||||
working_directory: Optional[str] = None
|
||||
additional_directories: Optional[List[str]] = None
|
||||
skip_git_repo_check: bool = False
|
||||
output_schema_file: Optional[str] = None
|
||||
model_reasoning_effort: Optional[str] = None
|
||||
cancellation_event: Optional[CancellationEvent] = None
|
||||
network_access_enabled: Optional[bool] = None
|
||||
web_search_enabled: Optional[bool] = None
|
||||
approval_policy: Optional[str] = None
|
||||
|
||||
|
||||
class CodexExec:
|
||||
def __init__(self, executable_path: Optional[str] = None, env: Optional[Dict[str, str]] = None) -> None:
|
||||
self.executable_path = executable_path or find_codex_path()
|
||||
self.env_override = env
|
||||
|
||||
def run(self, args: CodexExecArgs) -> Generator[str, None, None]:
|
||||
cancel_event = args.cancellation_event
|
||||
if cancel_event and cancel_event.is_set():
|
||||
raise CancelledError("Turn cancelled before start")
|
||||
|
||||
command_args: list[str] = ["exec", "--experimental-json"]
|
||||
if args.model:
|
||||
command_args.extend(["--model", args.model])
|
||||
if args.sandbox_mode:
|
||||
command_args.extend(["--sandbox", args.sandbox_mode])
|
||||
if args.working_directory:
|
||||
command_args.extend(["--cd", args.working_directory])
|
||||
if args.additional_directories:
|
||||
for extra_dir in args.additional_directories:
|
||||
command_args.extend(["--add-dir", extra_dir])
|
||||
if args.skip_git_repo_check:
|
||||
command_args.append("--skip-git-repo-check")
|
||||
if args.output_schema_file:
|
||||
command_args.extend(["--output-schema", args.output_schema_file])
|
||||
if args.model_reasoning_effort:
|
||||
command_args.extend(["--config", f'model_reasoning_effort="{args.model_reasoning_effort}"'])
|
||||
if args.network_access_enabled is not None:
|
||||
command_args.extend(
|
||||
["--config", f"sandbox_workspace_write.network_access={str(args.network_access_enabled).lower()}"]
|
||||
)
|
||||
if args.web_search_enabled is not None:
|
||||
command_args.extend(["--config", f"features.web_search_request={str(args.web_search_enabled).lower()}"])
|
||||
if args.approval_policy:
|
||||
command_args.extend(["--config", f'approval_policy="{args.approval_policy}"'])
|
||||
if args.images:
|
||||
for image in args.images:
|
||||
command_args.extend(["--image", image])
|
||||
if args.thread_id:
|
||||
command_args.extend(["resume", args.thread_id])
|
||||
|
||||
env = self._build_env(args)
|
||||
|
||||
process: Optional[subprocess.Popen[str]] = None
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
[self.executable_path, *command_args],
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1,
|
||||
env=env,
|
||||
)
|
||||
if not process.stdin or not process.stdout:
|
||||
raise RuntimeError("Failed to open stdio for Codex process")
|
||||
|
||||
process.stdin.write(args.input)
|
||||
process.stdin.close()
|
||||
|
||||
if cancel_event and cancel_event.is_set():
|
||||
raise CancelledError("Turn cancelled before first event")
|
||||
|
||||
for raw_line in process.stdout:
|
||||
if cancel_event and cancel_event.is_set():
|
||||
raise CancelledError("Turn cancelled")
|
||||
yield raw_line.rstrip("\r\n")
|
||||
|
||||
process.wait()
|
||||
if cancel_event and cancel_event.is_set():
|
||||
raise CancelledError("Turn cancelled after process exit")
|
||||
if process.returncode:
|
||||
stderr_output = process.stderr.read() if process.stderr else ""
|
||||
raise RuntimeError(f"Codex Exec exited with code {process.returncode}: {stderr_output}")
|
||||
except CancelledError:
|
||||
if process:
|
||||
terminate_process(process)
|
||||
raise
|
||||
except Exception:
|
||||
if process:
|
||||
terminate_process(process)
|
||||
raise
|
||||
finally:
|
||||
if process:
|
||||
if process.poll() is None:
|
||||
terminate_process(process)
|
||||
if process.stdout and not process.stdout.closed:
|
||||
process.stdout.close()
|
||||
if process.stderr and not process.stderr.closed:
|
||||
process.stderr.close()
|
||||
|
||||
def _build_env(self, args: CodexExecArgs) -> Dict[str, str]:
|
||||
env: Dict[str, str] = {}
|
||||
if self.env_override is not None:
|
||||
env.update(self.env_override)
|
||||
else:
|
||||
env.update({key: value for key, value in os.environ.items() if value is not None})
|
||||
|
||||
if INTERNAL_ORIGINATOR_ENV not in env:
|
||||
env[INTERNAL_ORIGINATOR_ENV] = PYTHON_SDK_ORIGINATOR
|
||||
if args.base_url:
|
||||
env["OPENAI_BASE_URL"] = args.base_url
|
||||
if args.api_key:
|
||||
env["CODEX_API_KEY"] = args.api_key
|
||||
return env
|
||||
|
||||
|
||||
def terminate_process(process: subprocess.Popen[str]) -> None:
|
||||
try:
|
||||
if process.poll() is None:
|
||||
process.terminate()
|
||||
try:
|
||||
process.wait(timeout=2)
|
||||
except Exception:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
except Exception:
|
||||
try:
|
||||
if process.poll() is None:
|
||||
process.kill()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def find_codex_path() -> str:
|
||||
platform_name = sys.platform
|
||||
machine = platform.machine().lower()
|
||||
|
||||
target_triple = None
|
||||
if platform_name.startswith("linux") or platform_name == "android":
|
||||
if machine in {"x86_64", "amd64"}:
|
||||
target_triple = "x86_64-unknown-linux-musl"
|
||||
elif machine in {"aarch64", "arm64"}:
|
||||
target_triple = "aarch64-unknown-linux-musl"
|
||||
elif platform_name == "darwin":
|
||||
if machine in {"x86_64", "amd64"}:
|
||||
target_triple = "x86_64-apple-darwin"
|
||||
elif machine in {"arm64", "aarch64"}:
|
||||
target_triple = "aarch64-apple-darwin"
|
||||
elif platform_name == "win32":
|
||||
if machine in {"x86_64", "amd64"}:
|
||||
target_triple = "x86_64-pc-windows-msvc"
|
||||
elif machine in {"arm64", "aarch64"}:
|
||||
target_triple = "aarch64-pc-windows-msvc"
|
||||
|
||||
if target_triple is None:
|
||||
raise RuntimeError(f"Unsupported platform: {platform_name} ({machine})")
|
||||
|
||||
package_root = Path(__file__).resolve().parent.parent
|
||||
vendor_root = package_root / "vendor" / target_triple / "codex"
|
||||
binary_name = "codex.exe" if platform_name == "win32" else "codex"
|
||||
binary_path = vendor_root / binary_name
|
||||
if not binary_path.exists():
|
||||
raise RuntimeError(
|
||||
f"Codex binary not found at {binary_path}. "
|
||||
"Install Codex or provide codex_path_override when creating the client."
|
||||
)
|
||||
return str(binary_path)
|
||||
36
sdk/python/src/codex_sdk/options.py
Normal file
36
sdk/python/src/codex_sdk/options.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Optional, Protocol
|
||||
|
||||
|
||||
class CancellationEvent(Protocol):
|
||||
def is_set(self) -> bool:
|
||||
...
|
||||
|
||||
|
||||
@dataclass
|
||||
class CodexOptions:
|
||||
codex_path_override: Optional[str] = None
|
||||
base_url: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
env: Optional[Dict[str, str]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThreadOptions:
|
||||
model: Optional[str] = None
|
||||
sandbox_mode: Optional[str] = None
|
||||
working_directory: Optional[str] = None
|
||||
skip_git_repo_check: bool = False
|
||||
model_reasoning_effort: Optional[str] = None
|
||||
network_access_enabled: Optional[bool] = None
|
||||
web_search_enabled: Optional[bool] = None
|
||||
approval_policy: Optional[str] = None
|
||||
additional_directories: Optional[list[str]] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurnOptions:
|
||||
output_schema: Optional[object] = None
|
||||
cancellation_event: Optional[CancellationEvent] = None
|
||||
38
sdk/python/src/codex_sdk/schema_file.py
Normal file
38
sdk/python/src/codex_sdk/schema_file.py
Normal file
@@ -0,0 +1,38 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import shutil
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Callable, Optional, Tuple
|
||||
|
||||
|
||||
SchemaFile = Tuple[Optional[str], Callable[[], None]]
|
||||
|
||||
|
||||
def create_output_schema_file(schema: object) -> SchemaFile:
|
||||
if schema is None:
|
||||
return None, _noop_cleanup
|
||||
|
||||
if not isinstance(schema, dict):
|
||||
raise ValueError("output_schema must be a plain JSON object")
|
||||
|
||||
schema_dir = Path(tempfile.mkdtemp(prefix="codex-output-schema-"))
|
||||
schema_path = schema_dir / "schema.json"
|
||||
|
||||
def cleanup() -> None:
|
||||
try:
|
||||
shutil.rmtree(schema_dir, ignore_errors=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
try:
|
||||
schema_path.write_text(json.dumps(schema), encoding="utf-8")
|
||||
return str(schema_path), cleanup
|
||||
except Exception:
|
||||
cleanup()
|
||||
raise
|
||||
|
||||
|
||||
def _noop_cleanup() -> None:
|
||||
return None
|
||||
140
sdk/python/src/codex_sdk/thread.py
Normal file
140
sdk/python/src/codex_sdk/thread.py
Normal file
@@ -0,0 +1,140 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator, List, Optional, Union
|
||||
|
||||
from .exec import CancelledError, CodexExec, CodexExecArgs
|
||||
from .options import CodexOptions, ThreadOptions, TurnOptions
|
||||
from .schema_file import create_output_schema_file
|
||||
from .types import (
|
||||
AgentMessageItem,
|
||||
ItemCompletedEvent,
|
||||
ThreadErrorEvent,
|
||||
ThreadEvent,
|
||||
ThreadItem,
|
||||
ThreadStartedEvent,
|
||||
TurnCompletedEvent,
|
||||
TurnFailedEvent,
|
||||
Usage,
|
||||
parse_thread_event,
|
||||
)
|
||||
|
||||
InputEntry = dict
|
||||
Input = Union[str, List[InputEntry]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurnResult:
|
||||
items: List[ThreadItem]
|
||||
final_response: str
|
||||
usage: Optional[Usage]
|
||||
|
||||
|
||||
@dataclass
|
||||
class StreamedTurn:
|
||||
events: Generator[ThreadEvent, None, None]
|
||||
|
||||
|
||||
class ThreadRunError(Exception):
|
||||
"""Raised when a turn fails."""
|
||||
|
||||
|
||||
class Thread:
|
||||
def __init__(
|
||||
self,
|
||||
exec_client: CodexExec,
|
||||
options: CodexOptions,
|
||||
thread_options: ThreadOptions,
|
||||
thread_id: Optional[str] = None,
|
||||
) -> None:
|
||||
self._exec = exec_client
|
||||
self._options = options
|
||||
self._thread_options = thread_options
|
||||
self._id = thread_id
|
||||
|
||||
@property
|
||||
def id(self) -> Optional[str]:
|
||||
return self._id
|
||||
|
||||
def run_streamed(self, input: Input, turn_options: Optional[TurnOptions] = None) -> StreamedTurn:
|
||||
return StreamedTurn(events=self._run_streamed_internal(input, turn_options or TurnOptions()))
|
||||
|
||||
def _run_streamed_internal(
|
||||
self, input: Input, turn_options: TurnOptions
|
||||
) -> Generator[ThreadEvent, None, None]:
|
||||
prompt, images = normalize_input(input)
|
||||
schema_path, cleanup = create_output_schema_file(turn_options.output_schema)
|
||||
args = CodexExecArgs(
|
||||
input=prompt,
|
||||
base_url=self._options.base_url,
|
||||
api_key=self._options.api_key,
|
||||
thread_id=self._id,
|
||||
images=images,
|
||||
model=self._thread_options.model,
|
||||
sandbox_mode=self._thread_options.sandbox_mode,
|
||||
working_directory=self._thread_options.working_directory,
|
||||
additional_directories=self._thread_options.additional_directories,
|
||||
skip_git_repo_check=self._thread_options.skip_git_repo_check,
|
||||
output_schema_file=schema_path,
|
||||
model_reasoning_effort=self._thread_options.model_reasoning_effort,
|
||||
cancellation_event=turn_options.cancellation_event,
|
||||
network_access_enabled=self._thread_options.network_access_enabled,
|
||||
web_search_enabled=self._thread_options.web_search_enabled,
|
||||
approval_policy=self._thread_options.approval_policy,
|
||||
)
|
||||
generator = self._exec.run(args)
|
||||
try:
|
||||
for line in generator:
|
||||
event = parse_thread_event(line)
|
||||
if isinstance(event, ThreadStartedEvent):
|
||||
self._id = event.thread_id
|
||||
yield event
|
||||
finally:
|
||||
cleanup()
|
||||
|
||||
def run(self, input: Input, turn_options: Optional[TurnOptions] = None) -> TurnResult:
|
||||
generator = self._run_streamed_internal(input, turn_options or TurnOptions())
|
||||
items: List[ThreadItem] = []
|
||||
final_response = ""
|
||||
usage: Optional[Usage] = None
|
||||
turn_failure: Optional[str] = None
|
||||
try:
|
||||
try:
|
||||
for event in generator:
|
||||
if isinstance(event, ItemCompletedEvent):
|
||||
if isinstance(event.item, AgentMessageItem):
|
||||
final_response = event.item.text
|
||||
items.append(event.item)
|
||||
elif isinstance(event, TurnCompletedEvent):
|
||||
usage = event.usage
|
||||
elif isinstance(event, TurnFailedEvent):
|
||||
turn_failure = event.error.message
|
||||
break
|
||||
elif isinstance(event, ThreadErrorEvent):
|
||||
turn_failure = event.message
|
||||
break
|
||||
except CancelledError:
|
||||
raise
|
||||
finally:
|
||||
generator.close()
|
||||
if turn_failure:
|
||||
raise ThreadRunError(turn_failure)
|
||||
return TurnResult(items=items, final_response=final_response, usage=usage)
|
||||
|
||||
|
||||
def normalize_input(input: Input) -> tuple[str, List[str]]:
|
||||
if isinstance(input, str):
|
||||
return input, []
|
||||
prompt_parts: List[str] = []
|
||||
images: List[str] = []
|
||||
for item in input:
|
||||
item_type = item.get("type")
|
||||
if item_type == "text":
|
||||
text = item.get("text")
|
||||
if text is not None:
|
||||
prompt_parts.append(str(text))
|
||||
elif item_type == "local_image":
|
||||
path = item.get("path")
|
||||
if path:
|
||||
images.append(str(path))
|
||||
return "\n\n".join(prompt_parts), images
|
||||
266
sdk/python/src/codex_sdk/types.py
Normal file
266
sdk/python/src/codex_sdk/types.py
Normal file
@@ -0,0 +1,266 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
JsonDict = Dict[str, Any]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Usage:
|
||||
input_tokens: int = 0
|
||||
cached_input_tokens: int = 0
|
||||
output_tokens: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThreadError:
|
||||
message: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class AgentMessageItem:
|
||||
id: str
|
||||
type: str
|
||||
text: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReasoningItem:
|
||||
id: str
|
||||
type: str
|
||||
text: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class CommandExecutionItem:
|
||||
id: str
|
||||
type: str
|
||||
command: str
|
||||
aggregated_output: str
|
||||
status: str
|
||||
exit_code: Optional[int] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileUpdateChange:
|
||||
path: str
|
||||
kind: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class FileChangeItem:
|
||||
id: str
|
||||
type: str
|
||||
changes: List[FileUpdateChange]
|
||||
status: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class McpToolCallResult:
|
||||
content: List[JsonDict]
|
||||
structured_content: Any
|
||||
|
||||
|
||||
@dataclass
|
||||
class McpToolCallError:
|
||||
message: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class McpToolCallItem:
|
||||
id: str
|
||||
type: str
|
||||
server: str
|
||||
tool: str
|
||||
arguments: Any
|
||||
status: str
|
||||
result: Optional[McpToolCallResult] = None
|
||||
error: Optional[McpToolCallError] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class WebSearchItem:
|
||||
id: str
|
||||
type: str
|
||||
query: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TodoItem:
|
||||
text: str
|
||||
completed: bool
|
||||
|
||||
|
||||
@dataclass
|
||||
class TodoListItem:
|
||||
id: str
|
||||
type: str
|
||||
items: List[TodoItem]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ErrorItem:
|
||||
id: str
|
||||
type: str
|
||||
message: str
|
||||
|
||||
|
||||
ThreadItem = Union[
|
||||
AgentMessageItem,
|
||||
ReasoningItem,
|
||||
CommandExecutionItem,
|
||||
FileChangeItem,
|
||||
McpToolCallItem,
|
||||
WebSearchItem,
|
||||
TodoListItem,
|
||||
ErrorItem,
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThreadStartedEvent:
|
||||
type: str
|
||||
thread_id: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurnStartedEvent:
|
||||
type: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurnCompletedEvent:
|
||||
type: str
|
||||
usage: Usage
|
||||
|
||||
|
||||
@dataclass
|
||||
class TurnFailedEvent:
|
||||
type: str
|
||||
error: ThreadError
|
||||
|
||||
|
||||
@dataclass
|
||||
class ItemStartedEvent:
|
||||
type: str
|
||||
item: ThreadItem
|
||||
|
||||
|
||||
@dataclass
|
||||
class ItemUpdatedEvent:
|
||||
type: str
|
||||
item: ThreadItem
|
||||
|
||||
|
||||
@dataclass
|
||||
class ItemCompletedEvent:
|
||||
type: str
|
||||
item: ThreadItem
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThreadErrorEvent:
|
||||
type: str
|
||||
message: str
|
||||
|
||||
|
||||
ThreadEvent = Union[
|
||||
ThreadStartedEvent,
|
||||
TurnStartedEvent,
|
||||
TurnCompletedEvent,
|
||||
TurnFailedEvent,
|
||||
ItemStartedEvent,
|
||||
ItemUpdatedEvent,
|
||||
ItemCompletedEvent,
|
||||
ThreadErrorEvent,
|
||||
]
|
||||
|
||||
|
||||
def parse_thread_event(payload: Union[str, JsonDict]) -> ThreadEvent:
|
||||
data = json.loads(payload) if isinstance(payload, str) else dict(payload)
|
||||
event_type = data.get("type")
|
||||
if event_type == "thread.started":
|
||||
return ThreadStartedEvent(type=event_type, thread_id=str(data["thread_id"]))
|
||||
if event_type == "turn.started":
|
||||
return TurnStartedEvent(type=event_type)
|
||||
if event_type == "turn.completed":
|
||||
usage_data = data.get("usage", {}) or {}
|
||||
usage = Usage(
|
||||
input_tokens=int(usage_data.get("input_tokens", 0) or 0),
|
||||
cached_input_tokens=int(usage_data.get("cached_input_tokens", 0) or 0),
|
||||
output_tokens=int(usage_data.get("output_tokens", 0) or 0),
|
||||
)
|
||||
return TurnCompletedEvent(type=event_type, usage=usage)
|
||||
if event_type == "turn.failed":
|
||||
error = data.get("error") or {}
|
||||
return TurnFailedEvent(type=event_type, error=ThreadError(message=str(error.get("message", ""))))
|
||||
if event_type == "item.started":
|
||||
return ItemStartedEvent(type=event_type, item=parse_thread_item(data["item"]))
|
||||
if event_type == "item.updated":
|
||||
return ItemUpdatedEvent(type=event_type, item=parse_thread_item(data["item"]))
|
||||
if event_type == "item.completed":
|
||||
return ItemCompletedEvent(type=event_type, item=parse_thread_item(data["item"]))
|
||||
if event_type == "error":
|
||||
return ThreadErrorEvent(type=event_type, message=str(data.get("message", "")))
|
||||
raise ValueError(f"Unsupported event type: {event_type}")
|
||||
|
||||
|
||||
def parse_thread_item(data: JsonDict) -> ThreadItem:
|
||||
item_type = data.get("type")
|
||||
item_id = str(data.get("id", ""))
|
||||
if item_type == "agent_message":
|
||||
return AgentMessageItem(id=item_id, type=item_type, text=str(data.get("text", "")))
|
||||
if item_type == "reasoning":
|
||||
return ReasoningItem(id=item_id, type=item_type, text=str(data.get("text", "")))
|
||||
if item_type == "command_execution":
|
||||
return CommandExecutionItem(
|
||||
id=item_id,
|
||||
type=item_type,
|
||||
command=str(data.get("command", "")),
|
||||
aggregated_output=str(data.get("aggregated_output", "")),
|
||||
exit_code=data.get("exit_code"),
|
||||
status=str(data.get("status", "")),
|
||||
)
|
||||
if item_type == "file_change":
|
||||
changes_data = data.get("changes") or []
|
||||
changes = [
|
||||
FileUpdateChange(path=str(change.get("path", "")), kind=str(change.get("kind", "")))
|
||||
for change in changes_data
|
||||
]
|
||||
return FileChangeItem(id=item_id, type=item_type, changes=changes, status=str(data.get("status", "")))
|
||||
if item_type == "mcp_tool_call":
|
||||
result_data = data.get("result")
|
||||
error_data = data.get("error")
|
||||
result = None
|
||||
if isinstance(result_data, dict):
|
||||
result = McpToolCallResult(
|
||||
content=list(result_data.get("content") or []),
|
||||
structured_content=result_data.get("structured_content"),
|
||||
)
|
||||
error = None
|
||||
if isinstance(error_data, dict):
|
||||
error = McpToolCallError(message=str(error_data.get("message", "")))
|
||||
return McpToolCallItem(
|
||||
id=item_id,
|
||||
type=item_type,
|
||||
server=str(data.get("server", "")),
|
||||
tool=str(data.get("tool", "")),
|
||||
arguments=data.get("arguments"),
|
||||
status=str(data.get("status", "")),
|
||||
result=result,
|
||||
error=error,
|
||||
)
|
||||
if item_type == "web_search":
|
||||
return WebSearchItem(id=item_id, type=item_type, query=str(data.get("query", "")))
|
||||
if item_type == "todo_list":
|
||||
todos_data = data.get("items") or []
|
||||
todos = [
|
||||
TodoItem(text=str(todo.get("text", "")), completed=bool(todo.get("completed", False)))
|
||||
for todo in todos_data
|
||||
]
|
||||
return TodoListItem(id=item_id, type=item_type, items=todos)
|
||||
if item_type == "error":
|
||||
return ErrorItem(id=item_id, type=item_type, message=str(data.get("message", "")))
|
||||
raise ValueError(f"Unsupported item type: {item_type}")
|
||||
Reference in New Issue
Block a user