python-codex 0.2.7__py3-none-any.whl → 0.3.0__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 (84) hide show
  1. pycodex/__init__.py +14 -14
  2. pycodex/agent.py +465 -499
  3. pycodex/bootstrap.py +417 -0
  4. pycodex/cli.py +236 -510
  5. pycodex/compat.py +19 -5
  6. pycodex/context.py +222 -212
  7. pycodex/doctor.py +52 -48
  8. pycodex/events.py +857 -0
  9. pycodex/feishu_card.py +217 -163
  10. pycodex/feishu_link.py +43 -83
  11. pycodex/model.py +324 -253
  12. pycodex/model_metadata.py +19 -7
  13. pycodex/portable.py +76 -45
  14. pycodex/portable_server.py +32 -24
  15. pycodex/prompts/models.json +245 -983
  16. pycodex/protocol.py +177 -137
  17. pycodex/runtime.py +579 -176
  18. pycodex/runtime_services.py +204 -157
  19. pycodex/tools/__init__.py +1 -1
  20. pycodex/tools/apply_patch_tool.py +69 -48
  21. pycodex/tools/base_tool.py +89 -42
  22. pycodex/tools/clock_tool.py +58 -25
  23. pycodex/tools/close_agent_tool.py +2 -2
  24. pycodex/tools/code_mode_manager.py +77 -64
  25. pycodex/tools/exec_command_tool.py +26 -11
  26. pycodex/tools/exec_tool.py +4 -4
  27. pycodex/tools/grep_files_tool.py +12 -10
  28. pycodex/tools/ipython_tool.py +10 -13
  29. pycodex/tools/list_dir_tool.py +13 -9
  30. pycodex/tools/read_file_tool.py +29 -17
  31. pycodex/tools/request_permissions_tool.py +15 -5
  32. pycodex/tools/request_user_input_tool.py +13 -104
  33. pycodex/tools/resume_agent_tool.py +2 -2
  34. pycodex/tools/send_input_tool.py +11 -8
  35. pycodex/tools/shell_command_tool.py +7 -5
  36. pycodex/tools/shell_tool.py +7 -5
  37. pycodex/tools/spawn_agent_tool.py +7 -4
  38. pycodex/tools/unified_exec_manager.py +102 -69
  39. pycodex/tools/update_plan_tool.py +8 -5
  40. pycodex/tools/view_image_tool.py +7 -5
  41. pycodex/tools/wait_agent_tool.py +27 -4
  42. pycodex/tools/wait_tool.py +5 -4
  43. pycodex/tools/web_search_tool.py +4 -2
  44. pycodex/tools/write_stdin_tool.py +12 -11
  45. pycodex/utils/__init__.py +2 -17
  46. pycodex/utils/compactor.py +41 -72
  47. pycodex/utils/debug.py +2 -2
  48. pycodex/utils/dotenv.py +6 -7
  49. pycodex/utils/event_helpers.py +190 -0
  50. pycodex/utils/get_env.py +27 -70
  51. pycodex/{image_utils.py → utils/image_utils.py} +8 -11
  52. pycodex/utils/random_ids.py +1 -2
  53. pycodex/utils/session_persist.py +217 -163
  54. pycodex/utils/truncation.py +21 -45
  55. python_codex-0.3.0.dist-info/METADATA +704 -0
  56. python_codex-0.3.0.dist-info/RECORD +90 -0
  57. responses_server/__init__.py +1 -5
  58. responses_server/__main__.py +0 -1
  59. responses_server/app.py +36 -31
  60. responses_server/config.py +23 -23
  61. responses_server/messages_api.py +51 -53
  62. responses_server/payload_processors.py +25 -20
  63. responses_server/server.py +11 -11
  64. responses_server/session_store.py +14 -11
  65. responses_server/stream_router.py +101 -98
  66. responses_server/tools/custom_adapter.py +17 -16
  67. responses_server/tools/web_search.py +39 -36
  68. responses_server/trajectory_dump.py +36 -14
  69. workspace_server/__main__.py +0 -1
  70. workspace_server/app.py +461 -375
  71. workspace_server/workspace.html +852 -228
  72. workspace_server/workspaces.html +94 -95
  73. workspace_server/workspaces.py +137 -79
  74. pycodex/collaboration.py +0 -20
  75. pycodex/interactive_session.py +0 -415
  76. pycodex/prompts/collaboration_default.md +0 -11
  77. pycodex/prompts/collaboration_plan.md +0 -128
  78. pycodex/utils/toolcall_visualize.py +0 -713
  79. pycodex/utils/visualize.py +0 -560
  80. python_codex-0.2.7.dist-info/METADATA +0 -455
  81. python_codex-0.2.7.dist-info/RECORD +0 -93
  82. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/WHEEL +0 -0
  83. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/entry_points.txt +0 -0
  84. {python_codex-0.2.7.dist-info → python_codex-0.3.0.dist-info}/licenses/LICENSE +0 -0
pycodex/protocol.py CHANGED
@@ -3,45 +3,47 @@
3
3
  核心抽象:
4
4
  - Agent:内层 turn 主循环,负责维护历史、调用模型、执行工具,并在
5
5
  `ToolCall -> ToolResult -> 下一轮模型调用` 之间反复闭环,直到得到稳定回复。
6
- - AgentRuntime:外层提交队列,负责按顺序接收 `Submission`,驱动
7
- `UserTurnOp` / `ShutdownOp` 这类运行时操作。
6
+ - AgentRuntime:外层调度与前端接口,负责按顺序处理用户请求;
7
+ 队列生命周期不作为请求入队。
8
8
  - ContextManager:负责把基础指令、AGENTS.md、环境信息等上下文拼成每轮模型看到的
9
9
  `Prompt` 前缀,但这些注入内容不会写回长期会话历史。
10
10
  - ModelClient:模型后端抽象,接收 `Prompt`,返回 `ModelResponse`。
11
- - ModelStreamEvent:模型在一次 `Prompt` 处理中途产生的流式事件,例如文本增量。
12
11
  - ToolRegistry:工具执行抽象,接收 `ToolCall`,产出 `ToolResult`。
13
12
 
14
13
  本文件只定义这些抽象之间传递的数据结构,不包含具体执行逻辑。
15
14
  """
16
15
 
17
- from copy import deepcopy
18
16
  import json
17
+ import typing
18
+ from copy import deepcopy
19
19
  from dataclasses import dataclass, field
20
20
  from typing import Any
21
+
21
22
  from .compat import Literal, TypeAlias
22
- import typing
23
23
 
24
- JSONValue: 'TypeAlias' = Any
25
- JSONDict: 'TypeAlias' = typing.Dict[str, Any]
24
+ JSONValue: "TypeAlias" = Any
25
+ JSONDict: "TypeAlias" = typing.Dict[str, Any]
26
26
 
27
27
 
28
- @dataclass(frozen=True, )
28
+ @dataclass(
29
+ frozen=True,
30
+ )
29
31
  class ToolSpec:
30
32
  """何时:Agent 准备发起一轮模型请求时,随 `Prompt.tools` 一起发送。
31
33
  发送方:Agent。
32
34
  接收方:ModelClient。
33
35
  """
34
36
 
35
- name: 'str'
36
- description: 'str'
37
- input_schema: 'typing.Union[JSONDict, None]' = None
37
+ name: "str"
38
+ description: "str"
39
+ input_schema: "typing.Union[JSONDict, None]" = None
38
40
  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
41
+ format: "typing.Union[JSONDict, None]" = None
42
+ options: "typing.Union[JSONDict, None]" = None
43
+ output_schema: "typing.Union[JSONDict, None]" = None
44
+ supports_parallel: "bool" = True
43
45
 
44
- def serialize(self) -> 'JSONDict':
46
+ def serialize(self) -> "JSONDict":
45
47
  if self.tool_type == "web_search":
46
48
  payload = {"type": "web_search"}
47
49
  if self.options is not None:
@@ -71,101 +73,161 @@ class ToolSpec:
71
73
  return payload
72
74
 
73
75
 
74
- @dataclass(frozen=True, )
76
+ @dataclass(
77
+ frozen=True,
78
+ )
75
79
  class UserMessage:
76
80
  """何时:外部发起一个新的用户 turn 时创建,并写入会话历史。
77
81
  发送方:外部调用方创建,Agent 转发。
78
82
  接收方:Agent 先接收,随后 ModelClient 在 `Prompt.input` 中看到它。
79
83
  """
80
84
 
81
- text: 'str'
85
+ text: "str"
82
86
  role: 'Literal["user"]' = "user"
87
+ id: "typing.Union[str, None]" = None
83
88
 
84
- def serialize(self) -> 'JSONDict':
85
- return {
89
+ def serialize(self) -> "JSONDict":
90
+ payload = {
86
91
  "type": "message",
87
92
  "role": self.role,
88
93
  "content": [{"type": "input_text", "text": self.text}],
89
94
  }
95
+ if self.id is not None:
96
+ payload["id"] = self.id
97
+ return payload
90
98
 
91
99
 
92
- @dataclass(frozen=True, )
100
+ @dataclass(
101
+ frozen=True,
102
+ )
93
103
  class AssistantMessage:
94
104
  """何时:模型要直接输出自然语言内容时产生,可作为中间文本或最终回复。
95
105
  发送方:ModelClient。
96
106
  接收方:Agent。
97
107
  """
98
108
 
99
- text: 'str'
109
+ text: "str"
100
110
  role: 'Literal["assistant"]' = "assistant"
111
+ id: "typing.Union[str, None]" = None
112
+ phase: "typing.Union[str, None]" = None
113
+ content_items: "typing.Union[typing.Tuple[JSONDict, ...], None]" = None
114
+
115
+ @classmethod
116
+ def from_response_item(cls, payload: "JSONDict") -> "AssistantMessage":
117
+ content = tuple(
118
+ {"type": part["type"], "text": str(part.get("text", ""))}
119
+ for part in payload.get("content", [])
120
+ if part.get("type") == "output_text"
121
+ )
122
+ return cls(
123
+ text="".join(part["text"] for part in content),
124
+ id=payload.get("id"),
125
+ phase=payload.get("phase"),
126
+ content_items=content if len(content) != 1 else None,
127
+ )
101
128
 
102
- def serialize(self) -> 'JSONDict':
103
- return {
129
+ def serialize(self) -> "JSONDict":
130
+ payload = {
104
131
  "type": "message",
105
132
  "role": self.role,
106
- "content": [{"type": "output_text", "text": self.text}],
133
+ "content": (
134
+ deepcopy(list(self.content_items))
135
+ if self.content_items is not None
136
+ else [{"type": "output_text", "text": self.text}]
137
+ ),
107
138
  }
139
+ if self.id is not None:
140
+ payload["id"] = self.id
141
+ if self.phase is not None:
142
+ payload["phase"] = self.phase
143
+ return payload
108
144
 
109
145
 
110
- @dataclass(frozen=True, )
146
+ @dataclass(
147
+ frozen=True,
148
+ )
111
149
  class ContextMessage:
112
- """何时:ContextManager 为单轮模型请求注入额外上下文时构造。
113
- 发送方:ContextManager。
150
+ """何时:注入额外上下文或用 compact 摘要替换历史时构造。
151
+ 发送方:ContextManager 或 compactor。
114
152
  接收方:ModelClient。
115
153
  """
116
154
 
117
- text: 'typing.Union[str, None]' = None
155
+ text: "typing.Union[str, None]" = None
118
156
  role: 'Literal["user", "developer"]' = "user"
119
- content_items: 'typing.Union[typing.Tuple[JSONDict, ...], None]' = None
157
+ content_items: "typing.Union[typing.Tuple[JSONDict, ...], None]" = None
158
+ id: "typing.Union[str, None]" = None
120
159
 
121
- def serialize(self) -> 'JSONDict':
160
+ def serialize(self) -> "JSONDict":
122
161
  if self.content_items is not None:
123
162
  content = list(self.content_items)
124
163
  else:
125
164
  if self.text is None:
126
165
  raise ValueError("ContextMessage requires `text` or `content_items`")
127
166
  content = [{"type": "input_text", "text": self.text}]
128
- return {
167
+ payload = {
129
168
  "type": "message",
130
169
  "role": self.role,
131
170
  "content": content,
132
171
  }
172
+ if self.id is not None:
173
+ payload["id"] = self.id
174
+ return payload
133
175
 
134
176
 
135
- @dataclass(frozen=True, )
177
+ @dataclass(
178
+ frozen=True,
179
+ )
136
180
  class ToolCall:
137
181
  """何时:模型决定调用工具而不是只输出文本时产生。
138
182
  发送方:ModelClient。
139
183
  接收方:Agent,随后由它转给 ToolRegistry 执行。
140
184
  """
141
185
 
142
- call_id: 'str'
143
- name: 'str'
144
- arguments: 'JSONValue'
186
+ call_id: "str"
187
+ name: "str"
188
+ arguments: "JSONValue"
145
189
  tool_type: 'Literal["function", "custom"]' = "function"
146
190
  kind: 'Literal["tool_call"]' = "tool_call"
191
+ id: "typing.Union[str, None]" = None
192
+ raw_arguments: "typing.Union[str, None]" = None
193
+ namespace: "typing.Union[str, None]" = None
194
+ status: "typing.Union[str, None]" = None
147
195
 
148
- def serialize(self) -> 'JSONDict':
196
+ def serialize(self) -> "JSONDict":
149
197
  if self.tool_type == "custom":
150
- return {
198
+ payload = {
151
199
  "type": "custom_tool_call",
152
200
  "name": self.name,
153
201
  "input": str(self.arguments),
154
202
  "call_id": self.call_id,
155
203
  }
156
- return {
157
- "type": "function_call",
158
- "name": self.name,
159
- "arguments": json.dumps(
160
- self.arguments,
161
- ensure_ascii=False,
162
- separators=(",", ":"),
163
- ),
164
- "call_id": self.call_id,
165
- }
204
+ else:
205
+ payload = {
206
+ "type": "function_call",
207
+ "name": self.name,
208
+ "arguments": (
209
+ self.raw_arguments
210
+ if self.raw_arguments is not None
211
+ else json.dumps(
212
+ self.arguments,
213
+ ensure_ascii=False,
214
+ separators=(",", ":"),
215
+ )
216
+ ),
217
+ "call_id": self.call_id,
218
+ }
219
+ if self.id is not None:
220
+ payload["id"] = self.id
221
+ if self.namespace is not None:
222
+ payload["namespace"] = self.namespace
223
+ if self.tool_type == "custom" and self.status is not None:
224
+ payload["status"] = self.status
225
+ return payload
166
226
 
167
227
 
168
- @dataclass(frozen=True, )
228
+ @dataclass(
229
+ frozen=True,
230
+ )
169
231
  class ReasoningItem:
170
232
  """何时:模型在一次 Responses 采样里产出 reasoning item 时产生。
171
233
  发送方:ModelClient。
@@ -173,30 +235,41 @@ class ReasoningItem:
173
235
  ModelClient。
174
236
  """
175
237
 
176
- payload: 'JSONDict'
238
+ payload: "JSONDict"
177
239
  kind: 'Literal["reasoning"]' = "reasoning"
178
240
 
179
- def serialize(self) -> 'JSONDict':
180
- return deepcopy(self.payload)
241
+ def serialize(self) -> "JSONDict":
242
+ payload = deepcopy(self.payload)
243
+ payload.setdefault("content", None)
244
+ payload.setdefault("encrypted_content", None)
245
+ if payload["content"] is not None and not any(
246
+ item.get("type") == "reasoning_text" for item in payload["content"]
247
+ ):
248
+ del payload["content"]
249
+ return payload
181
250
 
182
251
 
183
- @dataclass(frozen=True, )
252
+ @dataclass(
253
+ frozen=True,
254
+ )
184
255
  class ToolResult:
185
256
  """何时:某个 `ToolCall` 执行完成后产生,用于喂回下一轮模型调用。
186
257
  发送方:ToolRegistry 产出,Agent 追加并转发。
187
258
  接收方:Agent 先接收,随后 ModelClient 在下一轮 `Prompt.input` 中看到它。
188
259
  """
189
260
 
190
- call_id: 'str'
191
- name: 'str'
192
- output: 'JSONValue'
193
- content_items: 'typing.Union[typing.Tuple[JSONDict, ...], None]' = None
194
- success: 'typing.Union[bool, None]' = None
195
- is_error: 'bool' = False
261
+ call_id: "str"
262
+ name: "str"
263
+ output: "JSONValue"
264
+ content_items: "typing.Union[typing.Tuple[JSONDict, ...], None]" = None
265
+ # Local execution metadata; not part of a Responses API input item.
266
+ success: "typing.Union[bool, None]" = None
267
+ is_error: "bool" = False
196
268
  tool_type: 'Literal["function", "custom"]' = "function"
197
269
  kind: 'Literal["tool_result"]' = "tool_result"
270
+ id: "typing.Union[str, None]" = None
198
271
 
199
- def output_text(self) -> 'str':
272
+ def output_text(self) -> "str":
200
273
  if self.content_items is not None:
201
274
  text_parts = [
202
275
  str(item.get("text", ""))
@@ -212,8 +285,8 @@ class ToolResult:
212
285
  return self.output
213
286
  return json.dumps(self.output, ensure_ascii=False)
214
287
 
215
- def serialize(self) -> 'JSONDict':
216
- payload_output: 'JSONValue'
288
+ def serialize(self) -> "JSONDict":
289
+ payload_output: "JSONValue"
217
290
  if self.content_items is not None:
218
291
  payload_output = list(self.content_items)
219
292
  elif isinstance(self.output, str):
@@ -235,106 +308,73 @@ class ToolResult:
235
308
  "call_id": self.call_id,
236
309
  "output": payload_output,
237
310
  }
238
- if self.success is not None:
239
- payload["success"] = self.success
311
+ if self.id is not None:
312
+ payload["id"] = self.id
240
313
  if self.tool_type == "custom":
241
314
  payload["name"] = self.name
242
315
  return payload
243
316
 
244
317
 
245
- ConversationItem: 'TypeAlias' = typing.Union[typing.Union[typing.Union[typing.Union[typing.Union[UserMessage, AssistantMessage], ContextMessage], ToolCall], ReasoningItem], ToolResult]
246
- ModelOutputItem: 'TypeAlias' = typing.Union[typing.Union[AssistantMessage, ToolCall], ReasoningItem]
247
- Operation: 'TypeAlias' = "UserTurnOp | ShutdownOp"
248
-
249
-
250
- @dataclass(frozen=True, )
318
+ ConversationItem: "TypeAlias" = typing.Union[
319
+ typing.Union[
320
+ typing.Union[
321
+ typing.Union[typing.Union[UserMessage, AssistantMessage], ContextMessage],
322
+ ToolCall,
323
+ ],
324
+ ReasoningItem,
325
+ ],
326
+ ToolResult,
327
+ ]
328
+ ModelOutputItem: "TypeAlias" = typing.Union[
329
+ typing.Union[AssistantMessage, ToolCall], ReasoningItem
330
+ ]
331
+
332
+
333
+ @dataclass(
334
+ frozen=True,
335
+ )
251
336
  class Prompt:
252
337
  """何时:Agent 每发起一轮模型采样前构造。
253
338
  发送方:Agent。
254
339
  接收方:ModelClient。
255
340
  """
256
341
 
257
- input: 'typing.List[ConversationItem]'
258
- tools: 'typing.List[ToolSpec]'
259
- parallel_tool_calls: 'bool' = True
260
- base_instructions: 'typing.Union[str, None]' = None
261
- turn_id: 'typing.Union[str, None]' = None
262
- turn_metadata: 'typing.Union[JSONDict, None]' = None
342
+ input: "typing.List[ConversationItem]"
343
+ tools: "typing.List[ToolSpec]"
344
+ parallel_tool_calls: "bool" = True
345
+ base_instructions: "typing.Union[str, None]" = None
346
+ turn_id: "typing.Union[str, None]" = None
347
+ turn_metadata: "typing.Union[JSONDict, None]" = None
263
348
 
264
349
 
265
- @dataclass(frozen=True, )
350
+ @dataclass(
351
+ frozen=True,
352
+ )
266
353
  class ModelResponse:
267
354
  """何时:ModelClient 完成一轮 `Prompt` 处理后返回。
268
355
  发送方:ModelClient。
269
356
  接收方:Agent。
270
357
  """
271
358
 
272
- items: 'typing.List[ModelOutputItem]'
273
-
359
+ items: "typing.List[ModelOutputItem]"
274
360
 
275
- @dataclass(frozen=True, )
276
- class ModelStreamEvent:
277
- """何时:ModelClient 处理 `Prompt` 的过程中有流式中间结果时产生。
278
- 发送方:ModelClient。
279
- 接收方:Agent。
280
- """
361
+ def __post_init__(self) -> "None":
362
+ for item in self.items:
363
+ if not isinstance(item, (AssistantMessage, ToolCall, ReasoningItem)):
364
+ raise TypeError(f"invalid model output item: {type(item).__name__}")
281
365
 
282
- kind: 'str'
283
- payload: 'JSONDict' = field(default_factory=dict)
284
366
 
285
-
286
- @dataclass(frozen=True, )
367
+ @dataclass(
368
+ frozen=True,
369
+ )
287
370
  class TurnResult:
288
371
  """何时:一个 turn 已经收敛,Agent 决定结束本轮时返回。
289
372
  发送方:Agent。
290
373
  接收方:外部调用方。
291
374
  """
292
375
 
293
- turn_id: 'str'
294
- output_text: 'typing.Union[str, None]'
295
- iterations: 'int'
296
- response_items: 'typing.Tuple[ModelOutputItem, ...]'
297
- history: 'typing.Tuple[ConversationItem, ...]'
298
-
299
-
300
- @dataclass(frozen=True, )
301
- class AgentEvent:
302
- """何时:主循环运行过程中发生阶段性事件时发出,例如模型调用、工具开始/结束。
303
- 发送方:Agent。
304
- 接收方:可选的事件观察者 / 回调。
305
- """
306
-
307
- kind: 'str'
308
- turn_id: 'str'
309
- payload: 'typing.Dict[str, object]' = field(default_factory=dict)
310
-
311
-
312
- @dataclass(frozen=True, )
313
- class UserTurnOp:
314
- """何时:运行时要提交一个新的用户请求时创建。
315
- 发送方:外部运行时调用方。
316
- 接收方:AgentRuntime。
317
- """
318
-
319
- texts: 'typing.List[str]'
320
-
321
-
322
- @dataclass(frozen=True, )
323
- class ShutdownOp:
324
- """何时:运行时要停止外层提交循环时创建。
325
- 发送方:外部运行时调用方。
326
- 接收方:AgentRuntime。
327
- """
328
-
329
- pass
330
-
331
-
332
- @dataclass(frozen=True, )
333
- class Submission:
334
- """何时:任意运行时操作要进入 AgentRuntime 队列时创建。
335
- 发送方:外部运行时调用方。
336
- 接收方:AgentRuntime 的提交队列 / 外层循环。
337
- """
338
-
339
- id: 'str'
340
- op: 'Operation'
376
+ turn_id: "str"
377
+ output_text: "typing.Union[str, None]"
378
+ iterations: "int"
379
+ response_items: "typing.Tuple[ModelOutputItem, ...]"
380
+ history: "typing.Tuple[ConversationItem, ...]"