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
pycodex/runtime.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from collections import deque
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
from .agent import AgentLoop, EventHandler, NOOP_EVENT_HANDLER, TurnInterrupted
|
|
9
|
+
from .protocol import AgentEvent, Operation, ShutdownOp, Submission, TurnResult, UserTurnOp
|
|
10
|
+
from .utils import uuid7_string
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(slots=True)
|
|
14
|
+
class _QueuedSubmission:
|
|
15
|
+
submission: Submission
|
|
16
|
+
turn_id: str
|
|
17
|
+
futures: list[asyncio.Future[TurnResult | None]]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentRuntime:
|
|
21
|
+
"""Thin outer queue that mirrors the Rust `submission_loop` shape."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, agent_loop: AgentLoop) -> None:
|
|
24
|
+
self._agent_loop = agent_loop
|
|
25
|
+
self._enqueue_queue: deque[_QueuedSubmission] = deque()
|
|
26
|
+
self._steer_queue: deque[_QueuedSubmission] = deque()
|
|
27
|
+
self._queue_lock = asyncio.Lock()
|
|
28
|
+
self._queue_event = asyncio.Event()
|
|
29
|
+
self._current_submission: _QueuedSubmission | None = None
|
|
30
|
+
self._current_task: asyncio.Task[TurnResult] | None = None
|
|
31
|
+
self._event_handler = NOOP_EVENT_HANDLER
|
|
32
|
+
self._agent_loop.set_event_handler(self._handle_agent_event)
|
|
33
|
+
|
|
34
|
+
def set_event_handler(self, event_handler: EventHandler = NOOP_EVENT_HANDLER) -> None:
|
|
35
|
+
self._event_handler = event_handler
|
|
36
|
+
|
|
37
|
+
async def submit_user_turn(self, text: str) -> TurnResult:
|
|
38
|
+
_submission_id, future = await self.enqueue_user_turn(text, queue="enqueue")
|
|
39
|
+
result = await future
|
|
40
|
+
assert result is not None
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
async def enqueue_user_turn(
|
|
44
|
+
self,
|
|
45
|
+
text: str,
|
|
46
|
+
queue: Literal["enqueue", "steer"] = "enqueue",
|
|
47
|
+
) -> tuple[str, asyncio.Future[TurnResult | None]]:
|
|
48
|
+
future: asyncio.Future[TurnResult | None] = asyncio.get_running_loop().create_future()
|
|
49
|
+
return await self._enqueue_user_turn_to_queue(
|
|
50
|
+
text,
|
|
51
|
+
future,
|
|
52
|
+
queue=queue,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
async def shutdown(self) -> None:
|
|
56
|
+
submission = Submission(id=uuid7_string(), op=ShutdownOp())
|
|
57
|
+
future: asyncio.Future[TurnResult | None] = asyncio.get_running_loop().create_future()
|
|
58
|
+
self._enqueue_queue.append(
|
|
59
|
+
_QueuedSubmission(
|
|
60
|
+
submission=submission,
|
|
61
|
+
turn_id=submission.id,
|
|
62
|
+
futures=[future],
|
|
63
|
+
)
|
|
64
|
+
)
|
|
65
|
+
self._queue_event.set()
|
|
66
|
+
await future
|
|
67
|
+
|
|
68
|
+
async def run_forever(self) -> None:
|
|
69
|
+
while True:
|
|
70
|
+
queued = await self._next_submission()
|
|
71
|
+
submission = queued.submission
|
|
72
|
+
self._current_submission = queued
|
|
73
|
+
try:
|
|
74
|
+
if isinstance(submission.op, UserTurnOp):
|
|
75
|
+
self._current_task = asyncio.create_task(
|
|
76
|
+
self._agent_loop.run_turn(
|
|
77
|
+
list(submission.op.texts),
|
|
78
|
+
turn_id=queued.turn_id,
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
try:
|
|
82
|
+
result = await self._current_task
|
|
83
|
+
except TurnInterrupted:
|
|
84
|
+
self._finish_submission_exception(
|
|
85
|
+
queued,
|
|
86
|
+
RuntimeError("submission interrupted"),
|
|
87
|
+
)
|
|
88
|
+
continue
|
|
89
|
+
except asyncio.CancelledError:
|
|
90
|
+
self._finish_submission_exception(
|
|
91
|
+
queued,
|
|
92
|
+
RuntimeError("submission interrupted"),
|
|
93
|
+
)
|
|
94
|
+
continue
|
|
95
|
+
self._finish_submission_result(queued, result)
|
|
96
|
+
continue
|
|
97
|
+
|
|
98
|
+
if isinstance(submission.op, ShutdownOp):
|
|
99
|
+
self._finish_submission_result(queued, None)
|
|
100
|
+
break
|
|
101
|
+
|
|
102
|
+
self._finish_submission_exception(
|
|
103
|
+
queued,
|
|
104
|
+
RuntimeError(f"unsupported operation: {type(submission.op).__name__}"),
|
|
105
|
+
)
|
|
106
|
+
except Exception as exc: # pragma: no cover - defensive wrapper
|
|
107
|
+
self._finish_submission_exception(queued, exc)
|
|
108
|
+
finally:
|
|
109
|
+
self._current_task = None
|
|
110
|
+
self._current_submission = None
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def operation_name(op: Operation) -> str:
|
|
114
|
+
if isinstance(op, UserTurnOp):
|
|
115
|
+
return "user_turn"
|
|
116
|
+
if isinstance(op, ShutdownOp):
|
|
117
|
+
return "shutdown"
|
|
118
|
+
return type(op).__name__
|
|
119
|
+
|
|
120
|
+
async def _enqueue_user_turn_to_queue(
|
|
121
|
+
self,
|
|
122
|
+
text: str,
|
|
123
|
+
future: asyncio.Future[TurnResult | None],
|
|
124
|
+
queue: Literal["enqueue", "steer"],
|
|
125
|
+
) -> tuple[str, asyncio.Future[TurnResult | None]]:
|
|
126
|
+
if queue == "steer" and self._has_active_turn():
|
|
127
|
+
self._agent_loop.interrupt_asap = True
|
|
128
|
+
|
|
129
|
+
async with self._queue_lock:
|
|
130
|
+
if queue == "steer" and self._steer_queue:
|
|
131
|
+
queued = self._steer_queue[-1]
|
|
132
|
+
queued.submission.op.texts.append(text)
|
|
133
|
+
queued.futures.append(future)
|
|
134
|
+
return queued.submission.id, future
|
|
135
|
+
|
|
136
|
+
submission = Submission(id=uuid7_string(), op=UserTurnOp(texts=[text]))
|
|
137
|
+
current = self._current_submission if self._has_active_turn() else None
|
|
138
|
+
turn_id = (
|
|
139
|
+
current.turn_id if queue == "steer" and current is not None else submission.id
|
|
140
|
+
)
|
|
141
|
+
queued = _QueuedSubmission(
|
|
142
|
+
submission=submission,
|
|
143
|
+
turn_id=turn_id,
|
|
144
|
+
futures=[future],
|
|
145
|
+
)
|
|
146
|
+
if queue == "steer":
|
|
147
|
+
self._steer_queue.append(queued)
|
|
148
|
+
else:
|
|
149
|
+
self._enqueue_queue.append(queued)
|
|
150
|
+
self._queue_event.set()
|
|
151
|
+
return submission.id, future
|
|
152
|
+
|
|
153
|
+
async def _next_submission(self) -> _QueuedSubmission:
|
|
154
|
+
while True:
|
|
155
|
+
async with self._queue_lock:
|
|
156
|
+
queued: _QueuedSubmission | None = None
|
|
157
|
+
if self._steer_queue:
|
|
158
|
+
queued = self._steer_queue.popleft()
|
|
159
|
+
elif self._enqueue_queue:
|
|
160
|
+
queued = self._enqueue_queue.popleft()
|
|
161
|
+
if queued is not None:
|
|
162
|
+
if not self._steer_queue and not self._enqueue_queue:
|
|
163
|
+
self._queue_event.clear()
|
|
164
|
+
return queued
|
|
165
|
+
self._queue_event.clear()
|
|
166
|
+
await self._queue_event.wait()
|
|
167
|
+
|
|
168
|
+
@staticmethod
|
|
169
|
+
def _finish_submission_result(
|
|
170
|
+
queued: _QueuedSubmission,
|
|
171
|
+
result: TurnResult | None,
|
|
172
|
+
) -> None:
|
|
173
|
+
for future in queued.futures:
|
|
174
|
+
if not future.done():
|
|
175
|
+
future.set_result(result)
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def _finish_submission_exception(
|
|
179
|
+
queued: _QueuedSubmission,
|
|
180
|
+
exc: Exception,
|
|
181
|
+
) -> None:
|
|
182
|
+
for future in queued.futures:
|
|
183
|
+
if not future.done():
|
|
184
|
+
future.set_exception(exc)
|
|
185
|
+
|
|
186
|
+
def _has_active_turn(self) -> bool:
|
|
187
|
+
current_task = self._current_task
|
|
188
|
+
return current_task is not None and not current_task.done()
|
|
189
|
+
|
|
190
|
+
def _handle_agent_event(self, event: AgentEvent) -> None:
|
|
191
|
+
queued = self._current_submission
|
|
192
|
+
if queued is None:
|
|
193
|
+
self._event_handler(event)
|
|
194
|
+
return
|
|
195
|
+
payload = dict(event.payload)
|
|
196
|
+
payload.setdefault("submission_id", queued.submission.id)
|
|
197
|
+
payload.setdefault("turn_id", queued.turn_id)
|
|
198
|
+
self._event_handler(
|
|
199
|
+
AgentEvent(kind=event.kind, turn_id=event.turn_id, payload=payload)
|
|
200
|
+
)
|
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import random
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import TYPE_CHECKING, Literal
|
|
9
|
+
|
|
10
|
+
from .protocol import ConversationItem, TurnResult
|
|
11
|
+
from .utils import uuid7_string
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .runtime import AgentRuntime
|
|
15
|
+
|
|
16
|
+
PlanStatus = Literal["pending", "in_progress", "completed"]
|
|
17
|
+
PlanListener = Callable[[dict[str, object]], None]
|
|
18
|
+
RuntimeBuilder = Callable[
|
|
19
|
+
[str | None, str | None, tuple[ConversationItem, ...], str],
|
|
20
|
+
"AgentRuntime",
|
|
21
|
+
]
|
|
22
|
+
AsyncJSONHandler = Callable[[dict[str, object]], Awaitable[dict[str, object] | None]]
|
|
23
|
+
|
|
24
|
+
DEFAULT_AGENT_NICKNAME_CANDIDATES = (
|
|
25
|
+
"Bacon",
|
|
26
|
+
"Descartes",
|
|
27
|
+
"Pascal",
|
|
28
|
+
"Fermat",
|
|
29
|
+
"Huygens",
|
|
30
|
+
"Leibniz",
|
|
31
|
+
"Newton",
|
|
32
|
+
"Halley",
|
|
33
|
+
"Euler",
|
|
34
|
+
"Lagrange",
|
|
35
|
+
"Laplace",
|
|
36
|
+
"Volta",
|
|
37
|
+
"Gauss",
|
|
38
|
+
"Ampere",
|
|
39
|
+
"Faraday",
|
|
40
|
+
"Darwin",
|
|
41
|
+
"Lovelace",
|
|
42
|
+
"Boole",
|
|
43
|
+
"Pasteur",
|
|
44
|
+
"Maxwell",
|
|
45
|
+
"Mendel",
|
|
46
|
+
"Curie",
|
|
47
|
+
"Planck",
|
|
48
|
+
"Tesla",
|
|
49
|
+
"Poincare",
|
|
50
|
+
"Noether",
|
|
51
|
+
"Hilbert",
|
|
52
|
+
"Einstein",
|
|
53
|
+
"Raman",
|
|
54
|
+
"Bohr",
|
|
55
|
+
"Turing",
|
|
56
|
+
"Hubble",
|
|
57
|
+
"Feynman",
|
|
58
|
+
"Franklin",
|
|
59
|
+
"McClintock",
|
|
60
|
+
"Meitner",
|
|
61
|
+
"Herschel",
|
|
62
|
+
"Linnaeus",
|
|
63
|
+
"Wegener",
|
|
64
|
+
"Chandrasekhar",
|
|
65
|
+
"Sagan",
|
|
66
|
+
"Goodall",
|
|
67
|
+
"Carson",
|
|
68
|
+
"Carver",
|
|
69
|
+
"Socrates",
|
|
70
|
+
"Plato",
|
|
71
|
+
"Aristotle",
|
|
72
|
+
"Epicurus",
|
|
73
|
+
"Cicero",
|
|
74
|
+
"Confucius",
|
|
75
|
+
"Mencius",
|
|
76
|
+
"Zeno",
|
|
77
|
+
"Locke",
|
|
78
|
+
"Hume",
|
|
79
|
+
"Kant",
|
|
80
|
+
"Hegel",
|
|
81
|
+
"Kierkegaard",
|
|
82
|
+
"Mill",
|
|
83
|
+
"Nietzsche",
|
|
84
|
+
"Peirce",
|
|
85
|
+
"James",
|
|
86
|
+
"Dewey",
|
|
87
|
+
"Russell",
|
|
88
|
+
"Popper",
|
|
89
|
+
"Sartre",
|
|
90
|
+
"Beauvoir",
|
|
91
|
+
"Arendt",
|
|
92
|
+
"Rawls",
|
|
93
|
+
"Singer",
|
|
94
|
+
"Anscombe",
|
|
95
|
+
"Parfit",
|
|
96
|
+
"Kuhn",
|
|
97
|
+
"Boyle",
|
|
98
|
+
"Hooke",
|
|
99
|
+
"Harvey",
|
|
100
|
+
"Dalton",
|
|
101
|
+
"Helmholtz",
|
|
102
|
+
"Gibbs",
|
|
103
|
+
"Lorentz",
|
|
104
|
+
"Schrodinger",
|
|
105
|
+
"Heisenberg",
|
|
106
|
+
"Pauli",
|
|
107
|
+
"Dirac",
|
|
108
|
+
"Bernoulli",
|
|
109
|
+
"Godel",
|
|
110
|
+
"Nash",
|
|
111
|
+
"Banach",
|
|
112
|
+
"Ramanujan",
|
|
113
|
+
"Erdos",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass(frozen=True, slots=True)
|
|
118
|
+
class PlanItem:
|
|
119
|
+
step: str
|
|
120
|
+
status: PlanStatus
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class PlanStore:
|
|
124
|
+
def __init__(self) -> None:
|
|
125
|
+
self._explanation: str | None = None
|
|
126
|
+
self._plan: tuple[PlanItem, ...] = ()
|
|
127
|
+
self._listener: PlanListener = lambda _payload: None
|
|
128
|
+
|
|
129
|
+
def set_listener(self, listener: PlanListener | None) -> None:
|
|
130
|
+
self._listener = listener or (lambda _payload: None)
|
|
131
|
+
|
|
132
|
+
def update(self, explanation: str | None, plan: tuple[PlanItem, ...]) -> None:
|
|
133
|
+
in_progress = sum(1 for item in plan if item.status == "in_progress")
|
|
134
|
+
if in_progress > 1:
|
|
135
|
+
raise ValueError("at most one plan step can be in_progress")
|
|
136
|
+
self._explanation = explanation
|
|
137
|
+
self._plan = plan
|
|
138
|
+
self._listener(
|
|
139
|
+
{
|
|
140
|
+
"explanation": explanation,
|
|
141
|
+
"plan": [
|
|
142
|
+
{"step": item.step, "status": item.status}
|
|
143
|
+
for item in self._plan
|
|
144
|
+
],
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def snapshot(self) -> dict[str, object]:
|
|
149
|
+
return {
|
|
150
|
+
"explanation": self._explanation,
|
|
151
|
+
"plan": [
|
|
152
|
+
{"step": item.step, "status": item.status}
|
|
153
|
+
for item in self._plan
|
|
154
|
+
],
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class RequestUserInputManager:
|
|
159
|
+
def __init__(self) -> None:
|
|
160
|
+
self._handler: AsyncJSONHandler | None = None
|
|
161
|
+
|
|
162
|
+
def set_handler(self, handler: AsyncJSONHandler | None) -> None:
|
|
163
|
+
self._handler = handler
|
|
164
|
+
|
|
165
|
+
async def request(self, payload: dict[str, object]) -> dict[str, object] | None:
|
|
166
|
+
handler = self._handler
|
|
167
|
+
if handler is None:
|
|
168
|
+
return None
|
|
169
|
+
return await handler(payload)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class RequestPermissionsManager:
|
|
173
|
+
def __init__(self) -> None:
|
|
174
|
+
self._handler: AsyncJSONHandler | None = None
|
|
175
|
+
|
|
176
|
+
def set_handler(self, handler: AsyncJSONHandler | None) -> None:
|
|
177
|
+
self._handler = handler
|
|
178
|
+
|
|
179
|
+
async def request(self, payload: dict[str, object]) -> dict[str, object] | None:
|
|
180
|
+
handler = self._handler
|
|
181
|
+
if handler is None:
|
|
182
|
+
return None
|
|
183
|
+
return await handler(payload)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@dataclass(slots=True)
|
|
187
|
+
class ManagedAgent:
|
|
188
|
+
agent_id: str
|
|
189
|
+
runtime: "AgentRuntime"
|
|
190
|
+
worker_task: asyncio.Task[None]
|
|
191
|
+
nickname: str | None = None
|
|
192
|
+
state: str = "pending_init"
|
|
193
|
+
completed_message: str | None = None
|
|
194
|
+
error_message: str | None = None
|
|
195
|
+
pending_submission_ids: set[str] = field(default_factory=set)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class SubAgentManager:
|
|
199
|
+
def __init__(self) -> None:
|
|
200
|
+
self._runtime_builder: RuntimeBuilder | None = None
|
|
201
|
+
self._agents: dict[str, ManagedAgent] = {}
|
|
202
|
+
self._condition = asyncio.Condition()
|
|
203
|
+
self._available_nicknames: list[str] = []
|
|
204
|
+
self._nickname_random = random.Random()
|
|
205
|
+
|
|
206
|
+
def set_runtime_builder(self, builder: RuntimeBuilder | None) -> None:
|
|
207
|
+
self._runtime_builder = builder
|
|
208
|
+
|
|
209
|
+
async def spawn_agent(
|
|
210
|
+
self,
|
|
211
|
+
message: str | None,
|
|
212
|
+
items: list[dict[str, object]] | None,
|
|
213
|
+
agent_type: str | None,
|
|
214
|
+
fork_context: bool,
|
|
215
|
+
model: str | None,
|
|
216
|
+
reasoning_effort: str | None,
|
|
217
|
+
history: tuple[ConversationItem, ...],
|
|
218
|
+
) -> dict[str, object]:
|
|
219
|
+
builder = self._runtime_builder
|
|
220
|
+
if builder is None:
|
|
221
|
+
raise RuntimeError("spawn_agent is unavailable before runtime initialization")
|
|
222
|
+
|
|
223
|
+
initial_history = history if fork_context else ()
|
|
224
|
+
agent_id = uuid7_string()
|
|
225
|
+
runtime = builder(model, reasoning_effort, initial_history, agent_id)
|
|
226
|
+
worker_task = asyncio.create_task(runtime.run_forever())
|
|
227
|
+
nickname = self._next_nickname()
|
|
228
|
+
managed = ManagedAgent(
|
|
229
|
+
agent_id=agent_id,
|
|
230
|
+
runtime=runtime,
|
|
231
|
+
worker_task=worker_task,
|
|
232
|
+
nickname=nickname,
|
|
233
|
+
)
|
|
234
|
+
async with self._condition:
|
|
235
|
+
self._agents[agent_id] = managed
|
|
236
|
+
self._condition.notify_all()
|
|
237
|
+
|
|
238
|
+
initial_prompt = self._compose_prompt(message, items)
|
|
239
|
+
if initial_prompt:
|
|
240
|
+
await self.send_input(agent_id, initial_prompt, interrupt=False)
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
"agent_id": agent_id,
|
|
244
|
+
"nickname": nickname,
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async def send_input(
|
|
248
|
+
self,
|
|
249
|
+
agent_id: str,
|
|
250
|
+
prompt_text: str,
|
|
251
|
+
interrupt: bool,
|
|
252
|
+
) -> dict[str, object]:
|
|
253
|
+
managed = self._agents.get(agent_id)
|
|
254
|
+
if managed is None:
|
|
255
|
+
raise RuntimeError(f"unknown agent: {agent_id}")
|
|
256
|
+
if managed.state == "shutdown":
|
|
257
|
+
raise RuntimeError(f"agent is shutdown: {agent_id}")
|
|
258
|
+
|
|
259
|
+
submission_id, future = await managed.runtime.enqueue_user_turn(
|
|
260
|
+
prompt_text,
|
|
261
|
+
queue="steer" if interrupt else "enqueue",
|
|
262
|
+
)
|
|
263
|
+
managed.state = "running"
|
|
264
|
+
managed.completed_message = None
|
|
265
|
+
managed.error_message = None
|
|
266
|
+
managed.pending_submission_ids.add(submission_id)
|
|
267
|
+
asyncio.create_task(self._track_submission(managed, submission_id, future))
|
|
268
|
+
async with self._condition:
|
|
269
|
+
self._condition.notify_all()
|
|
270
|
+
return {"submission_id": submission_id}
|
|
271
|
+
|
|
272
|
+
async def resume_agent(self, agent_id: str) -> dict[str, object]:
|
|
273
|
+
managed = self._agents.get(agent_id)
|
|
274
|
+
if managed is None:
|
|
275
|
+
return {"status": "not_found"}
|
|
276
|
+
if managed.worker_task.done():
|
|
277
|
+
managed.worker_task = asyncio.create_task(managed.runtime.run_forever())
|
|
278
|
+
managed.state = "pending_init"
|
|
279
|
+
managed.completed_message = None
|
|
280
|
+
managed.error_message = None
|
|
281
|
+
async with self._condition:
|
|
282
|
+
self._condition.notify_all()
|
|
283
|
+
return {"status": self._status_payload(managed)}
|
|
284
|
+
|
|
285
|
+
async def close_agent(self, agent_id: str) -> dict[str, object]:
|
|
286
|
+
managed = self._agents.get(agent_id)
|
|
287
|
+
if managed is None:
|
|
288
|
+
return {"status": "not_found"}
|
|
289
|
+
previous_status = self._status_payload(managed)
|
|
290
|
+
if not managed.worker_task.done():
|
|
291
|
+
managed.runtime._agent_loop.interrupt_asap = True
|
|
292
|
+
await managed.runtime.shutdown()
|
|
293
|
+
await managed.worker_task
|
|
294
|
+
managed.state = "shutdown"
|
|
295
|
+
managed.pending_submission_ids.clear()
|
|
296
|
+
async with self._condition:
|
|
297
|
+
self._condition.notify_all()
|
|
298
|
+
return {"status": previous_status}
|
|
299
|
+
|
|
300
|
+
def _next_nickname(self) -> str:
|
|
301
|
+
if not self._available_nicknames:
|
|
302
|
+
self._available_nicknames = list(DEFAULT_AGENT_NICKNAME_CANDIDATES)
|
|
303
|
+
self._nickname_random.shuffle(self._available_nicknames)
|
|
304
|
+
return self._available_nicknames.pop()
|
|
305
|
+
|
|
306
|
+
async def wait_agents(
|
|
307
|
+
self,
|
|
308
|
+
agent_ids: list[str],
|
|
309
|
+
timeout_ms: int = 30_000,
|
|
310
|
+
) -> dict[str, object]:
|
|
311
|
+
timeout_seconds = max(timeout_ms, 1) / 1000.0
|
|
312
|
+
loop = asyncio.get_running_loop()
|
|
313
|
+
deadline = loop.time() + timeout_seconds
|
|
314
|
+
|
|
315
|
+
while True:
|
|
316
|
+
snapshot = {
|
|
317
|
+
agent_id: self._status_payload(self._agents.get(agent_id))
|
|
318
|
+
for agent_id in agent_ids
|
|
319
|
+
}
|
|
320
|
+
if any(self._is_final_status(status) for status in snapshot.values()):
|
|
321
|
+
return {"status": snapshot, "timed_out": False}
|
|
322
|
+
|
|
323
|
+
remaining = deadline - loop.time()
|
|
324
|
+
if remaining <= 0:
|
|
325
|
+
return {"status": {}, "timed_out": True}
|
|
326
|
+
|
|
327
|
+
async with self._condition:
|
|
328
|
+
try:
|
|
329
|
+
await asyncio.wait_for(self._condition.wait(), timeout=remaining)
|
|
330
|
+
except asyncio.TimeoutError:
|
|
331
|
+
return {"status": {}, "timed_out": True}
|
|
332
|
+
|
|
333
|
+
async def _track_submission(
|
|
334
|
+
self,
|
|
335
|
+
managed: ManagedAgent,
|
|
336
|
+
submission_id: str,
|
|
337
|
+
future: asyncio.Future[TurnResult | None],
|
|
338
|
+
) -> None:
|
|
339
|
+
try:
|
|
340
|
+
result = await future
|
|
341
|
+
except Exception as exc: # pragma: no cover - background safety
|
|
342
|
+
managed.error_message = f"{type(exc).__name__}: {exc}"
|
|
343
|
+
managed.state = "errored"
|
|
344
|
+
else:
|
|
345
|
+
managed.completed_message = None if result is None else result.output_text
|
|
346
|
+
managed.state = "completed"
|
|
347
|
+
finally:
|
|
348
|
+
managed.pending_submission_ids.discard(submission_id)
|
|
349
|
+
async with self._condition:
|
|
350
|
+
self._condition.notify_all()
|
|
351
|
+
|
|
352
|
+
def _compose_prompt(
|
|
353
|
+
self,
|
|
354
|
+
message: str | None,
|
|
355
|
+
items: list[dict[str, object]] | None,
|
|
356
|
+
) -> str:
|
|
357
|
+
parts: list[str] = []
|
|
358
|
+
if message:
|
|
359
|
+
parts.append(message.strip())
|
|
360
|
+
for item in items or []:
|
|
361
|
+
item_type = str(item.get("type", ""))
|
|
362
|
+
if item_type == "text":
|
|
363
|
+
text = str(item.get("text", "")).strip()
|
|
364
|
+
if text:
|
|
365
|
+
parts.append(text)
|
|
366
|
+
elif item_type == "image":
|
|
367
|
+
image_url = str(item.get("image_url", "")).strip()
|
|
368
|
+
if image_url:
|
|
369
|
+
parts.append(f"[image] {image_url}")
|
|
370
|
+
else:
|
|
371
|
+
parts.append(json.dumps(item, ensure_ascii=False))
|
|
372
|
+
return "\n\n".join(part for part in parts if part)
|
|
373
|
+
|
|
374
|
+
def _status_payload(self, managed: ManagedAgent | None) -> object:
|
|
375
|
+
if managed is None:
|
|
376
|
+
return "not_found"
|
|
377
|
+
if managed.error_message is not None:
|
|
378
|
+
return {"errored": managed.error_message}
|
|
379
|
+
if managed.state == "completed":
|
|
380
|
+
return {"completed": managed.completed_message}
|
|
381
|
+
if managed.state in {"pending_init", "running", "shutdown"}:
|
|
382
|
+
return managed.state
|
|
383
|
+
return managed.state
|
|
384
|
+
|
|
385
|
+
def _is_final_status(self, status: object) -> bool:
|
|
386
|
+
if isinstance(status, str):
|
|
387
|
+
return status in {"shutdown", "not_found"}
|
|
388
|
+
if isinstance(status, dict):
|
|
389
|
+
return "completed" in status or "errored" in status
|
|
390
|
+
return False
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
class RuntimeEnvironment:
|
|
394
|
+
def __init__(self) -> None:
|
|
395
|
+
self.plan_store = PlanStore()
|
|
396
|
+
self.subagent_manager = SubAgentManager()
|
|
397
|
+
self.request_user_input_manager = RequestUserInputManager()
|
|
398
|
+
self.request_permissions_manager = RequestPermissionsManager()
|
|
399
|
+
|
|
400
|
+
def configure_runtime_builder(self, builder: RuntimeBuilder | None) -> None:
|
|
401
|
+
self.subagent_manager.set_runtime_builder(builder)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
_RUNTIME_ENV = RuntimeEnvironment()
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def get_runtime_environment() -> RuntimeEnvironment:
|
|
408
|
+
return _RUNTIME_ENV
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Tool package for the Python Codex prototype.
|
|
2
|
+
|
|
3
|
+
This package groups the local tool abstractions and concrete tool
|
|
4
|
+
implementations that back `pycodex`.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .base_tool import BaseTool, Registry, ToolContext, ToolRegistry
|
|
8
|
+
from .apply_patch_tool import ApplyPatchTool
|
|
9
|
+
from .close_agent_tool import CloseAgentTool
|
|
10
|
+
from .code_mode_manager import CodeModeManager
|
|
11
|
+
from .exec_command_tool import ExecCommandTool
|
|
12
|
+
from .exec_tool import ExecTool
|
|
13
|
+
from .grep_files_tool import GrepFilesTool
|
|
14
|
+
from .list_dir_tool import ListDirTool
|
|
15
|
+
from .read_file_tool import ReadFileTool
|
|
16
|
+
from .request_permissions_tool import RequestPermissionsTool
|
|
17
|
+
from .request_user_input_tool import RequestUserInputTool
|
|
18
|
+
from .resume_agent_tool import ResumeAgentTool
|
|
19
|
+
from .send_input_tool import SendInputTool
|
|
20
|
+
from .shell_command_tool import ShellCommandTool
|
|
21
|
+
from .shell_tool import ShellTool
|
|
22
|
+
from .spawn_agent_tool import SpawnAgentTool
|
|
23
|
+
from .unified_exec_manager import UnifiedExecManager
|
|
24
|
+
from .update_plan_tool import UpdatePlanTool
|
|
25
|
+
from .view_image_tool import ViewImageTool
|
|
26
|
+
from .wait_agent_tool import WaitAgentTool
|
|
27
|
+
from .wait_tool import WaitTool
|
|
28
|
+
from .web_search_tool import WebSearchTool
|
|
29
|
+
from .write_stdin_tool import WriteStdinTool
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"ApplyPatchTool",
|
|
33
|
+
"BaseTool",
|
|
34
|
+
"CloseAgentTool",
|
|
35
|
+
"CodeModeManager",
|
|
36
|
+
"ExecTool",
|
|
37
|
+
"ExecCommandTool",
|
|
38
|
+
"GrepFilesTool",
|
|
39
|
+
"ListDirTool",
|
|
40
|
+
"ReadFileTool",
|
|
41
|
+
"Registry",
|
|
42
|
+
"RequestPermissionsTool",
|
|
43
|
+
"RequestUserInputTool",
|
|
44
|
+
"ResumeAgentTool",
|
|
45
|
+
"SendInputTool",
|
|
46
|
+
"ShellCommandTool",
|
|
47
|
+
"ShellTool",
|
|
48
|
+
"SpawnAgentTool",
|
|
49
|
+
"ToolContext",
|
|
50
|
+
"ToolRegistry",
|
|
51
|
+
"UnifiedExecManager",
|
|
52
|
+
"UpdatePlanTool",
|
|
53
|
+
"ViewImageTool",
|
|
54
|
+
"WaitAgentTool",
|
|
55
|
+
"WaitTool",
|
|
56
|
+
"WebSearchTool",
|
|
57
|
+
"WriteStdinTool",
|
|
58
|
+
]
|