python-codex 0.1.1__py3-none-any.whl → 0.1.3__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.
- pycodex/__init__.py +5 -1
- pycodex/agent.py +39 -41
- pycodex/cli.py +51 -43
- pycodex/collaboration.py +6 -7
- pycodex/compat.py +99 -0
- pycodex/context.py +87 -87
- pycodex/doctor.py +40 -40
- pycodex/model.py +69 -69
- pycodex/portable.py +33 -33
- pycodex/portable_server.py +22 -21
- pycodex/protocol.py +84 -86
- pycodex/runtime.py +36 -35
- pycodex/runtime_services.py +72 -69
- pycodex/tools/agent_tool_schemas.py +0 -2
- pycodex/tools/apply_patch_tool.py +43 -44
- pycodex/tools/base_tool.py +35 -36
- pycodex/tools/close_agent_tool.py +2 -4
- pycodex/tools/code_mode_manager.py +61 -61
- pycodex/tools/exec_command_tool.py +5 -6
- pycodex/tools/exec_runtime.js +3 -3
- pycodex/tools/exec_tool.py +3 -5
- pycodex/tools/grep_files_tool.py +10 -11
- pycodex/tools/list_dir_tool.py +8 -9
- pycodex/tools/read_file_tool.py +13 -14
- pycodex/tools/request_permissions_tool.py +2 -4
- pycodex/tools/request_user_input_tool.py +13 -14
- pycodex/tools/resume_agent_tool.py +2 -4
- pycodex/tools/send_input_tool.py +8 -9
- pycodex/tools/shell_command_tool.py +5 -6
- pycodex/tools/shell_tool.py +5 -6
- pycodex/tools/spawn_agent_tool.py +4 -5
- pycodex/tools/unified_exec_manager.py +79 -61
- pycodex/tools/update_plan_tool.py +4 -5
- pycodex/tools/view_image_tool.py +4 -5
- pycodex/tools/wait_agent_tool.py +2 -4
- pycodex/tools/wait_tool.py +4 -5
- pycodex/tools/web_search_tool.py +1 -3
- pycodex/tools/write_stdin_tool.py +4 -5
- pycodex/utils/dotenv.py +6 -6
- pycodex/utils/get_env.py +57 -34
- pycodex/utils/random_ids.py +1 -2
- pycodex/utils/visualize.py +79 -79
- {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/METADATA +15 -9
- python_codex-0.1.3.dist-info/RECORD +74 -0
- {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/WHEEL +1 -1
- responses_server/__init__.py +17 -0
- responses_server/__main__.py +5 -0
- responses_server/app.py +227 -0
- responses_server/config.py +63 -0
- responses_server/payload_processors.py +86 -0
- responses_server/server.py +63 -0
- responses_server/session_store.py +37 -0
- responses_server/stream_router.py +784 -0
- responses_server/tools/__init__.py +4 -0
- responses_server/tools/custom_adapter.py +235 -0
- responses_server/tools/web_search.py +263 -0
- python_codex-0.1.1.dist-info/RECORD +0 -62
- {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/entry_points.txt +0 -0
- {python_codex-0.1.1.dist-info → python_codex-0.1.3.dist-info}/licenses/LICENSE +0 -0
pycodex/portable_server.py
CHANGED
|
@@ -1,56 +1,57 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
import argparse
|
|
4
3
|
import hashlib
|
|
5
4
|
import json
|
|
6
5
|
import threading
|
|
7
|
-
from http.server import BaseHTTPRequestHandler
|
|
6
|
+
from http.server import BaseHTTPRequestHandler
|
|
8
7
|
from pathlib import Path
|
|
9
8
|
from urllib.parse import unquote, urlparse
|
|
10
9
|
|
|
10
|
+
from .compat import ThreadingHTTPServer
|
|
11
11
|
from .portable import (
|
|
12
12
|
DEFAULT_STORAGE_SERVER,
|
|
13
13
|
HEALTHCHECK_PATH,
|
|
14
14
|
STORAGE_API_PREFIX,
|
|
15
15
|
_call_id_from_payload,
|
|
16
16
|
)
|
|
17
|
+
import typing
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
class CodexStorageServer:
|
|
20
21
|
def __init__(
|
|
21
22
|
self,
|
|
22
|
-
root: str
|
|
23
|
-
host: str = "127.0.0.1",
|
|
24
|
-
port: int = 5577,
|
|
25
|
-
) -> None:
|
|
23
|
+
root: 'typing.Union[str, Path]',
|
|
24
|
+
host: 'str' = "127.0.0.1",
|
|
25
|
+
port: 'int' = 5577,
|
|
26
|
+
) -> 'None':
|
|
26
27
|
self._root = Path(root).resolve()
|
|
27
28
|
self._root.mkdir(parents=True, exist_ok=True)
|
|
28
29
|
self._objects_dir = self._root / "objects"
|
|
29
30
|
self._objects_dir.mkdir(parents=True, exist_ok=True)
|
|
30
31
|
self._server = ThreadingHTTPServer((host, port), self._build_handler())
|
|
31
|
-
self._thread: threading.Thread
|
|
32
|
+
self._thread: 'typing.Union[threading.Thread, None]' = None
|
|
32
33
|
|
|
33
34
|
@property
|
|
34
|
-
def host(self) -> str:
|
|
35
|
+
def host(self) -> 'str':
|
|
35
36
|
return str(self._server.server_address[0])
|
|
36
37
|
|
|
37
38
|
@property
|
|
38
|
-
def port(self) -> int:
|
|
39
|
+
def port(self) -> 'int':
|
|
39
40
|
return int(self._server.server_address[1])
|
|
40
41
|
|
|
41
42
|
@property
|
|
42
|
-
def server_address(self) -> str:
|
|
43
|
+
def server_address(self) -> 'str':
|
|
43
44
|
return f"{self.host}:{self.port}"
|
|
44
45
|
|
|
45
46
|
@property
|
|
46
|
-
def base_url(self) -> str:
|
|
47
|
+
def base_url(self) -> 'str':
|
|
47
48
|
return f"http://{self.server_address}{STORAGE_API_PREFIX}"
|
|
48
49
|
|
|
49
50
|
@property
|
|
50
|
-
def root(self) -> Path:
|
|
51
|
+
def root(self) -> 'Path':
|
|
51
52
|
return self._root
|
|
52
53
|
|
|
53
|
-
def start(self) -> None:
|
|
54
|
+
def start(self) -> 'None':
|
|
54
55
|
if self._thread is not None:
|
|
55
56
|
return
|
|
56
57
|
self._thread = threading.Thread(
|
|
@@ -60,7 +61,7 @@ class CodexStorageServer:
|
|
|
60
61
|
)
|
|
61
62
|
self._thread.start()
|
|
62
63
|
|
|
63
|
-
def stop(self) -> None:
|
|
64
|
+
def stop(self) -> 'None':
|
|
64
65
|
self._server.shutdown()
|
|
65
66
|
self._server.server_close()
|
|
66
67
|
if self._thread is not None:
|
|
@@ -71,7 +72,7 @@ class CodexStorageServer:
|
|
|
71
72
|
server = self
|
|
72
73
|
|
|
73
74
|
class Handler(BaseHTTPRequestHandler):
|
|
74
|
-
def do_GET(self) -> None: # noqa: N802
|
|
75
|
+
def do_GET(self) -> 'None': # noqa: N802
|
|
75
76
|
path = urlparse(self.path).path
|
|
76
77
|
if path == HEALTHCHECK_PATH:
|
|
77
78
|
self._send_json(200, {"ok": True})
|
|
@@ -101,7 +102,7 @@ class CodexStorageServer:
|
|
|
101
102
|
self.end_headers()
|
|
102
103
|
self.wfile.write(payload)
|
|
103
104
|
|
|
104
|
-
def do_POST(self) -> None: # noqa: N802
|
|
105
|
+
def do_POST(self) -> 'None': # noqa: N802
|
|
105
106
|
path = urlparse(self.path).path
|
|
106
107
|
if path != f"{STORAGE_API_PREFIX}/put":
|
|
107
108
|
self._send_json(404, {"error": "not found"})
|
|
@@ -138,10 +139,10 @@ class CodexStorageServer:
|
|
|
138
139
|
},
|
|
139
140
|
)
|
|
140
141
|
|
|
141
|
-
def log_message(self, _format: str, *_args) -> None:
|
|
142
|
+
def log_message(self, _format: 'str', *_args) -> 'None':
|
|
142
143
|
return
|
|
143
144
|
|
|
144
|
-
def _send_json(self, status: int, payload:
|
|
145
|
+
def _send_json(self, status: 'int', payload: 'typing.Dict[str, object]') -> 'None':
|
|
145
146
|
body = json.dumps(payload).encode("utf-8")
|
|
146
147
|
self.send_response(status)
|
|
147
148
|
self.send_header("Content-Type", "application/json")
|
|
@@ -151,11 +152,11 @@ class CodexStorageServer:
|
|
|
151
152
|
|
|
152
153
|
return Handler
|
|
153
154
|
|
|
154
|
-
def _object_path(self, call_id: str) -> Path:
|
|
155
|
+
def _object_path(self, call_id: 'str') -> 'Path':
|
|
155
156
|
return self._objects_dir / f"{call_id}.bin"
|
|
156
157
|
|
|
157
158
|
|
|
158
|
-
def build_parser() -> argparse.ArgumentParser:
|
|
159
|
+
def build_parser() -> 'argparse.ArgumentParser':
|
|
159
160
|
parser = argparse.ArgumentParser(
|
|
160
161
|
prog="python -m pycodex.portable_server",
|
|
161
162
|
description="Run a pycodex remote storage service for --put/--call testing.",
|
|
@@ -179,7 +180,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
179
180
|
return parser
|
|
180
181
|
|
|
181
182
|
|
|
182
|
-
def main(argv:
|
|
183
|
+
def main(argv: 'typing.Union[typing.List[str], None]' = None) -> 'int':
|
|
183
184
|
parser = build_parser()
|
|
184
185
|
args = parser.parse_args(argv)
|
|
185
186
|
server = CodexStorageServer(args.root, host=args.host, port=args.port)
|
pycodex/protocol.py
CHANGED
|
@@ -14,35 +14,35 @@
|
|
|
14
14
|
本文件只定义这些抽象之间传递的数据结构,不包含具体执行逻辑。
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
from __future__ import annotations
|
|
18
|
-
|
|
19
17
|
from copy import deepcopy
|
|
20
18
|
import json
|
|
21
19
|
from dataclasses import dataclass, field
|
|
22
|
-
from typing import Any
|
|
20
|
+
from typing import Any
|
|
21
|
+
from .compat import Literal, TypeAlias
|
|
22
|
+
import typing
|
|
23
23
|
|
|
24
|
-
JSONValue: TypeAlias = Any
|
|
25
|
-
JSONDict: TypeAlias =
|
|
24
|
+
JSONValue: 'TypeAlias' = Any
|
|
25
|
+
JSONDict: 'TypeAlias' = typing.Dict[str, Any]
|
|
26
26
|
|
|
27
27
|
|
|
28
|
-
@dataclass(frozen=True,
|
|
28
|
+
@dataclass(frozen=True, )
|
|
29
29
|
class ToolSpec:
|
|
30
30
|
"""何时:AgentLoop 准备发起一轮模型请求时,随 `Prompt.tools` 一起发送。
|
|
31
31
|
发送方:AgentLoop。
|
|
32
32
|
接收方:ModelClient。
|
|
33
33
|
"""
|
|
34
34
|
|
|
35
|
-
name: str
|
|
36
|
-
description: str
|
|
37
|
-
input_schema: JSONDict
|
|
38
|
-
tool_type: Literal["function", "custom", "web_search"] = "function"
|
|
39
|
-
format: JSONDict
|
|
40
|
-
options: JSONDict
|
|
41
|
-
output_schema: JSONDict
|
|
42
|
-
supports_parallel: bool = True
|
|
43
|
-
raw_payload: JSONDict
|
|
44
|
-
|
|
45
|
-
def serialize(self) -> JSONDict:
|
|
35
|
+
name: 'str'
|
|
36
|
+
description: 'str'
|
|
37
|
+
input_schema: 'typing.Union[JSONDict, None]' = None
|
|
38
|
+
tool_type: 'Literal["function", "custom", "web_search"]' = "function"
|
|
39
|
+
format: 'typing.Union[JSONDict, None]' = None
|
|
40
|
+
options: 'typing.Union[JSONDict, None]' = None
|
|
41
|
+
output_schema: 'typing.Union[JSONDict, None]' = None
|
|
42
|
+
supports_parallel: 'bool' = True
|
|
43
|
+
raw_payload: 'typing.Union[JSONDict, None]' = None
|
|
44
|
+
|
|
45
|
+
def serialize(self) -> 'JSONDict':
|
|
46
46
|
if self.raw_payload is not None:
|
|
47
47
|
return deepcopy(self.raw_payload)
|
|
48
48
|
if self.tool_type == "web_search":
|
|
@@ -76,17 +76,17 @@ class ToolSpec:
|
|
|
76
76
|
return payload
|
|
77
77
|
|
|
78
78
|
|
|
79
|
-
@dataclass(frozen=True,
|
|
79
|
+
@dataclass(frozen=True, )
|
|
80
80
|
class UserMessage:
|
|
81
81
|
"""何时:外部发起一个新的用户 turn 时创建,并写入会话历史。
|
|
82
82
|
发送方:外部调用方创建,AgentLoop 转发。
|
|
83
83
|
接收方:AgentLoop 先接收,随后 ModelClient 在 `Prompt.input` 中看到它。
|
|
84
84
|
"""
|
|
85
85
|
|
|
86
|
-
text: str
|
|
87
|
-
role: Literal["user"] = "user"
|
|
86
|
+
text: 'str'
|
|
87
|
+
role: 'Literal["user"]' = "user"
|
|
88
88
|
|
|
89
|
-
def serialize(self) -> JSONDict:
|
|
89
|
+
def serialize(self) -> 'JSONDict':
|
|
90
90
|
return {
|
|
91
91
|
"type": "message",
|
|
92
92
|
"role": self.role,
|
|
@@ -94,17 +94,17 @@ class UserMessage:
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
@dataclass(frozen=True,
|
|
97
|
+
@dataclass(frozen=True, )
|
|
98
98
|
class AssistantMessage:
|
|
99
99
|
"""何时:模型要直接输出自然语言内容时产生,可作为中间文本或最终回复。
|
|
100
100
|
发送方:ModelClient。
|
|
101
101
|
接收方:AgentLoop。
|
|
102
102
|
"""
|
|
103
103
|
|
|
104
|
-
text: str
|
|
105
|
-
role: Literal["assistant"] = "assistant"
|
|
104
|
+
text: 'str'
|
|
105
|
+
role: 'Literal["assistant"]' = "assistant"
|
|
106
106
|
|
|
107
|
-
def serialize(self) -> JSONDict:
|
|
107
|
+
def serialize(self) -> 'JSONDict':
|
|
108
108
|
return {
|
|
109
109
|
"type": "message",
|
|
110
110
|
"role": self.role,
|
|
@@ -112,18 +112,18 @@ class AssistantMessage:
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
@dataclass(frozen=True,
|
|
115
|
+
@dataclass(frozen=True, )
|
|
116
116
|
class ContextMessage:
|
|
117
117
|
"""何时:ContextManager 为单轮模型请求注入额外上下文时构造。
|
|
118
118
|
发送方:ContextManager。
|
|
119
119
|
接收方:ModelClient。
|
|
120
120
|
"""
|
|
121
121
|
|
|
122
|
-
text: str
|
|
123
|
-
role: Literal["user", "developer"] = "user"
|
|
124
|
-
content_items:
|
|
122
|
+
text: 'typing.Union[str, None]' = None
|
|
123
|
+
role: 'Literal["user", "developer"]' = "user"
|
|
124
|
+
content_items: 'typing.Union[typing.Tuple[JSONDict, ...], None]' = None
|
|
125
125
|
|
|
126
|
-
def serialize(self) -> JSONDict:
|
|
126
|
+
def serialize(self) -> 'JSONDict':
|
|
127
127
|
if self.content_items is not None:
|
|
128
128
|
content = list(self.content_items)
|
|
129
129
|
else:
|
|
@@ -137,20 +137,20 @@ class ContextMessage:
|
|
|
137
137
|
}
|
|
138
138
|
|
|
139
139
|
|
|
140
|
-
@dataclass(frozen=True,
|
|
140
|
+
@dataclass(frozen=True, )
|
|
141
141
|
class ToolCall:
|
|
142
142
|
"""何时:模型决定调用工具而不是只输出文本时产生。
|
|
143
143
|
发送方:ModelClient。
|
|
144
144
|
接收方:AgentLoop,随后由它转给 ToolRegistry 执行。
|
|
145
145
|
"""
|
|
146
146
|
|
|
147
|
-
call_id: str
|
|
148
|
-
name: str
|
|
149
|
-
arguments: JSONValue
|
|
150
|
-
tool_type: Literal["function", "custom"] = "function"
|
|
151
|
-
kind: Literal["tool_call"] = "tool_call"
|
|
147
|
+
call_id: 'str'
|
|
148
|
+
name: 'str'
|
|
149
|
+
arguments: 'JSONValue'
|
|
150
|
+
tool_type: 'Literal["function", "custom"]' = "function"
|
|
151
|
+
kind: 'Literal["tool_call"]' = "tool_call"
|
|
152
152
|
|
|
153
|
-
def serialize(self) -> JSONDict:
|
|
153
|
+
def serialize(self) -> 'JSONDict':
|
|
154
154
|
if self.tool_type == "custom":
|
|
155
155
|
return {
|
|
156
156
|
"type": "custom_tool_call",
|
|
@@ -170,7 +170,7 @@ class ToolCall:
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
|
|
173
|
-
@dataclass(frozen=True,
|
|
173
|
+
@dataclass(frozen=True, )
|
|
174
174
|
class ReasoningItem:
|
|
175
175
|
"""何时:模型在一次 Responses 采样里产出 reasoning item 时产生。
|
|
176
176
|
发送方:ModelClient。
|
|
@@ -178,30 +178,30 @@ class ReasoningItem:
|
|
|
178
178
|
ModelClient。
|
|
179
179
|
"""
|
|
180
180
|
|
|
181
|
-
payload: JSONDict
|
|
182
|
-
kind: Literal["reasoning"] = "reasoning"
|
|
181
|
+
payload: 'JSONDict'
|
|
182
|
+
kind: 'Literal["reasoning"]' = "reasoning"
|
|
183
183
|
|
|
184
|
-
def serialize(self) -> JSONDict:
|
|
184
|
+
def serialize(self) -> 'JSONDict':
|
|
185
185
|
return deepcopy(self.payload)
|
|
186
186
|
|
|
187
187
|
|
|
188
|
-
@dataclass(frozen=True,
|
|
188
|
+
@dataclass(frozen=True, )
|
|
189
189
|
class ToolResult:
|
|
190
190
|
"""何时:某个 `ToolCall` 执行完成后产生,用于喂回下一轮模型调用。
|
|
191
191
|
发送方:ToolRegistry 产出,AgentLoop 追加并转发。
|
|
192
192
|
接收方:AgentLoop 先接收,随后 ModelClient 在下一轮 `Prompt.input` 中看到它。
|
|
193
193
|
"""
|
|
194
194
|
|
|
195
|
-
call_id: str
|
|
196
|
-
name: str
|
|
197
|
-
output: JSONValue
|
|
198
|
-
content_items:
|
|
199
|
-
success: bool
|
|
200
|
-
is_error: bool = False
|
|
201
|
-
tool_type: Literal["function", "custom"] = "function"
|
|
202
|
-
kind: Literal["tool_result"] = "tool_result"
|
|
195
|
+
call_id: 'str'
|
|
196
|
+
name: 'str'
|
|
197
|
+
output: 'JSONValue'
|
|
198
|
+
content_items: 'typing.Union[typing.Tuple[JSONDict, ...], None]' = None
|
|
199
|
+
success: 'typing.Union[bool, None]' = None
|
|
200
|
+
is_error: 'bool' = False
|
|
201
|
+
tool_type: 'Literal["function", "custom"]' = "function"
|
|
202
|
+
kind: 'Literal["tool_result"]' = "tool_result"
|
|
203
203
|
|
|
204
|
-
def output_text(self) -> str:
|
|
204
|
+
def output_text(self) -> 'str':
|
|
205
205
|
if self.content_items is not None:
|
|
206
206
|
text_parts = [
|
|
207
207
|
str(item.get("text", ""))
|
|
@@ -217,8 +217,8 @@ class ToolResult:
|
|
|
217
217
|
return self.output
|
|
218
218
|
return json.dumps(self.output, ensure_ascii=False)
|
|
219
219
|
|
|
220
|
-
def serialize(self) -> JSONDict:
|
|
221
|
-
payload_output: JSONValue
|
|
220
|
+
def serialize(self) -> 'JSONDict':
|
|
221
|
+
payload_output: 'JSONValue'
|
|
222
222
|
if self.content_items is not None:
|
|
223
223
|
payload_output = list(self.content_items)
|
|
224
224
|
elif isinstance(self.output, str):
|
|
@@ -247,86 +247,84 @@ class ToolResult:
|
|
|
247
247
|
return payload
|
|
248
248
|
|
|
249
249
|
|
|
250
|
-
ConversationItem: TypeAlias =
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
ModelOutputItem: TypeAlias = AssistantMessage | ToolCall | ReasoningItem
|
|
254
|
-
Operation: TypeAlias = "UserTurnOp | ShutdownOp"
|
|
250
|
+
ConversationItem: 'TypeAlias' = typing.Union[typing.Union[typing.Union[typing.Union[typing.Union[UserMessage, AssistantMessage], ContextMessage], ToolCall], ReasoningItem], ToolResult]
|
|
251
|
+
ModelOutputItem: 'TypeAlias' = typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem]
|
|
252
|
+
Operation: 'TypeAlias' = "UserTurnOp | ShutdownOp"
|
|
255
253
|
|
|
256
254
|
|
|
257
|
-
@dataclass(frozen=True,
|
|
255
|
+
@dataclass(frozen=True, )
|
|
258
256
|
class Prompt:
|
|
259
257
|
"""何时:AgentLoop 每发起一轮模型采样前构造。
|
|
260
258
|
发送方:AgentLoop。
|
|
261
259
|
接收方:ModelClient。
|
|
262
260
|
"""
|
|
263
261
|
|
|
264
|
-
input:
|
|
265
|
-
tools:
|
|
266
|
-
parallel_tool_calls: bool = True
|
|
267
|
-
base_instructions: str
|
|
268
|
-
turn_id: str
|
|
269
|
-
turn_metadata: JSONDict
|
|
262
|
+
input: 'typing.List[ConversationItem]'
|
|
263
|
+
tools: 'typing.List[ToolSpec]'
|
|
264
|
+
parallel_tool_calls: 'bool' = True
|
|
265
|
+
base_instructions: 'typing.Union[str, None]' = None
|
|
266
|
+
turn_id: 'typing.Union[str, None]' = None
|
|
267
|
+
turn_metadata: 'typing.Union[JSONDict, None]' = None
|
|
270
268
|
|
|
271
269
|
|
|
272
|
-
@dataclass(frozen=True,
|
|
270
|
+
@dataclass(frozen=True, )
|
|
273
271
|
class ModelResponse:
|
|
274
272
|
"""何时:ModelClient 完成一轮 `Prompt` 处理后返回。
|
|
275
273
|
发送方:ModelClient。
|
|
276
274
|
接收方:AgentLoop。
|
|
277
275
|
"""
|
|
278
276
|
|
|
279
|
-
items:
|
|
277
|
+
items: 'typing.List[ModelOutputItem]'
|
|
280
278
|
|
|
281
279
|
|
|
282
|
-
@dataclass(frozen=True,
|
|
280
|
+
@dataclass(frozen=True, )
|
|
283
281
|
class ModelStreamEvent:
|
|
284
282
|
"""何时:ModelClient 处理 `Prompt` 的过程中有流式中间结果时产生。
|
|
285
283
|
发送方:ModelClient。
|
|
286
284
|
接收方:AgentLoop。
|
|
287
285
|
"""
|
|
288
286
|
|
|
289
|
-
kind: str
|
|
290
|
-
payload: JSONDict = field(default_factory=dict)
|
|
287
|
+
kind: 'str'
|
|
288
|
+
payload: 'JSONDict' = field(default_factory=dict)
|
|
291
289
|
|
|
292
290
|
|
|
293
|
-
@dataclass(frozen=True,
|
|
291
|
+
@dataclass(frozen=True, )
|
|
294
292
|
class TurnResult:
|
|
295
293
|
"""何时:一个 turn 已经收敛,AgentLoop 决定结束本轮时返回。
|
|
296
294
|
发送方:AgentLoop。
|
|
297
295
|
接收方:外部调用方。
|
|
298
296
|
"""
|
|
299
297
|
|
|
300
|
-
turn_id: str
|
|
301
|
-
output_text: str
|
|
302
|
-
iterations: int
|
|
303
|
-
response_items:
|
|
304
|
-
history:
|
|
298
|
+
turn_id: 'str'
|
|
299
|
+
output_text: 'typing.Union[str, None]'
|
|
300
|
+
iterations: 'int'
|
|
301
|
+
response_items: 'typing.Tuple[ModelOutputItem, ...]'
|
|
302
|
+
history: 'typing.Tuple[ConversationItem, ...]'
|
|
305
303
|
|
|
306
304
|
|
|
307
|
-
@dataclass(frozen=True,
|
|
305
|
+
@dataclass(frozen=True, )
|
|
308
306
|
class AgentEvent:
|
|
309
307
|
"""何时:主循环运行过程中发生阶段性事件时发出,例如模型调用、工具开始/结束。
|
|
310
308
|
发送方:AgentLoop。
|
|
311
309
|
接收方:可选的事件观察者 / 回调。
|
|
312
310
|
"""
|
|
313
311
|
|
|
314
|
-
kind: str
|
|
315
|
-
turn_id: str
|
|
316
|
-
payload:
|
|
312
|
+
kind: 'str'
|
|
313
|
+
turn_id: 'str'
|
|
314
|
+
payload: 'typing.Dict[str, object]' = field(default_factory=dict)
|
|
317
315
|
|
|
318
316
|
|
|
319
|
-
@dataclass(frozen=True,
|
|
317
|
+
@dataclass(frozen=True, )
|
|
320
318
|
class UserTurnOp:
|
|
321
319
|
"""何时:运行时要提交一个新的用户请求时创建。
|
|
322
320
|
发送方:外部运行时调用方。
|
|
323
321
|
接收方:AgentRuntime。
|
|
324
322
|
"""
|
|
325
323
|
|
|
326
|
-
texts:
|
|
324
|
+
texts: 'typing.List[str]'
|
|
327
325
|
|
|
328
326
|
|
|
329
|
-
@dataclass(frozen=True,
|
|
327
|
+
@dataclass(frozen=True, )
|
|
330
328
|
class ShutdownOp:
|
|
331
329
|
"""何时:运行时要停止外层提交循环时创建。
|
|
332
330
|
发送方:外部运行时调用方。
|
|
@@ -336,12 +334,12 @@ class ShutdownOp:
|
|
|
336
334
|
pass
|
|
337
335
|
|
|
338
336
|
|
|
339
|
-
@dataclass(frozen=True,
|
|
337
|
+
@dataclass(frozen=True, )
|
|
340
338
|
class Submission:
|
|
341
339
|
"""何时:任意运行时操作要进入 AgentRuntime 队列时创建。
|
|
342
340
|
发送方:外部运行时调用方。
|
|
343
341
|
接收方:AgentRuntime 的提交队列 / 外层循环。
|
|
344
342
|
"""
|
|
345
343
|
|
|
346
|
-
id: str
|
|
347
|
-
op: Operation
|
|
344
|
+
id: 'str'
|
|
345
|
+
op: 'Operation'
|
pycodex/runtime.py
CHANGED
|
@@ -1,44 +1,45 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
1
|
|
|
3
2
|
import asyncio
|
|
4
3
|
from collections import deque
|
|
5
4
|
from dataclasses import dataclass
|
|
6
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
7
6
|
|
|
8
7
|
from .agent import AgentLoop, EventHandler, NOOP_EVENT_HANDLER, TurnInterrupted
|
|
8
|
+
from .compat import Literal
|
|
9
9
|
from .protocol import AgentEvent, Operation, ShutdownOp, Submission, TurnResult, UserTurnOp
|
|
10
10
|
from .utils import uuid7_string
|
|
11
|
+
import typing
|
|
11
12
|
|
|
12
13
|
if TYPE_CHECKING:
|
|
13
14
|
from .runtime_services import RuntimeEnvironment
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
@dataclass
|
|
17
|
+
@dataclass
|
|
17
18
|
class _QueuedSubmission:
|
|
18
|
-
submission: Submission
|
|
19
|
-
turn_id: str
|
|
20
|
-
futures:
|
|
19
|
+
submission: 'Submission'
|
|
20
|
+
turn_id: 'str'
|
|
21
|
+
futures: 'typing.List[asyncio.Future[typing.Union[TurnResult, None]]]'
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class AgentRuntime:
|
|
24
25
|
"""Thin outer queue that mirrors the Rust `submission_loop` shape."""
|
|
25
26
|
|
|
26
|
-
def __init__(self, agent_loop: AgentLoop, runtime_environment: RuntimeEnvironment
|
|
27
|
+
def __init__(self, agent_loop: 'AgentLoop', runtime_environment: 'typing.Union[RuntimeEnvironment, None]' = None) -> 'None':
|
|
27
28
|
self._agent_loop = agent_loop
|
|
28
29
|
self.runtime_environment = runtime_environment
|
|
29
|
-
self._enqueue_queue: deque[_QueuedSubmission] = deque()
|
|
30
|
-
self._steer_queue: deque[_QueuedSubmission] = deque()
|
|
30
|
+
self._enqueue_queue: 'deque[_QueuedSubmission]' = deque()
|
|
31
|
+
self._steer_queue: 'deque[_QueuedSubmission]' = deque()
|
|
31
32
|
self._queue_lock = asyncio.Lock()
|
|
32
33
|
self._queue_event = asyncio.Event()
|
|
33
|
-
self._current_submission: _QueuedSubmission
|
|
34
|
-
self._current_task: asyncio.Task[TurnResult]
|
|
34
|
+
self._current_submission: 'typing.Union[_QueuedSubmission, None]' = None
|
|
35
|
+
self._current_task: 'typing.Union[asyncio.Task[TurnResult], None]' = None
|
|
35
36
|
self._event_handler = NOOP_EVENT_HANDLER
|
|
36
37
|
self._agent_loop.set_event_handler(self._handle_agent_event)
|
|
37
38
|
|
|
38
|
-
def set_event_handler(self, event_handler: EventHandler = NOOP_EVENT_HANDLER) -> None:
|
|
39
|
+
def set_event_handler(self, event_handler: 'EventHandler' = NOOP_EVENT_HANDLER) -> 'None':
|
|
39
40
|
self._event_handler = event_handler
|
|
40
41
|
|
|
41
|
-
async def submit_user_turn(self, text: str) -> TurnResult:
|
|
42
|
+
async def submit_user_turn(self, text: 'str') -> 'TurnResult':
|
|
42
43
|
_submission_id, future = await self.enqueue_user_turn(text, queue="enqueue")
|
|
43
44
|
result = await future
|
|
44
45
|
assert result is not None
|
|
@@ -46,19 +47,19 @@ class AgentRuntime:
|
|
|
46
47
|
|
|
47
48
|
async def enqueue_user_turn(
|
|
48
49
|
self,
|
|
49
|
-
text: str,
|
|
50
|
-
queue: Literal["enqueue", "steer"] = "enqueue",
|
|
51
|
-
) ->
|
|
52
|
-
future: asyncio.Future[TurnResult
|
|
50
|
+
text: 'str',
|
|
51
|
+
queue: 'Literal["enqueue", "steer"]' = "enqueue",
|
|
52
|
+
) -> 'typing.Tuple[str, asyncio.Future[typing.Union[TurnResult, None]]]':
|
|
53
|
+
future: 'asyncio.Future[typing.Union[TurnResult, None]]' = asyncio.get_running_loop().create_future()
|
|
53
54
|
return await self._enqueue_user_turn_to_queue(
|
|
54
55
|
text,
|
|
55
56
|
future,
|
|
56
57
|
queue=queue,
|
|
57
58
|
)
|
|
58
59
|
|
|
59
|
-
async def shutdown(self) -> None:
|
|
60
|
+
async def shutdown(self) -> 'None':
|
|
60
61
|
submission = Submission(id=uuid7_string(), op=ShutdownOp())
|
|
61
|
-
future: asyncio.Future[TurnResult
|
|
62
|
+
future: 'asyncio.Future[typing.Union[TurnResult, None]]' = asyncio.get_running_loop().create_future()
|
|
62
63
|
self._enqueue_queue.append(
|
|
63
64
|
_QueuedSubmission(
|
|
64
65
|
submission=submission,
|
|
@@ -69,7 +70,7 @@ class AgentRuntime:
|
|
|
69
70
|
self._queue_event.set()
|
|
70
71
|
await future
|
|
71
72
|
|
|
72
|
-
async def run_forever(self) -> None:
|
|
73
|
+
async def run_forever(self) -> 'None':
|
|
73
74
|
while True:
|
|
74
75
|
queued = await self._next_submission()
|
|
75
76
|
submission = queued.submission
|
|
@@ -114,7 +115,7 @@ class AgentRuntime:
|
|
|
114
115
|
self._current_submission = None
|
|
115
116
|
|
|
116
117
|
@staticmethod
|
|
117
|
-
def operation_name(op: Operation) -> str:
|
|
118
|
+
def operation_name(op: 'Operation') -> 'str':
|
|
118
119
|
if isinstance(op, UserTurnOp):
|
|
119
120
|
return "user_turn"
|
|
120
121
|
if isinstance(op, ShutdownOp):
|
|
@@ -123,10 +124,10 @@ class AgentRuntime:
|
|
|
123
124
|
|
|
124
125
|
async def _enqueue_user_turn_to_queue(
|
|
125
126
|
self,
|
|
126
|
-
text: str,
|
|
127
|
-
future: asyncio.Future[TurnResult
|
|
128
|
-
queue: Literal["enqueue", "steer"],
|
|
129
|
-
) ->
|
|
127
|
+
text: 'str',
|
|
128
|
+
future: 'asyncio.Future[typing.Union[TurnResult, None]]',
|
|
129
|
+
queue: 'Literal["enqueue", "steer"]',
|
|
130
|
+
) -> 'typing.Tuple[str, asyncio.Future[typing.Union[TurnResult, None]]]':
|
|
130
131
|
if queue == "steer" and self._has_active_turn():
|
|
131
132
|
self._agent_loop.interrupt_asap = True
|
|
132
133
|
|
|
@@ -154,10 +155,10 @@ class AgentRuntime:
|
|
|
154
155
|
self._queue_event.set()
|
|
155
156
|
return submission.id, future
|
|
156
157
|
|
|
157
|
-
async def _next_submission(self) -> _QueuedSubmission:
|
|
158
|
+
async def _next_submission(self) -> '_QueuedSubmission':
|
|
158
159
|
while True:
|
|
159
160
|
async with self._queue_lock:
|
|
160
|
-
queued: _QueuedSubmission
|
|
161
|
+
queued: 'typing.Union[_QueuedSubmission, None]' = None
|
|
161
162
|
if self._steer_queue:
|
|
162
163
|
queued = self._steer_queue.popleft()
|
|
163
164
|
elif self._enqueue_queue:
|
|
@@ -171,27 +172,27 @@ class AgentRuntime:
|
|
|
171
172
|
|
|
172
173
|
@staticmethod
|
|
173
174
|
def _finish_submission_result(
|
|
174
|
-
queued: _QueuedSubmission,
|
|
175
|
-
result: TurnResult
|
|
176
|
-
) -> None:
|
|
175
|
+
queued: '_QueuedSubmission',
|
|
176
|
+
result: 'typing.Union[TurnResult, None]',
|
|
177
|
+
) -> 'None':
|
|
177
178
|
for future in queued.futures:
|
|
178
179
|
if not future.done():
|
|
179
180
|
future.set_result(result)
|
|
180
181
|
|
|
181
182
|
@staticmethod
|
|
182
183
|
def _finish_submission_exception(
|
|
183
|
-
queued: _QueuedSubmission,
|
|
184
|
-
exc: Exception,
|
|
185
|
-
) -> None:
|
|
184
|
+
queued: '_QueuedSubmission',
|
|
185
|
+
exc: 'Exception',
|
|
186
|
+
) -> 'None':
|
|
186
187
|
for future in queued.futures:
|
|
187
188
|
if not future.done():
|
|
188
189
|
future.set_exception(exc)
|
|
189
190
|
|
|
190
|
-
def _has_active_turn(self) -> bool:
|
|
191
|
+
def _has_active_turn(self) -> 'bool':
|
|
191
192
|
current_task = self._current_task
|
|
192
193
|
return current_task is not None and not current_task.done()
|
|
193
194
|
|
|
194
|
-
def _handle_agent_event(self, event: AgentEvent) -> None:
|
|
195
|
+
def _handle_agent_event(self, event: 'AgentEvent') -> 'None':
|
|
195
196
|
queued = self._current_submission
|
|
196
197
|
if queued is None:
|
|
197
198
|
self._event_handler(event)
|