bazaar-compute-node 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.
- bazaar_compute_node/__init__.py +3 -0
- bazaar_compute_node/app/__init__.py +1 -0
- bazaar_compute_node/app/application.py +398 -0
- bazaar_compute_node/app/attachments.py +154 -0
- bazaar_compute_node/app/command.py +342 -0
- bazaar_compute_node/app/config.py +121 -0
- bazaar_compute_node/app/registry.py +120 -0
- bazaar_compute_node/app/transport.py +264 -0
- bazaar_compute_node/app/windows_pipe.py +463 -0
- bazaar_compute_node/app/wrapper.py +63 -0
- bazaar_compute_node/bcc.py +524 -0
- bazaar_compute_node/cli.py +382 -0
- bazaar_compute_node/contrib/__init__.py +1 -0
- bazaar_compute_node/contrib/codex_app_server/__init__.py +63 -0
- bazaar_compute_node/contrib/codex_app_server/approval.py +168 -0
- bazaar_compute_node/contrib/codex_app_server/client.py +408 -0
- bazaar_compute_node/contrib/codex_app_server/events.py +431 -0
- bazaar_compute_node/contrib/codex_app_server/plugin.py +15 -0
- bazaar_compute_node/contrib/codex_app_server/process.py +583 -0
- bazaar_compute_node/contrib/codex_app_server/protocol.py +103 -0
- bazaar_compute_node/contrib/codex_app_server/runtime.py +513 -0
- bazaar_compute_node/contrib/logging/__init__.py +5 -0
- bazaar_compute_node/contrib/logging/audit.py +61 -0
- bazaar_compute_node/contrib/logging/plugin.py +11 -0
- bazaar_compute_node/contrib/sqlite/__init__.py +14 -0
- bazaar_compute_node/contrib/sqlite/codec.py +768 -0
- bazaar_compute_node/contrib/sqlite/database.py +282 -0
- bazaar_compute_node/contrib/sqlite/migrations.py +646 -0
- bazaar_compute_node/contrib/sqlite/plugin.py +11 -0
- bazaar_compute_node/contrib/sqlite/repository.py +1059 -0
- bazaar_compute_node/contrib/wecom/__init__.py +1 -0
- bazaar_compute_node/contrib/wecom/channel.py +960 -0
- bazaar_compute_node/contrib/wecom/markdown.py +146 -0
- bazaar_compute_node/contrib/wecom/plugin.py +29 -0
- bazaar_compute_node/core/__init__.py +5 -0
- bazaar_compute_node/core/approval.py +51 -0
- bazaar_compute_node/core/audit.py +101 -0
- bazaar_compute_node/core/channel.py +121 -0
- bazaar_compute_node/core/client.py +30 -0
- bazaar_compute_node/core/command.py +85 -0
- bazaar_compute_node/core/concurrency.py +29 -0
- bazaar_compute_node/core/correlation.py +48 -0
- bazaar_compute_node/core/instruction.py +224 -0
- bazaar_compute_node/core/lifecycle.py +48 -0
- bazaar_compute_node/core/models/__init__.py +63 -0
- bazaar_compute_node/core/models/entities.py +514 -0
- bazaar_compute_node/core/models/states.py +369 -0
- bazaar_compute_node/core/observability.py +47 -0
- bazaar_compute_node/core/orchestration/__init__.py +5 -0
- bazaar_compute_node/core/orchestration/command.py +614 -0
- bazaar_compute_node/core/orchestration/services.py +135 -0
- bazaar_compute_node/core/orchestration/session.py +891 -0
- bazaar_compute_node/core/orchestration/turn.py +451 -0
- bazaar_compute_node/core/outcomes.py +51 -0
- bazaar_compute_node/core/paths.py +19 -0
- bazaar_compute_node/core/runtime.py +118 -0
- bazaar_compute_node/core/storage.py +167 -0
- bazaar_compute_node-0.1.3.dist-info/METADATA +178 -0
- bazaar_compute_node-0.1.3.dist-info/RECORD +62 -0
- bazaar_compute_node-0.1.3.dist-info/WHEEL +4 -0
- bazaar_compute_node-0.1.3.dist-info/entry_points.txt +15 -0
- bazaar_compute_node-0.1.3.dist-info/licenses/LICENSE +613 -0
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
from dataclasses import dataclass, replace
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from time import time_ns
|
|
11
|
+
|
|
12
|
+
from ...core.approval import IApprovalHandler
|
|
13
|
+
from ...core.instruction import DeveloperInstructionContext
|
|
14
|
+
from ...core.lifecycle import IAsyncLifecycle
|
|
15
|
+
from ...core.models import (
|
|
16
|
+
RuntimeEventState,
|
|
17
|
+
RuntimeSession,
|
|
18
|
+
RuntimeTurn,
|
|
19
|
+
)
|
|
20
|
+
from ...core.outcomes import ProviderCallResult, ProviderCallStatus
|
|
21
|
+
from ...core.paths import resolve_workspace_dir
|
|
22
|
+
from ...core.runtime import (
|
|
23
|
+
IRuntime,
|
|
24
|
+
IRuntimeTurnStream,
|
|
25
|
+
RuntimeCommandContext,
|
|
26
|
+
RuntimeSandboxMode,
|
|
27
|
+
RuntimeSessionUnavailable,
|
|
28
|
+
)
|
|
29
|
+
from .client import (
|
|
30
|
+
CodexAppServerClient,
|
|
31
|
+
parse_thread_response,
|
|
32
|
+
parse_turn_response,
|
|
33
|
+
)
|
|
34
|
+
from .events import CodexTurnEventStream
|
|
35
|
+
from .process import JsonlProcessSpec, JsonlProcessSupervisor
|
|
36
|
+
from .protocol import (
|
|
37
|
+
CodexAppServerProtocolError,
|
|
38
|
+
JsonlProcessExited,
|
|
39
|
+
JsonlProcessNotRunning,
|
|
40
|
+
JsonlProtocolError,
|
|
41
|
+
JsonlRemoteError,
|
|
42
|
+
JsonlRequestTimeout,
|
|
43
|
+
JsonlTransportError,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
_INITIALIZE_ATTEMPTS = 2
|
|
47
|
+
_RESUME_ATTEMPTS = 2
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(slots=True)
|
|
51
|
+
class _CodexConnection:
|
|
52
|
+
supervisor: JsonlProcessSupervisor
|
|
53
|
+
client: CodexAppServerClient
|
|
54
|
+
workspace: Path
|
|
55
|
+
provider_thread_id: str
|
|
56
|
+
active_turn_id: str | None = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class CodexAppServerRuntime(IRuntime, IAsyncLifecycle):
|
|
60
|
+
"""Run one persistent Codex App Server process per bcn runtime session."""
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def name(self) -> str:
|
|
64
|
+
return "codex"
|
|
65
|
+
|
|
66
|
+
def environment_variable_names(self) -> tuple[str, ...]:
|
|
67
|
+
return (
|
|
68
|
+
"CODEX_HOME",
|
|
69
|
+
"CODEX_SQLITE_HOME",
|
|
70
|
+
"CODEX_CA_CERTIFICATE",
|
|
71
|
+
"SSL_CERT_FILE",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
def __init__(
|
|
75
|
+
self,
|
|
76
|
+
context: RuntimeCommandContext,
|
|
77
|
+
*,
|
|
78
|
+
executable: str = "codex",
|
|
79
|
+
model: str | None = None,
|
|
80
|
+
effort: str | None = None,
|
|
81
|
+
) -> None:
|
|
82
|
+
if not executable:
|
|
83
|
+
raise ValueError("executable must be a non-empty string")
|
|
84
|
+
if not context.node_id:
|
|
85
|
+
raise ValueError("runtime context node_id must be non-empty")
|
|
86
|
+
if model is not None and not model:
|
|
87
|
+
raise ValueError("model must be a non-empty string or None")
|
|
88
|
+
if effort is not None and not effort:
|
|
89
|
+
raise ValueError("effort must be a non-empty string or None")
|
|
90
|
+
self._context = context
|
|
91
|
+
self._executable = executable
|
|
92
|
+
self._model = model
|
|
93
|
+
self._effort = effort
|
|
94
|
+
self._connections: dict[str, _CodexConnection] = {}
|
|
95
|
+
self._logger = logging.getLogger("bazaar_compute_node.runtime.codex")
|
|
96
|
+
self._started = False
|
|
97
|
+
self._stopping = False
|
|
98
|
+
|
|
99
|
+
async def start(self, *, timeout: float) -> None:
|
|
100
|
+
del timeout
|
|
101
|
+
if self._stopping:
|
|
102
|
+
raise RuntimeError("Codex App Server runtime is stopping")
|
|
103
|
+
self._started = True
|
|
104
|
+
|
|
105
|
+
async def stop(self, *, timeout: float) -> None:
|
|
106
|
+
if self._stopping:
|
|
107
|
+
return
|
|
108
|
+
self._stopping = True
|
|
109
|
+
connections = tuple(self._connections.items())
|
|
110
|
+
self._connections.clear()
|
|
111
|
+
for _session_id, connection in connections:
|
|
112
|
+
try:
|
|
113
|
+
await connection.supervisor.stop(timeout=timeout)
|
|
114
|
+
except asyncio.CancelledError:
|
|
115
|
+
raise
|
|
116
|
+
except OSError, TimeoutError, JsonlTransportError:
|
|
117
|
+
continue
|
|
118
|
+
self._started = False
|
|
119
|
+
|
|
120
|
+
async def start_session(
|
|
121
|
+
self, session: RuntimeSession, *, timeout: float
|
|
122
|
+
) -> ProviderCallResult[RuntimeSession]:
|
|
123
|
+
self._ensure_started()
|
|
124
|
+
existing = self._connections.pop(session.id, None)
|
|
125
|
+
if existing is not None:
|
|
126
|
+
await self._stop_connection(existing, timeout=timeout)
|
|
127
|
+
connection: _CodexConnection | None = None
|
|
128
|
+
try:
|
|
129
|
+
connection = await self._open_connection(session, timeout=timeout)
|
|
130
|
+
response = await connection.client.start_thread(
|
|
131
|
+
DeveloperInstructionContext(
|
|
132
|
+
node_id=self._context.node_id,
|
|
133
|
+
runtime_session_id=session.id,
|
|
134
|
+
runtime=session.runtime,
|
|
135
|
+
workspace=str(connection.workspace),
|
|
136
|
+
).render(),
|
|
137
|
+
model=self._model,
|
|
138
|
+
cwd=connection.workspace,
|
|
139
|
+
timeout=timeout,
|
|
140
|
+
)
|
|
141
|
+
thread = parse_thread_response(response)
|
|
142
|
+
connection.provider_thread_id = thread.thread_id
|
|
143
|
+
self._connections[session.id] = connection
|
|
144
|
+
return ProviderCallResult(
|
|
145
|
+
status=ProviderCallStatus.CONFIRMED,
|
|
146
|
+
value=replace(
|
|
147
|
+
session,
|
|
148
|
+
provider_thread_id=thread.thread_id,
|
|
149
|
+
updated_at_ms=_now_ms(),
|
|
150
|
+
),
|
|
151
|
+
receipt={"provider_thread_id": thread.thread_id},
|
|
152
|
+
)
|
|
153
|
+
except asyncio.CancelledError:
|
|
154
|
+
if connection is not None:
|
|
155
|
+
await self._stop_connection(connection, timeout=timeout)
|
|
156
|
+
raise
|
|
157
|
+
except Exception as error: # noqa: BLE001
|
|
158
|
+
if connection is not None:
|
|
159
|
+
await self._stop_connection(connection, timeout=timeout)
|
|
160
|
+
return _provider_result(error)
|
|
161
|
+
|
|
162
|
+
async def resume_session(
|
|
163
|
+
self, session: RuntimeSession, *, timeout: float
|
|
164
|
+
) -> ProviderCallResult[RuntimeSession]:
|
|
165
|
+
self._ensure_started()
|
|
166
|
+
provider_thread_id = session.provider_thread_id
|
|
167
|
+
if provider_thread_id is None:
|
|
168
|
+
return ProviderCallResult(
|
|
169
|
+
status=ProviderCallStatus.FAILED,
|
|
170
|
+
error_kind="provider_failed",
|
|
171
|
+
error_message="cannot resume a runtime session without a provider thread",
|
|
172
|
+
)
|
|
173
|
+
connection = self._connections.pop(session.id, None)
|
|
174
|
+
if connection is not None and not connection.supervisor.is_running:
|
|
175
|
+
await self._stop_connection(connection, timeout=timeout)
|
|
176
|
+
connection = None
|
|
177
|
+
for attempt in range(_RESUME_ATTEMPTS):
|
|
178
|
+
if connection is None:
|
|
179
|
+
try:
|
|
180
|
+
connection = await self._open_connection(session, timeout=timeout)
|
|
181
|
+
except asyncio.CancelledError:
|
|
182
|
+
raise
|
|
183
|
+
except Exception as error: # noqa: BLE001
|
|
184
|
+
return _provider_result(error)
|
|
185
|
+
try:
|
|
186
|
+
response = await connection.client.resume_thread(
|
|
187
|
+
provider_thread_id,
|
|
188
|
+
model=self._model,
|
|
189
|
+
cwd=connection.workspace,
|
|
190
|
+
timeout=timeout,
|
|
191
|
+
)
|
|
192
|
+
thread = parse_thread_response(response)
|
|
193
|
+
if thread.thread_id != provider_thread_id:
|
|
194
|
+
raise CodexAppServerProtocolError(
|
|
195
|
+
"thread/resume returned a different provider thread"
|
|
196
|
+
)
|
|
197
|
+
connection.provider_thread_id = thread.thread_id
|
|
198
|
+
self._connections[session.id] = connection
|
|
199
|
+
if attempt > 0:
|
|
200
|
+
self._logger.info(
|
|
201
|
+
"%s",
|
|
202
|
+
json.dumps(
|
|
203
|
+
{
|
|
204
|
+
"event_name": "runtime.process.resume.retry_succeeded",
|
|
205
|
+
"metadata": {
|
|
206
|
+
"attempt": attempt + 1,
|
|
207
|
+
"session_id": session.bcn_session_id,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
separators=(",", ":"),
|
|
211
|
+
sort_keys=True,
|
|
212
|
+
),
|
|
213
|
+
)
|
|
214
|
+
return ProviderCallResult(
|
|
215
|
+
status=ProviderCallStatus.CONFIRMED,
|
|
216
|
+
value=replace(
|
|
217
|
+
session,
|
|
218
|
+
provider_thread_id=thread.thread_id,
|
|
219
|
+
updated_at_ms=_now_ms(),
|
|
220
|
+
),
|
|
221
|
+
receipt={"provider_thread_id": thread.thread_id},
|
|
222
|
+
)
|
|
223
|
+
except asyncio.CancelledError:
|
|
224
|
+
await self._stop_connection(connection, timeout=timeout)
|
|
225
|
+
raise
|
|
226
|
+
except (
|
|
227
|
+
JsonlProcessExited,
|
|
228
|
+
JsonlProcessNotRunning,
|
|
229
|
+
JsonlRequestTimeout,
|
|
230
|
+
) as error:
|
|
231
|
+
await self._stop_connection(connection, timeout=timeout)
|
|
232
|
+
connection = None
|
|
233
|
+
if attempt + 1 == _RESUME_ATTEMPTS:
|
|
234
|
+
self._logger.error(
|
|
235
|
+
"%s",
|
|
236
|
+
json.dumps(
|
|
237
|
+
{
|
|
238
|
+
"event_name": "runtime.process.resume.retry_exhausted",
|
|
239
|
+
"metadata": {
|
|
240
|
+
"attempt": attempt + 1,
|
|
241
|
+
"error_type": type(error).__name__,
|
|
242
|
+
"session_id": session.bcn_session_id,
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
separators=(",", ":"),
|
|
246
|
+
sort_keys=True,
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
return _provider_result(error)
|
|
250
|
+
self._logger.warning(
|
|
251
|
+
"%s",
|
|
252
|
+
json.dumps(
|
|
253
|
+
{
|
|
254
|
+
"event_name": "runtime.process.resume.retrying",
|
|
255
|
+
"metadata": {
|
|
256
|
+
"attempt": attempt + 1,
|
|
257
|
+
"error_type": type(error).__name__,
|
|
258
|
+
"next_attempt": attempt + 2,
|
|
259
|
+
"session_id": session.bcn_session_id,
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
separators=(",", ":"),
|
|
263
|
+
sort_keys=True,
|
|
264
|
+
),
|
|
265
|
+
)
|
|
266
|
+
except Exception as error: # noqa: BLE001
|
|
267
|
+
await self._stop_connection(connection, timeout=timeout)
|
|
268
|
+
return _provider_result(error)
|
|
269
|
+
raise AssertionError("Codex resume retry loop did not return")
|
|
270
|
+
|
|
271
|
+
async def start_turn(
|
|
272
|
+
self,
|
|
273
|
+
session: RuntimeSession,
|
|
274
|
+
turn: RuntimeTurn,
|
|
275
|
+
input_text: str,
|
|
276
|
+
approval_handler: IApprovalHandler,
|
|
277
|
+
*,
|
|
278
|
+
timeout: float,
|
|
279
|
+
) -> IRuntimeTurnStream:
|
|
280
|
+
self._ensure_started()
|
|
281
|
+
connection = self._connections.get(session.id)
|
|
282
|
+
if connection is None or not connection.supervisor.is_running:
|
|
283
|
+
raise RuntimeSessionUnavailable("Codex App Server process is not running")
|
|
284
|
+
if connection.active_turn_id is not None:
|
|
285
|
+
raise RuntimeError(
|
|
286
|
+
f"runtime session already has an active turn: {connection.active_turn_id}"
|
|
287
|
+
)
|
|
288
|
+
provider_turn_id: str | None = None
|
|
289
|
+
initial_error: BaseException | None = None
|
|
290
|
+
initial_error_kind = "provider_unknown"
|
|
291
|
+
initial_error_state = RuntimeEventState.UNKNOWN
|
|
292
|
+
try:
|
|
293
|
+
if self._context.sandbox_mode is RuntimeSandboxMode.WORKSPACE_WRITE:
|
|
294
|
+
sandbox_policy: dict[str, object] = {
|
|
295
|
+
"type": "workspaceWrite",
|
|
296
|
+
"writableRoots": [str(connection.workspace)],
|
|
297
|
+
"networkAccess": self._context.network_access,
|
|
298
|
+
}
|
|
299
|
+
elif self._context.sandbox_mode is RuntimeSandboxMode.DANGER_FULL_ACCESS:
|
|
300
|
+
sandbox_policy = {"type": "dangerFullAccess"}
|
|
301
|
+
else:
|
|
302
|
+
raise AssertionError(
|
|
303
|
+
f"unsupported runtime sandbox mode: {self._context.sandbox_mode}"
|
|
304
|
+
)
|
|
305
|
+
response = await connection.client.start_turn(
|
|
306
|
+
connection.provider_thread_id,
|
|
307
|
+
input_text,
|
|
308
|
+
client_user_message_id=turn.client_user_message_id,
|
|
309
|
+
model=self._model,
|
|
310
|
+
effort=self._effort,
|
|
311
|
+
cwd=connection.workspace,
|
|
312
|
+
sandbox_policy=sandbox_policy,
|
|
313
|
+
timeout=timeout,
|
|
314
|
+
)
|
|
315
|
+
provider_turn = parse_turn_response(response)
|
|
316
|
+
provider_turn_id = provider_turn.turn_id
|
|
317
|
+
except asyncio.CancelledError:
|
|
318
|
+
raise
|
|
319
|
+
except Exception as error: # noqa: BLE001
|
|
320
|
+
initial_error = error
|
|
321
|
+
if isinstance(error, JsonlRemoteError):
|
|
322
|
+
initial_error_kind = "provider_failed"
|
|
323
|
+
initial_error_state = RuntimeEventState.FAILED
|
|
324
|
+
elif isinstance(error, CodexAppServerProtocolError):
|
|
325
|
+
initial_error_kind = "protocol"
|
|
326
|
+
initial_error_state = RuntimeEventState.FAILED
|
|
327
|
+
connection.active_turn_id = turn.turn_id
|
|
328
|
+
return CodexTurnEventStream(
|
|
329
|
+
connection.supervisor,
|
|
330
|
+
node_id=self._context.node_id,
|
|
331
|
+
runtime=session.runtime,
|
|
332
|
+
session_id=session.bcn_session_id,
|
|
333
|
+
runtime_session_id=session.id,
|
|
334
|
+
turn_id=turn.turn_id,
|
|
335
|
+
provider_thread_id=connection.provider_thread_id,
|
|
336
|
+
provider_turn_id=provider_turn_id,
|
|
337
|
+
approval_handler=approval_handler,
|
|
338
|
+
approval_timeout=timeout,
|
|
339
|
+
initial_error=initial_error,
|
|
340
|
+
initial_error_kind=initial_error_kind,
|
|
341
|
+
initial_error_state=initial_error_state,
|
|
342
|
+
on_closed=lambda: self._clear_active_turn(session.id, turn.turn_id),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
async def interrupt_turn(
|
|
346
|
+
self,
|
|
347
|
+
session: RuntimeSession,
|
|
348
|
+
turn: RuntimeTurn,
|
|
349
|
+
*,
|
|
350
|
+
timeout: float,
|
|
351
|
+
) -> ProviderCallResult[RuntimeTurn]:
|
|
352
|
+
self._ensure_started()
|
|
353
|
+
connection = self._connections.get(session.id)
|
|
354
|
+
provider_turn_id = turn.provider_turn_id
|
|
355
|
+
if connection is None or provider_turn_id is None:
|
|
356
|
+
return ProviderCallResult(
|
|
357
|
+
status=ProviderCallStatus.FAILED,
|
|
358
|
+
error_kind="provider_failed",
|
|
359
|
+
error_message="runtime turn has no active provider binding",
|
|
360
|
+
)
|
|
361
|
+
try:
|
|
362
|
+
await connection.client.interrupt_turn(
|
|
363
|
+
connection.provider_thread_id,
|
|
364
|
+
provider_turn_id,
|
|
365
|
+
timeout=timeout,
|
|
366
|
+
)
|
|
367
|
+
except asyncio.CancelledError:
|
|
368
|
+
raise
|
|
369
|
+
except Exception as error: # noqa: BLE001
|
|
370
|
+
result = _provider_result(error)
|
|
371
|
+
return ProviderCallResult(
|
|
372
|
+
status=result.status,
|
|
373
|
+
error_kind=result.error_kind,
|
|
374
|
+
error_message=result.error_message,
|
|
375
|
+
)
|
|
376
|
+
return ProviderCallResult(
|
|
377
|
+
status=ProviderCallStatus.QUEUED,
|
|
378
|
+
value=turn,
|
|
379
|
+
receipt={
|
|
380
|
+
"provider_thread_id": connection.provider_thread_id,
|
|
381
|
+
"provider_turn_id": provider_turn_id,
|
|
382
|
+
},
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
async def stop_session(
|
|
386
|
+
self, session: RuntimeSession, *, timeout: float
|
|
387
|
+
) -> ProviderCallResult[RuntimeSession]:
|
|
388
|
+
connection = self._connections.pop(session.id, None)
|
|
389
|
+
if connection is None:
|
|
390
|
+
return ProviderCallResult(
|
|
391
|
+
status=ProviderCallStatus.CONFIRMED,
|
|
392
|
+
value=session,
|
|
393
|
+
)
|
|
394
|
+
try:
|
|
395
|
+
await connection.supervisor.stop(timeout=timeout)
|
|
396
|
+
except asyncio.CancelledError:
|
|
397
|
+
raise
|
|
398
|
+
except Exception as error: # noqa: BLE001
|
|
399
|
+
return _provider_result(error)
|
|
400
|
+
return ProviderCallResult(
|
|
401
|
+
status=ProviderCallStatus.CONFIRMED,
|
|
402
|
+
value=session,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
async def _open_connection(
|
|
406
|
+
self,
|
|
407
|
+
session: RuntimeSession,
|
|
408
|
+
*,
|
|
409
|
+
timeout: float,
|
|
410
|
+
) -> _CodexConnection:
|
|
411
|
+
executable = shutil.which(self._executable)
|
|
412
|
+
if executable is None:
|
|
413
|
+
raise FileNotFoundError(f"Codex executable not found: {self._executable}")
|
|
414
|
+
workspace = resolve_workspace_dir(session.workspace_id)
|
|
415
|
+
await asyncio.to_thread(
|
|
416
|
+
workspace.mkdir, parents=True, exist_ok=True, mode=0o700
|
|
417
|
+
)
|
|
418
|
+
if os.name != "nt":
|
|
419
|
+
await asyncio.to_thread(workspace.chmod, 0o700)
|
|
420
|
+
environment = dict(self._context.environment_for_session(session))
|
|
421
|
+
for attempt in range(_INITIALIZE_ATTEMPTS):
|
|
422
|
+
supervisor = JsonlProcessSupervisor(
|
|
423
|
+
JsonlProcessSpec(
|
|
424
|
+
executable=executable,
|
|
425
|
+
arguments=("app-server", "--stdio"),
|
|
426
|
+
cwd=workspace,
|
|
427
|
+
environment=environment,
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
client = CodexAppServerClient(supervisor)
|
|
431
|
+
await supervisor.start(timeout=self._context.startup_timeout_seconds)
|
|
432
|
+
try:
|
|
433
|
+
await client.initialize(
|
|
434
|
+
client_info=self._context.client_info,
|
|
435
|
+
timeout=self._context.startup_timeout_seconds,
|
|
436
|
+
)
|
|
437
|
+
except JsonlRequestTimeout:
|
|
438
|
+
await supervisor.stop(timeout=timeout)
|
|
439
|
+
if attempt + 1 == _INITIALIZE_ATTEMPTS:
|
|
440
|
+
raise
|
|
441
|
+
continue
|
|
442
|
+
except BaseException:
|
|
443
|
+
await supervisor.stop(timeout=timeout)
|
|
444
|
+
raise
|
|
445
|
+
return _CodexConnection(
|
|
446
|
+
supervisor=supervisor,
|
|
447
|
+
client=client,
|
|
448
|
+
workspace=workspace,
|
|
449
|
+
provider_thread_id=session.provider_thread_id or "",
|
|
450
|
+
)
|
|
451
|
+
raise AssertionError("Codex initialization retry loop did not return")
|
|
452
|
+
|
|
453
|
+
async def _stop_connection(
|
|
454
|
+
self,
|
|
455
|
+
connection: _CodexConnection,
|
|
456
|
+
*,
|
|
457
|
+
timeout: float,
|
|
458
|
+
) -> None:
|
|
459
|
+
try:
|
|
460
|
+
await connection.supervisor.stop(timeout=timeout)
|
|
461
|
+
except asyncio.CancelledError:
|
|
462
|
+
raise
|
|
463
|
+
except OSError, TimeoutError, JsonlTransportError:
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
def _clear_active_turn(self, session_id: str, turn_id: str) -> None:
|
|
467
|
+
connection = self._connections.get(session_id)
|
|
468
|
+
if connection is not None and connection.active_turn_id == turn_id:
|
|
469
|
+
connection.active_turn_id = None
|
|
470
|
+
|
|
471
|
+
def _ensure_started(self) -> None:
|
|
472
|
+
if not self._started or self._stopping:
|
|
473
|
+
raise RuntimeError("Codex App Server runtime is not started")
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def _provider_result(error: BaseException) -> ProviderCallResult[RuntimeSession]:
|
|
477
|
+
if isinstance(
|
|
478
|
+
error, (JsonlProcessExited, JsonlProcessNotRunning, JsonlRequestTimeout)
|
|
479
|
+
):
|
|
480
|
+
return ProviderCallResult(
|
|
481
|
+
status=ProviderCallStatus.UNKNOWN,
|
|
482
|
+
error_kind="provider_unknown",
|
|
483
|
+
error_message=_safe_error_message(error),
|
|
484
|
+
)
|
|
485
|
+
if isinstance(error, (JsonlProtocolError, CodexAppServerProtocolError)):
|
|
486
|
+
return ProviderCallResult(
|
|
487
|
+
status=ProviderCallStatus.FAILED,
|
|
488
|
+
error_kind="protocol",
|
|
489
|
+
error_message=_safe_error_message(error),
|
|
490
|
+
)
|
|
491
|
+
if isinstance(error, JsonlRemoteError):
|
|
492
|
+
return ProviderCallResult(
|
|
493
|
+
status=ProviderCallStatus.FAILED,
|
|
494
|
+
error_kind="provider_failed",
|
|
495
|
+
error_message=_safe_error_message(error),
|
|
496
|
+
)
|
|
497
|
+
return ProviderCallResult(
|
|
498
|
+
status=ProviderCallStatus.FAILED,
|
|
499
|
+
error_kind="provider_failed",
|
|
500
|
+
error_message=_safe_error_message(error),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _safe_error_message(error: BaseException) -> str:
|
|
505
|
+
message = str(error).strip()
|
|
506
|
+
return message or type(error).__name__
|
|
507
|
+
|
|
508
|
+
|
|
509
|
+
def _now_ms() -> int:
|
|
510
|
+
return time_ns() // 1_000_000
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
__all__ = ["CodexAppServerRuntime"]
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from dataclasses import fields
|
|
6
|
+
|
|
7
|
+
from ...core.audit import AuditEvent
|
|
8
|
+
from ...core.observability import IAudit, LogLevel
|
|
9
|
+
|
|
10
|
+
_LOG_LEVELS = {
|
|
11
|
+
LogLevel.DEBUG: logging.DEBUG,
|
|
12
|
+
LogLevel.INFO: logging.INFO,
|
|
13
|
+
LogLevel.WARNING: logging.WARNING,
|
|
14
|
+
LogLevel.ERROR: logging.ERROR,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LoggingAudit(IAudit):
|
|
19
|
+
"""Emit sanitized audit events through the process logging pipeline."""
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def name(self) -> str:
|
|
23
|
+
return "logging"
|
|
24
|
+
|
|
25
|
+
def __init__(self, logger: logging.Logger | None = None) -> None:
|
|
26
|
+
if logger is None:
|
|
27
|
+
logger = logging.getLogger("bazaar_compute_node.audit")
|
|
28
|
+
if not logger.handlers:
|
|
29
|
+
logger.addHandler(logging.StreamHandler())
|
|
30
|
+
logger.setLevel(logging.INFO)
|
|
31
|
+
logger.propagate = False
|
|
32
|
+
self._logger = logger
|
|
33
|
+
|
|
34
|
+
async def append(self, event: AuditEvent, *, timeout: float) -> None:
|
|
35
|
+
del timeout
|
|
36
|
+
correlation = {
|
|
37
|
+
field.name: value
|
|
38
|
+
for field in fields(event.correlation)
|
|
39
|
+
if (value := getattr(event.correlation, field.name)) is not None
|
|
40
|
+
}
|
|
41
|
+
payload: dict[str, object] = {
|
|
42
|
+
"event_name": event.event_name,
|
|
43
|
+
"state": event.state.value,
|
|
44
|
+
"created_at_ms": event.created_at_ms,
|
|
45
|
+
"correlation": correlation,
|
|
46
|
+
"metadata": dict(event.metadata),
|
|
47
|
+
}
|
|
48
|
+
for key, value in (
|
|
49
|
+
("duration_ms", event.duration_ms),
|
|
50
|
+
("error_kind", event.error_kind.value if event.error_kind else None),
|
|
51
|
+
("error_type", event.error_type),
|
|
52
|
+
("error_message", event.error_message),
|
|
53
|
+
("traceback_ref", event.traceback_ref),
|
|
54
|
+
):
|
|
55
|
+
if value is not None:
|
|
56
|
+
payload[key] = value
|
|
57
|
+
self._logger.log(
|
|
58
|
+
_LOG_LEVELS[event.level],
|
|
59
|
+
"%s",
|
|
60
|
+
json.dumps(payload, separators=(",", ":"), sort_keys=True, default=str),
|
|
61
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""SQLite storage foundation for persistent node state."""
|
|
2
|
+
|
|
3
|
+
from .database import NodeIdentityError, SqliteDatabase
|
|
4
|
+
from .migrations import (
|
|
5
|
+
MigrationChecksumError,
|
|
6
|
+
MigrationError,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"MigrationChecksumError",
|
|
11
|
+
"MigrationError",
|
|
12
|
+
"NodeIdentityError",
|
|
13
|
+
"SqliteDatabase",
|
|
14
|
+
]
|