python-codex 0.0.1__py3-none-any.whl → 0.1.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.
- pycodex/__init__.py +139 -2
- pycodex/agent.py +290 -0
- pycodex/cli.py +641 -0
- pycodex/collaboration.py +21 -0
- pycodex/context.py +580 -0
- pycodex/doctor.py +360 -0
- pycodex/model.py +533 -0
- pycodex/prompts/collaboration_default.md +11 -0
- pycodex/prompts/collaboration_plan.md +128 -0
- pycodex/prompts/default_base_instructions.md +275 -0
- pycodex/prompts/exec_tools.json +411 -0
- pycodex/prompts/models.json +847 -0
- pycodex/prompts/permissions/approval_policy/never.md +1 -0
- pycodex/prompts/permissions/approval_policy/on_failure.md +1 -0
- pycodex/prompts/permissions/approval_policy/on_request.md +57 -0
- pycodex/prompts/permissions/approval_policy/on_request_rule_request_permission.md +33 -0
- pycodex/prompts/permissions/approval_policy/unless_trusted.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/danger_full_access.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/read_only.md +1 -0
- pycodex/prompts/permissions/sandbox_mode/workspace_write.md +1 -0
- pycodex/prompts/subagent_tools.json +163 -0
- pycodex/protocol.py +347 -0
- pycodex/runtime.py +200 -0
- pycodex/runtime_services.py +408 -0
- pycodex/tools/__init__.py +58 -0
- pycodex/tools/agent_tool_schemas.py +70 -0
- pycodex/tools/apply_patch_tool.py +363 -0
- pycodex/tools/base_tool.py +168 -0
- pycodex/tools/close_agent_tool.py +55 -0
- pycodex/tools/code_mode_manager.py +519 -0
- pycodex/tools/exec_command_tool.py +96 -0
- pycodex/tools/exec_runtime.js +161 -0
- pycodex/tools/exec_tool.py +48 -0
- pycodex/tools/grep_files_tool.py +150 -0
- pycodex/tools/list_dir_tool.py +135 -0
- pycodex/tools/read_file_tool.py +217 -0
- pycodex/tools/request_permissions_tool.py +95 -0
- pycodex/tools/request_user_input_tool.py +167 -0
- pycodex/tools/resume_agent_tool.py +56 -0
- pycodex/tools/send_input_tool.py +106 -0
- pycodex/tools/shell_command_tool.py +107 -0
- pycodex/tools/shell_tool.py +112 -0
- pycodex/tools/spawn_agent_tool.py +97 -0
- pycodex/tools/unified_exec_manager.py +380 -0
- pycodex/tools/update_plan_tool.py +79 -0
- pycodex/tools/view_image_tool.py +111 -0
- pycodex/tools/wait_agent_tool.py +75 -0
- pycodex/tools/wait_tool.py +68 -0
- pycodex/tools/web_search_tool.py +30 -0
- pycodex/tools/write_stdin_tool.py +75 -0
- pycodex/utils/__init__.py +40 -0
- pycodex/utils/dotenv.py +64 -0
- pycodex/utils/get_env.py +218 -0
- pycodex/utils/random_ids.py +19 -0
- pycodex/utils/visualize.py +978 -0
- python_codex-0.1.0.dist-info/METADATA +267 -0
- python_codex-0.1.0.dist-info/RECORD +60 -0
- python_codex-0.1.0.dist-info/entry_points.txt +2 -0
- python_codex-0.1.0.dist-info/licenses/LICENSE +201 -0
- python_codex-0.0.1.dist-info/METADATA +0 -30
- python_codex-0.0.1.dist-info/RECORD +0 -4
- {python_codex-0.0.1.dist-info → python_codex-0.1.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Approval policy is currently never. Do not provide the `sandbox_permissions` for any reason, commands will be rejected.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Approvals are your mechanism to get user consent to run shell commands without the sandbox. `approval_policy` is `on-failure`: The harness will allow all commands to run in the sandbox (if enabled), and failures will be escalated to the user for approval to run again without the sandbox.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Escalation Requests
|
|
2
|
+
|
|
3
|
+
Commands are run outside the sandbox if they are approved by the user, or match an existing rule that allows it to run unrestricted. The command string is split into independent command segments at shell control operators, including but not limited to:
|
|
4
|
+
|
|
5
|
+
- Pipes: |
|
|
6
|
+
- Logical operators: &&, ||
|
|
7
|
+
- Command separators: ;
|
|
8
|
+
- Subshell boundaries: (...), $(...)
|
|
9
|
+
|
|
10
|
+
Each resulting segment is evaluated independently for sandbox restrictions and approval requirements.
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
|
|
14
|
+
git pull | tee output.txt
|
|
15
|
+
|
|
16
|
+
This is treated as two command segments:
|
|
17
|
+
|
|
18
|
+
["git", "pull"]
|
|
19
|
+
|
|
20
|
+
["tee", "output.txt"]
|
|
21
|
+
|
|
22
|
+
Commands that use more advanced shell features like redirection (>, >>, <), substitutions ($(...), ...), environment variables (FOO=bar), or wildcard patterns (*, ?) will not be evaluated against rules, to limit the scope of what an approved rule allows.
|
|
23
|
+
|
|
24
|
+
## How to request escalation
|
|
25
|
+
|
|
26
|
+
IMPORTANT: To request approval to execute a command that will require escalated privileges:
|
|
27
|
+
|
|
28
|
+
- Provide the `sandbox_permissions` parameter with the value `"require_escalated"`
|
|
29
|
+
- Include a short question asking the user if they want to allow the action in `justification` parameter. e.g. "Do you want to download and install dependencies for this project?"
|
|
30
|
+
- Optionally suggest a `prefix_rule` - this will be shown to the user with an option to persist the rule approval for future sessions.
|
|
31
|
+
|
|
32
|
+
If you run a command that is important to solving the user's query, but it fails because of sandboxing or with a likely sandbox-related network error (for example DNS/host resolution, registry/index access, or dependency download failure), rerun the command with "require_escalated". ALWAYS proceed to use the `justification` parameter - do not message the user before requesting approval for the command.
|
|
33
|
+
|
|
34
|
+
## When to request escalation
|
|
35
|
+
|
|
36
|
+
While commands are running inside the sandbox, here are some scenarios that will require escalation outside the sandbox:
|
|
37
|
+
|
|
38
|
+
- You need to run a command that writes to a directory that requires it (e.g. running tests that write to /var)
|
|
39
|
+
- You need to run a GUI app (e.g., open/xdg-open/osascript) to open browsers or files.
|
|
40
|
+
- If you run a command that is important to solving the user's query, but it fails because of sandboxing or with a likely sandbox-related network error (for example DNS/host resolution, registry/index access, or dependency download failure), rerun the command with `require_escalated`. ALWAYS proceed to use the `sandbox_permissions` and `justification` parameters. do not message the user before requesting approval for the command.
|
|
41
|
+
- You are about to take a potentially destructive action such as an `rm` or `git reset` that the user did not explicitly ask for.
|
|
42
|
+
- Be judicious with escalating, but if completing the user's request requires it, you should do so - don't try and circumvent approvals by using other tools.
|
|
43
|
+
|
|
44
|
+
## prefix_rule guidance
|
|
45
|
+
|
|
46
|
+
When choosing a `prefix_rule`, request one that will allow you to fulfill similar requests from the user in the future without re-requesting escalation. It should be categorical and reasonably scoped to similar capabilities. You should rarely pass the entire command into `prefix_rule`.
|
|
47
|
+
|
|
48
|
+
### Banned prefix_rules
|
|
49
|
+
Avoid requesting overly broad prefixes that the user would be ill-advised to approve. For example, do not request ["python3"], ["python", "-"], or other similar prefixes that would allow arbitrary scripting.
|
|
50
|
+
NEVER provide a prefix_rule argument for destructive commands like rm.
|
|
51
|
+
NEVER provide a prefix_rule if your command uses a heredoc or herestring.
|
|
52
|
+
|
|
53
|
+
### Examples
|
|
54
|
+
Good examples of prefixes:
|
|
55
|
+
- ["npm", "run", "dev"]
|
|
56
|
+
- ["gh", "pr", "check"]
|
|
57
|
+
- ["cargo", "test"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Permission Requests
|
|
2
|
+
|
|
3
|
+
Commands may require user approval before execution. Prefer requesting sandboxed additional permissions instead of asking to run fully outside the sandbox.
|
|
4
|
+
|
|
5
|
+
## Preferred request mode
|
|
6
|
+
|
|
7
|
+
When you need extra sandboxed permissions for one command, use:
|
|
8
|
+
|
|
9
|
+
- `sandbox_permissions: "with_additional_permissions"`
|
|
10
|
+
- `additional_permissions` with one or more of:
|
|
11
|
+
- `network.enabled`: set to `true` to enable network access
|
|
12
|
+
- `file_system.read`: list of paths that need read access
|
|
13
|
+
- `file_system.write`: list of paths that need write access
|
|
14
|
+
|
|
15
|
+
When using the `request_permissions` tool directly, only request `network` and `file_system` permissions.
|
|
16
|
+
|
|
17
|
+
This keeps execution inside the current sandbox policy, while adding only the requested permissions for that command, unless an exec-policy allow rule applies and authorizes running the command outside the sandbox.
|
|
18
|
+
|
|
19
|
+
If the command already matches an exec-policy allow rule, the command can be auto-approved without an extra prompt. In that case, exec-policy allow behavior (including any sandbox bypass) takes precedence.
|
|
20
|
+
|
|
21
|
+
## Escalation Requests
|
|
22
|
+
|
|
23
|
+
Use full escalation only when sandboxed additional permissions cannot satisfy the task.
|
|
24
|
+
|
|
25
|
+
- `sandbox_permissions: "require_escalated"`
|
|
26
|
+
- Include `justification` as a short question asking for approval.
|
|
27
|
+
- Optionally include `prefix_rule` to suggest a reusable allow rule.
|
|
28
|
+
|
|
29
|
+
## Command segmentation reminder
|
|
30
|
+
|
|
31
|
+
The command string is split into independent command segments at shell control operators, including pipes (`|`), logical operators (`&&`, `||`), command separators (`;`), and subshell boundaries (`(...)`, `$()`).
|
|
32
|
+
|
|
33
|
+
Each segment is evaluated independently for sandbox restrictions and approval requirements.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Approvals are your mechanism to get user consent to run shell commands without the sandbox. `approval_policy` is `unless-trusted`: The harness will escalate most commands for user approval, apart from a limited allowlist of safe "read" commands.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `danger-full-access`: No filesystem sandboxing - all commands are permitted. Network access is {network_access}.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `read-only`: The sandbox only permits reading files. Network access is {network_access}.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Filesystem sandboxing defines which files can be read or written. `sandbox_mode` is `workspace-write`: The sandbox permits reading files, and editing files in `cwd` and `writable_roots`. Editing files in other directories requires approval. Network access is {network_access}.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"type": "function",
|
|
4
|
+
"name": "exec_command",
|
|
5
|
+
"description": "Runs a command in a PTY, returning output or a session ID for ongoing interaction.",
|
|
6
|
+
"strict": false,
|
|
7
|
+
"parameters": {
|
|
8
|
+
"type": "object",
|
|
9
|
+
"properties": {
|
|
10
|
+
"cmd": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"description": "Shell command to execute."
|
|
13
|
+
},
|
|
14
|
+
"justification": {
|
|
15
|
+
"type": "string",
|
|
16
|
+
"description": "Only set if sandbox_permissions is \\\"require_escalated\\\".\n Request approval from the user to run this command outside the sandbox.\n Phrased as a simple question that summarizes the purpose of the\n command as it relates to the task at hand - e.g. 'Do you want to\n fetch and pull the latest version of this git branch?'"
|
|
17
|
+
},
|
|
18
|
+
"login": {
|
|
19
|
+
"type": "boolean",
|
|
20
|
+
"description": "Whether to run the shell with -l/-i semantics. Defaults to true."
|
|
21
|
+
},
|
|
22
|
+
"max_output_tokens": {
|
|
23
|
+
"type": "number",
|
|
24
|
+
"description": "Maximum number of tokens to return. Excess output will be truncated."
|
|
25
|
+
},
|
|
26
|
+
"prefix_rule": {
|
|
27
|
+
"type": "array",
|
|
28
|
+
"items": {
|
|
29
|
+
"type": "string"
|
|
30
|
+
},
|
|
31
|
+
"description": "Only specify when sandbox_permissions is `require_escalated`.\n Suggest a prefix command pattern that will allow you to fulfill similar requests from the user in the future.\n Should be a short but reasonable prefix, e.g. [\\\"git\\\", \\\"pull\\\"] or [\\\"uv\\\", \\\"run\\\"] or [\\\"pytest\\\"]."
|
|
32
|
+
},
|
|
33
|
+
"sandbox_permissions": {
|
|
34
|
+
"type": "string",
|
|
35
|
+
"description": "Sandbox permissions for the command. Set to \"require_escalated\" to request running without sandbox restrictions; defaults to \"use_default\"."
|
|
36
|
+
},
|
|
37
|
+
"shell": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"description": "Shell binary to launch. Defaults to the user's default shell."
|
|
40
|
+
},
|
|
41
|
+
"tty": {
|
|
42
|
+
"type": "boolean",
|
|
43
|
+
"description": "Whether to allocate a TTY for the command. Defaults to false (plain pipes); set to true to open a PTY and access TTY process."
|
|
44
|
+
},
|
|
45
|
+
"workdir": {
|
|
46
|
+
"type": "string",
|
|
47
|
+
"description": "Optional working directory to run the command in; defaults to the turn cwd."
|
|
48
|
+
},
|
|
49
|
+
"yield_time_ms": {
|
|
50
|
+
"type": "number",
|
|
51
|
+
"description": "How long to wait (in milliseconds) for output before yielding."
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
"required": [
|
|
55
|
+
"cmd"
|
|
56
|
+
],
|
|
57
|
+
"additionalProperties": false
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "function",
|
|
62
|
+
"name": "write_stdin",
|
|
63
|
+
"description": "Writes characters to an existing unified exec session and returns recent output.",
|
|
64
|
+
"strict": false,
|
|
65
|
+
"parameters": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": {
|
|
68
|
+
"chars": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "Bytes to write to stdin (may be empty to poll)."
|
|
71
|
+
},
|
|
72
|
+
"max_output_tokens": {
|
|
73
|
+
"type": "number",
|
|
74
|
+
"description": "Maximum number of tokens to return. Excess output will be truncated."
|
|
75
|
+
},
|
|
76
|
+
"session_id": {
|
|
77
|
+
"type": "number",
|
|
78
|
+
"description": "Identifier of the running unified exec session."
|
|
79
|
+
},
|
|
80
|
+
"yield_time_ms": {
|
|
81
|
+
"type": "number",
|
|
82
|
+
"description": "How long to wait (in milliseconds) for output before yielding."
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
"required": [
|
|
86
|
+
"session_id"
|
|
87
|
+
],
|
|
88
|
+
"additionalProperties": false
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"type": "function",
|
|
93
|
+
"name": "update_plan",
|
|
94
|
+
"description": "Updates the task plan.\nProvide an optional explanation and a list of plan items, each with a step and status.\nAt most one step can be in_progress at a time.\n",
|
|
95
|
+
"strict": false,
|
|
96
|
+
"parameters": {
|
|
97
|
+
"type": "object",
|
|
98
|
+
"properties": {
|
|
99
|
+
"explanation": {
|
|
100
|
+
"type": "string"
|
|
101
|
+
},
|
|
102
|
+
"plan": {
|
|
103
|
+
"type": "array",
|
|
104
|
+
"items": {
|
|
105
|
+
"type": "object",
|
|
106
|
+
"properties": {
|
|
107
|
+
"status": {
|
|
108
|
+
"type": "string",
|
|
109
|
+
"description": "One of: pending, in_progress, completed"
|
|
110
|
+
},
|
|
111
|
+
"step": {
|
|
112
|
+
"type": "string"
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"required": [
|
|
116
|
+
"step",
|
|
117
|
+
"status"
|
|
118
|
+
],
|
|
119
|
+
"additionalProperties": false
|
|
120
|
+
},
|
|
121
|
+
"description": "The list of steps"
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
"required": [
|
|
125
|
+
"plan"
|
|
126
|
+
],
|
|
127
|
+
"additionalProperties": false
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"type": "custom",
|
|
132
|
+
"name": "apply_patch",
|
|
133
|
+
"description": "Use the `apply_patch` tool to edit files. This is a FREEFORM tool, so do not wrap the patch in JSON.",
|
|
134
|
+
"format": {
|
|
135
|
+
"type": "grammar",
|
|
136
|
+
"syntax": "lark",
|
|
137
|
+
"definition": "start: begin_patch hunk+ end_patch\nbegin_patch: \"*** Begin Patch\" LF\nend_patch: \"*** End Patch\" LF?\n\nhunk: add_hunk | delete_hunk | update_hunk\nadd_hunk: \"*** Add File: \" filename LF add_line+\ndelete_hunk: \"*** Delete File: \" filename LF\nupdate_hunk: \"*** Update File: \" filename LF change_move? change?\n\nfilename: /(.+)/\nadd_line: \"+\" /(.*)/ LF -> line\n\nchange_move: \"*** Move to: \" filename LF\nchange: (change_context | change_line)+ eof_line?\nchange_context: (\"@@\" | \"@@ \" /(.+)/) LF\nchange_line: (\"+\" | \"-\" | \" \") /(.*)/ LF\neof_line: \"*** End of File\" LF\n\n%import common.LF\n"
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
"type": "web_search",
|
|
142
|
+
"external_web_access": true
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
"type": "function",
|
|
146
|
+
"name": "view_image",
|
|
147
|
+
"description": "View a local image from the filesystem (only use if given a full filepath by the user, and the image isn't already attached to the thread context within <image ...> tags).",
|
|
148
|
+
"strict": false,
|
|
149
|
+
"parameters": {
|
|
150
|
+
"type": "object",
|
|
151
|
+
"properties": {
|
|
152
|
+
"path": {
|
|
153
|
+
"type": "string",
|
|
154
|
+
"description": "Local filesystem path to an image file"
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
"required": [
|
|
158
|
+
"path"
|
|
159
|
+
],
|
|
160
|
+
"additionalProperties": false
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
]
|
pycodex/protocol.py
ADDED
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""pycodex 的最小协议层定义。
|
|
2
|
+
|
|
3
|
+
核心抽象:
|
|
4
|
+
- AgentLoop:内层 turn 主循环,负责维护历史、调用模型、执行工具,并在
|
|
5
|
+
`ToolCall -> ToolResult -> 下一轮模型调用` 之间反复闭环,直到得到稳定回复。
|
|
6
|
+
- AgentRuntime:外层提交队列,负责按顺序接收 `Submission`,驱动
|
|
7
|
+
`UserTurnOp` / `ShutdownOp` 这类运行时操作。
|
|
8
|
+
- ContextManager:负责把基础指令、AGENTS.md、环境信息等上下文拼成每轮模型看到的
|
|
9
|
+
`Prompt` 前缀,但这些注入内容不会写回长期会话历史。
|
|
10
|
+
- ModelClient:模型后端抽象,接收 `Prompt`,返回 `ModelResponse`。
|
|
11
|
+
- ModelStreamEvent:模型在一次 `Prompt` 处理中途产生的流式事件,例如文本增量。
|
|
12
|
+
- ToolRegistry:工具执行抽象,接收 `ToolCall`,产出 `ToolResult`。
|
|
13
|
+
|
|
14
|
+
本文件只定义这些抽象之间传递的数据结构,不包含具体执行逻辑。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from copy import deepcopy
|
|
20
|
+
import json
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import Any, Literal, TypeAlias
|
|
23
|
+
|
|
24
|
+
JSONValue: TypeAlias = Any
|
|
25
|
+
JSONDict: TypeAlias = dict[str, Any]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True, slots=True)
|
|
29
|
+
class ToolSpec:
|
|
30
|
+
"""何时:AgentLoop 准备发起一轮模型请求时,随 `Prompt.tools` 一起发送。
|
|
31
|
+
发送方:AgentLoop。
|
|
32
|
+
接收方:ModelClient。
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
name: str
|
|
36
|
+
description: str
|
|
37
|
+
input_schema: JSONDict | None = None
|
|
38
|
+
tool_type: Literal["function", "custom", "web_search"] = "function"
|
|
39
|
+
format: JSONDict | None = None
|
|
40
|
+
options: JSONDict | None = None
|
|
41
|
+
output_schema: JSONDict | None = None
|
|
42
|
+
supports_parallel: bool = True
|
|
43
|
+
raw_payload: JSONDict | None = None
|
|
44
|
+
|
|
45
|
+
def serialize(self) -> JSONDict:
|
|
46
|
+
if self.raw_payload is not None:
|
|
47
|
+
return deepcopy(self.raw_payload)
|
|
48
|
+
if self.tool_type == "web_search":
|
|
49
|
+
payload = {"type": "web_search"}
|
|
50
|
+
if self.options is not None:
|
|
51
|
+
payload.update(self.options)
|
|
52
|
+
return payload
|
|
53
|
+
|
|
54
|
+
if self.tool_type == "custom":
|
|
55
|
+
if self.format is None:
|
|
56
|
+
raise ValueError("custom tools require `format`")
|
|
57
|
+
return {
|
|
58
|
+
"type": "custom",
|
|
59
|
+
"name": self.name,
|
|
60
|
+
"description": self.description,
|
|
61
|
+
"format": self.format,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if self.input_schema is None:
|
|
65
|
+
raise ValueError("function tools require `input_schema`")
|
|
66
|
+
|
|
67
|
+
payload = {
|
|
68
|
+
"type": "function",
|
|
69
|
+
"name": self.name,
|
|
70
|
+
"description": self.description,
|
|
71
|
+
"parameters": self.input_schema,
|
|
72
|
+
"strict": False,
|
|
73
|
+
}
|
|
74
|
+
if self.output_schema is not None:
|
|
75
|
+
payload["output_schema"] = self.output_schema
|
|
76
|
+
return payload
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@dataclass(frozen=True, slots=True)
|
|
80
|
+
class UserMessage:
|
|
81
|
+
"""何时:外部发起一个新的用户 turn 时创建,并写入会话历史。
|
|
82
|
+
发送方:外部调用方创建,AgentLoop 转发。
|
|
83
|
+
接收方:AgentLoop 先接收,随后 ModelClient 在 `Prompt.input` 中看到它。
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
text: str
|
|
87
|
+
role: Literal["user"] = "user"
|
|
88
|
+
|
|
89
|
+
def serialize(self) -> JSONDict:
|
|
90
|
+
return {
|
|
91
|
+
"type": "message",
|
|
92
|
+
"role": self.role,
|
|
93
|
+
"content": [{"type": "input_text", "text": self.text}],
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass(frozen=True, slots=True)
|
|
98
|
+
class AssistantMessage:
|
|
99
|
+
"""何时:模型要直接输出自然语言内容时产生,可作为中间文本或最终回复。
|
|
100
|
+
发送方:ModelClient。
|
|
101
|
+
接收方:AgentLoop。
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
text: str
|
|
105
|
+
role: Literal["assistant"] = "assistant"
|
|
106
|
+
|
|
107
|
+
def serialize(self) -> JSONDict:
|
|
108
|
+
return {
|
|
109
|
+
"type": "message",
|
|
110
|
+
"role": self.role,
|
|
111
|
+
"content": [{"type": "output_text", "text": self.text}],
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass(frozen=True, slots=True)
|
|
116
|
+
class ContextMessage:
|
|
117
|
+
"""何时:ContextManager 为单轮模型请求注入额外上下文时构造。
|
|
118
|
+
发送方:ContextManager。
|
|
119
|
+
接收方:ModelClient。
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
text: str | None = None
|
|
123
|
+
role: Literal["user", "developer"] = "user"
|
|
124
|
+
content_items: tuple[JSONDict, ...] | None = None
|
|
125
|
+
|
|
126
|
+
def serialize(self) -> JSONDict:
|
|
127
|
+
if self.content_items is not None:
|
|
128
|
+
content = list(self.content_items)
|
|
129
|
+
else:
|
|
130
|
+
if self.text is None:
|
|
131
|
+
raise ValueError("ContextMessage requires `text` or `content_items`")
|
|
132
|
+
content = [{"type": "input_text", "text": self.text}]
|
|
133
|
+
return {
|
|
134
|
+
"type": "message",
|
|
135
|
+
"role": self.role,
|
|
136
|
+
"content": content,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass(frozen=True, slots=True)
|
|
141
|
+
class ToolCall:
|
|
142
|
+
"""何时:模型决定调用工具而不是只输出文本时产生。
|
|
143
|
+
发送方:ModelClient。
|
|
144
|
+
接收方:AgentLoop,随后由它转给 ToolRegistry 执行。
|
|
145
|
+
"""
|
|
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"
|
|
152
|
+
|
|
153
|
+
def serialize(self) -> JSONDict:
|
|
154
|
+
if self.tool_type == "custom":
|
|
155
|
+
return {
|
|
156
|
+
"type": "custom_tool_call",
|
|
157
|
+
"name": self.name,
|
|
158
|
+
"input": str(self.arguments),
|
|
159
|
+
"call_id": self.call_id,
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
"type": "function_call",
|
|
163
|
+
"name": self.name,
|
|
164
|
+
"arguments": json.dumps(
|
|
165
|
+
self.arguments,
|
|
166
|
+
ensure_ascii=False,
|
|
167
|
+
separators=(",", ":"),
|
|
168
|
+
),
|
|
169
|
+
"call_id": self.call_id,
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True, slots=True)
|
|
174
|
+
class ReasoningItem:
|
|
175
|
+
"""何时:模型在一次 Responses 采样里产出 reasoning item 时产生。
|
|
176
|
+
发送方:ModelClient。
|
|
177
|
+
接收方:AgentLoop;它会把该 item 保留进 history,并在后续请求里原样回传给
|
|
178
|
+
ModelClient。
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
payload: JSONDict
|
|
182
|
+
kind: Literal["reasoning"] = "reasoning"
|
|
183
|
+
|
|
184
|
+
def serialize(self) -> JSONDict:
|
|
185
|
+
return deepcopy(self.payload)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
@dataclass(frozen=True, slots=True)
|
|
189
|
+
class ToolResult:
|
|
190
|
+
"""何时:某个 `ToolCall` 执行完成后产生,用于喂回下一轮模型调用。
|
|
191
|
+
发送方:ToolRegistry 产出,AgentLoop 追加并转发。
|
|
192
|
+
接收方:AgentLoop 先接收,随后 ModelClient 在下一轮 `Prompt.input` 中看到它。
|
|
193
|
+
"""
|
|
194
|
+
|
|
195
|
+
call_id: str
|
|
196
|
+
name: str
|
|
197
|
+
output: JSONValue
|
|
198
|
+
content_items: tuple[JSONDict, ...] | None = None
|
|
199
|
+
success: bool | None = None
|
|
200
|
+
is_error: bool = False
|
|
201
|
+
tool_type: Literal["function", "custom"] = "function"
|
|
202
|
+
kind: Literal["tool_result"] = "tool_result"
|
|
203
|
+
|
|
204
|
+
def output_text(self) -> str:
|
|
205
|
+
if self.content_items is not None:
|
|
206
|
+
text_parts = [
|
|
207
|
+
str(item.get("text", ""))
|
|
208
|
+
for item in self.content_items
|
|
209
|
+
if item.get("type") == "input_text"
|
|
210
|
+
]
|
|
211
|
+
if text_parts:
|
|
212
|
+
return "\n".join(text_parts)
|
|
213
|
+
if isinstance(self.output, str):
|
|
214
|
+
return self.output
|
|
215
|
+
return json.dumps(self.output, ensure_ascii=False)
|
|
216
|
+
if isinstance(self.output, str):
|
|
217
|
+
return self.output
|
|
218
|
+
return json.dumps(self.output, ensure_ascii=False)
|
|
219
|
+
|
|
220
|
+
def serialize(self) -> JSONDict:
|
|
221
|
+
payload_output: JSONValue
|
|
222
|
+
if self.content_items is not None:
|
|
223
|
+
payload_output = list(self.content_items)
|
|
224
|
+
elif isinstance(self.output, str):
|
|
225
|
+
payload_output = self.output
|
|
226
|
+
else:
|
|
227
|
+
payload_output = json.dumps(
|
|
228
|
+
self.output,
|
|
229
|
+
ensure_ascii=False,
|
|
230
|
+
separators=(",", ":"),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
item_type = (
|
|
234
|
+
"custom_tool_call_output"
|
|
235
|
+
if self.tool_type == "custom"
|
|
236
|
+
else "function_call_output"
|
|
237
|
+
)
|
|
238
|
+
payload = {
|
|
239
|
+
"type": item_type,
|
|
240
|
+
"call_id": self.call_id,
|
|
241
|
+
"output": payload_output,
|
|
242
|
+
}
|
|
243
|
+
if self.success is not None:
|
|
244
|
+
payload["success"] = self.success
|
|
245
|
+
if self.tool_type == "custom":
|
|
246
|
+
payload["name"] = self.name
|
|
247
|
+
return payload
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
ConversationItem: TypeAlias = (
|
|
251
|
+
UserMessage | AssistantMessage | ContextMessage | ToolCall | ReasoningItem | ToolResult
|
|
252
|
+
)
|
|
253
|
+
ModelOutputItem: TypeAlias = AssistantMessage | ToolCall | ReasoningItem
|
|
254
|
+
Operation: TypeAlias = "UserTurnOp | ShutdownOp"
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
@dataclass(frozen=True, slots=True)
|
|
258
|
+
class Prompt:
|
|
259
|
+
"""何时:AgentLoop 每发起一轮模型采样前构造。
|
|
260
|
+
发送方:AgentLoop。
|
|
261
|
+
接收方:ModelClient。
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
input: list[ConversationItem]
|
|
265
|
+
tools: list[ToolSpec]
|
|
266
|
+
parallel_tool_calls: bool = True
|
|
267
|
+
base_instructions: str | None = None
|
|
268
|
+
turn_id: str | None = None
|
|
269
|
+
turn_metadata: JSONDict | None = None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@dataclass(frozen=True, slots=True)
|
|
273
|
+
class ModelResponse:
|
|
274
|
+
"""何时:ModelClient 完成一轮 `Prompt` 处理后返回。
|
|
275
|
+
发送方:ModelClient。
|
|
276
|
+
接收方:AgentLoop。
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
items: list[ModelOutputItem]
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass(frozen=True, slots=True)
|
|
283
|
+
class ModelStreamEvent:
|
|
284
|
+
"""何时:ModelClient 处理 `Prompt` 的过程中有流式中间结果时产生。
|
|
285
|
+
发送方:ModelClient。
|
|
286
|
+
接收方:AgentLoop。
|
|
287
|
+
"""
|
|
288
|
+
|
|
289
|
+
kind: str
|
|
290
|
+
payload: JSONDict = field(default_factory=dict)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@dataclass(frozen=True, slots=True)
|
|
294
|
+
class TurnResult:
|
|
295
|
+
"""何时:一个 turn 已经收敛,AgentLoop 决定结束本轮时返回。
|
|
296
|
+
发送方:AgentLoop。
|
|
297
|
+
接收方:外部调用方。
|
|
298
|
+
"""
|
|
299
|
+
|
|
300
|
+
turn_id: str
|
|
301
|
+
output_text: str | None
|
|
302
|
+
iterations: int
|
|
303
|
+
response_items: tuple[ModelOutputItem, ...]
|
|
304
|
+
history: tuple[ConversationItem, ...]
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
@dataclass(frozen=True, slots=True)
|
|
308
|
+
class AgentEvent:
|
|
309
|
+
"""何时:主循环运行过程中发生阶段性事件时发出,例如模型调用、工具开始/结束。
|
|
310
|
+
发送方:AgentLoop。
|
|
311
|
+
接收方:可选的事件观察者 / 回调。
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
kind: str
|
|
315
|
+
turn_id: str
|
|
316
|
+
payload: dict[str, object] = field(default_factory=dict)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
@dataclass(frozen=True, slots=True)
|
|
320
|
+
class UserTurnOp:
|
|
321
|
+
"""何时:运行时要提交一个新的用户请求时创建。
|
|
322
|
+
发送方:外部运行时调用方。
|
|
323
|
+
接收方:AgentRuntime。
|
|
324
|
+
"""
|
|
325
|
+
|
|
326
|
+
texts: list[str]
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
@dataclass(frozen=True, slots=True)
|
|
330
|
+
class ShutdownOp:
|
|
331
|
+
"""何时:运行时要停止外层提交循环时创建。
|
|
332
|
+
发送方:外部运行时调用方。
|
|
333
|
+
接收方:AgentRuntime。
|
|
334
|
+
"""
|
|
335
|
+
|
|
336
|
+
pass
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
@dataclass(frozen=True, slots=True)
|
|
340
|
+
class Submission:
|
|
341
|
+
"""何时:任意运行时操作要进入 AgentRuntime 队列时创建。
|
|
342
|
+
发送方:外部运行时调用方。
|
|
343
|
+
接收方:AgentRuntime 的提交队列 / 外层循环。
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
id: str
|
|
347
|
+
op: Operation
|