bazaar-compute-node 0.1.3__tar.gz → 0.1.5__tar.gz
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-0.1.3 → bazaar_compute_node-0.1.5}/PKG-INFO +1 -1
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/pyproject.toml +1 -1
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/pyproject.toml.orig +1 -1
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/__init__.py +4 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/client.py +34 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/events.py +8 -33
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/runtime.py +51 -2
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/sqlite/codec.py +0 -67
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/sqlite/database.py +34 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/sqlite/migrations.py +15 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/sqlite/repository.py +0 -210
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/models/entities.py +6 -22
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/orchestration/session.py +107 -4
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/orchestration/turn.py +104 -54
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/runtime.py +16 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/storage.py +0 -5
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/LICENSE +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/README.md +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/__init__.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/__init__.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/application.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/attachments.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/command.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/config.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/registry.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/transport.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/wrapper.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/bcc.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/cli.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/__init__.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/approval.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/plugin.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/process.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/protocol.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/wecom/channel.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/__init__.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/approval.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/audit.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/channel.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/client.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/command.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/concurrency.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/correlation.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/instruction.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/lifecycle.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/models/__init__.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/models/states.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/observability.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/orchestration/command.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/outcomes.py +0 -0
- {bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/paths.py +0 -0
|
@@ -11,10 +11,12 @@ from .client import (
|
|
|
11
11
|
build_thread_start_params,
|
|
12
12
|
build_turn_interrupt_params,
|
|
13
13
|
build_turn_start_params,
|
|
14
|
+
build_turn_steer_params,
|
|
14
15
|
parse_error_notification,
|
|
15
16
|
parse_thread_response,
|
|
16
17
|
parse_turn_notification,
|
|
17
18
|
parse_turn_response,
|
|
19
|
+
parse_turn_steer_response,
|
|
18
20
|
)
|
|
19
21
|
from .events import CodexTurnEventStream
|
|
20
22
|
from .process import (
|
|
@@ -56,8 +58,10 @@ __all__ = [
|
|
|
56
58
|
"build_thread_start_params",
|
|
57
59
|
"build_turn_interrupt_params",
|
|
58
60
|
"build_turn_start_params",
|
|
61
|
+
"build_turn_steer_params",
|
|
59
62
|
"parse_error_notification",
|
|
60
63
|
"parse_thread_response",
|
|
61
64
|
"parse_turn_notification",
|
|
62
65
|
"parse_turn_response",
|
|
66
|
+
"parse_turn_steer_response",
|
|
63
67
|
]
|
|
@@ -153,6 +153,21 @@ def build_turn_interrupt_params(thread_id: str, turn_id: str) -> dict[str, objec
|
|
|
153
153
|
return {"threadId": thread_id, "turnId": turn_id}
|
|
154
154
|
|
|
155
155
|
|
|
156
|
+
def build_turn_steer_params(
|
|
157
|
+
thread_id: str,
|
|
158
|
+
turn_id: str,
|
|
159
|
+
input_text: str,
|
|
160
|
+
) -> dict[str, object]:
|
|
161
|
+
_validate_non_empty_string("thread_id", thread_id)
|
|
162
|
+
_validate_non_empty_string("turn_id", turn_id)
|
|
163
|
+
_validate_non_empty_string("input_text", input_text)
|
|
164
|
+
return {
|
|
165
|
+
"threadId": thread_id,
|
|
166
|
+
"expectedTurnId": turn_id,
|
|
167
|
+
"input": [{"type": "text", "text": input_text}],
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
156
171
|
class CodexAppServerClient:
|
|
157
172
|
"""Typed facade over the adapter-local Codex JSONL supervisor."""
|
|
158
173
|
|
|
@@ -270,6 +285,20 @@ class CodexAppServerClient:
|
|
|
270
285
|
timeout=timeout,
|
|
271
286
|
)
|
|
272
287
|
|
|
288
|
+
async def steer_turn(
|
|
289
|
+
self,
|
|
290
|
+
thread_id: str,
|
|
291
|
+
turn_id: str,
|
|
292
|
+
input_text: str,
|
|
293
|
+
*,
|
|
294
|
+
timeout: float,
|
|
295
|
+
) -> JsonlMessage:
|
|
296
|
+
return await self.supervisor.request(
|
|
297
|
+
"turn/steer",
|
|
298
|
+
build_turn_steer_params(thread_id, turn_id, input_text),
|
|
299
|
+
timeout=timeout,
|
|
300
|
+
)
|
|
301
|
+
|
|
273
302
|
async def receive(self, *, timeout: float | None = None) -> JsonlMessage:
|
|
274
303
|
return await self.supervisor.receive(timeout=timeout)
|
|
275
304
|
|
|
@@ -290,6 +319,11 @@ def parse_turn_response(response: Mapping[str, object]) -> CodexTurnInfo:
|
|
|
290
319
|
return _parse_turn(_require_mapping(result, "turn"))
|
|
291
320
|
|
|
292
321
|
|
|
322
|
+
def parse_turn_steer_response(response: Mapping[str, object]) -> str:
|
|
323
|
+
result = _require_mapping(response, "result")
|
|
324
|
+
return _require_text(result, "turnId", "result.turnId")
|
|
325
|
+
|
|
326
|
+
|
|
293
327
|
def parse_turn_notification(message: Mapping[str, object]) -> tuple[str, CodexTurnInfo]:
|
|
294
328
|
params = _require_mapping(message, "params")
|
|
295
329
|
thread_id = _require_text(params, "threadId", "params.threadId")
|
|
@@ -4,7 +4,6 @@ import asyncio
|
|
|
4
4
|
from collections.abc import Callable, Mapping
|
|
5
5
|
from time import time_ns
|
|
6
6
|
from typing import Self, cast
|
|
7
|
-
from uuid import uuid7
|
|
8
7
|
|
|
9
8
|
from ...core.approval import IApprovalHandler
|
|
10
9
|
from ...core.models import (
|
|
@@ -21,12 +20,6 @@ from .approval import (
|
|
|
21
20
|
parse_approval_request,
|
|
22
21
|
)
|
|
23
22
|
|
|
24
|
-
_DURABLE_ITEM_METHODS = {
|
|
25
|
-
"item/started",
|
|
26
|
-
"item/completed",
|
|
27
|
-
"item/autoApprovalReview/started",
|
|
28
|
-
"item/autoApprovalReview/completed",
|
|
29
|
-
}
|
|
30
23
|
_STREAM_EVENT_KINDS = {
|
|
31
24
|
"item/agentMessage/delta": StreamEventKind.AGENT_MESSAGE_DELTA,
|
|
32
25
|
"item/plan/delta": StreamEventKind.PLAN_DELTA,
|
|
@@ -56,8 +49,6 @@ class CodexTurnEventStream(IRuntimeTurnStream):
|
|
|
56
49
|
self,
|
|
57
50
|
supervisor: JsonlProcessSupervisor,
|
|
58
51
|
*,
|
|
59
|
-
node_id: str,
|
|
60
|
-
runtime: str,
|
|
61
52
|
session_id: str,
|
|
62
53
|
runtime_session_id: str,
|
|
63
54
|
turn_id: str,
|
|
@@ -71,8 +62,6 @@ class CodexTurnEventStream(IRuntimeTurnStream):
|
|
|
71
62
|
on_closed: Callable[[], None] | None = None,
|
|
72
63
|
) -> None:
|
|
73
64
|
self._supervisor = supervisor
|
|
74
|
-
self._node_id = node_id
|
|
75
|
-
self._runtime = runtime
|
|
76
65
|
self._session_id = session_id
|
|
77
66
|
self._runtime_session_id = runtime_session_id
|
|
78
67
|
self._turn_id = turn_id
|
|
@@ -217,7 +206,6 @@ class CodexTurnEventStream(IRuntimeTurnStream):
|
|
|
217
206
|
return self._event(
|
|
218
207
|
event_name="codex.turn.error",
|
|
219
208
|
state=RuntimeEventState.STARTED,
|
|
220
|
-
error_type=error.error_type,
|
|
221
209
|
error_message=error.message,
|
|
222
210
|
metadata=metadata,
|
|
223
211
|
)
|
|
@@ -231,9 +219,14 @@ class CodexTurnEventStream(IRuntimeTurnStream):
|
|
|
231
219
|
return None
|
|
232
220
|
if method == "item/reasoning/summaryPartAdded":
|
|
233
221
|
return None
|
|
234
|
-
if method
|
|
235
|
-
|
|
236
|
-
|
|
222
|
+
if method in {
|
|
223
|
+
"item/started",
|
|
224
|
+
"item/completed",
|
|
225
|
+
"item/autoApprovalReview/started",
|
|
226
|
+
"item/autoApprovalReview/completed",
|
|
227
|
+
}:
|
|
228
|
+
return None
|
|
229
|
+
if method == "turn/progress" or method.startswith("item/"):
|
|
237
230
|
stream_id = params.get("itemId")
|
|
238
231
|
if not isinstance(stream_id, str) or not stream_id:
|
|
239
232
|
stream_id = None
|
|
@@ -255,15 +248,6 @@ class CodexTurnEventStream(IRuntimeTurnStream):
|
|
|
255
248
|
stream_id=stream_id,
|
|
256
249
|
content=content,
|
|
257
250
|
)
|
|
258
|
-
if method == "turn/started" or method in _DURABLE_ITEM_METHODS:
|
|
259
|
-
metadata = self._provider_metadata(method, params)
|
|
260
|
-
if provider_turn_id is not None:
|
|
261
|
-
metadata["provider_turn_id"] = provider_turn_id
|
|
262
|
-
return self._event(
|
|
263
|
-
event_name="codex.turn.progress",
|
|
264
|
-
state=RuntimeEventState.STARTED,
|
|
265
|
-
metadata=metadata,
|
|
266
|
-
)
|
|
267
251
|
return None
|
|
268
252
|
|
|
269
253
|
def _event(
|
|
@@ -272,25 +256,16 @@ class CodexTurnEventStream(IRuntimeTurnStream):
|
|
|
272
256
|
event_name: str,
|
|
273
257
|
state: RuntimeEventState,
|
|
274
258
|
error_kind: str | None = None,
|
|
275
|
-
error_type: str | None = None,
|
|
276
259
|
error_message: str | None = None,
|
|
277
260
|
metadata: Mapping[str, object] | None = None,
|
|
278
261
|
) -> RuntimeEvent:
|
|
279
262
|
return RuntimeEvent(
|
|
280
|
-
event_seq=0,
|
|
281
|
-
event_id=str(uuid7()),
|
|
282
263
|
created_at_ms=time_ns() // 1_000_000,
|
|
283
|
-
level="error" if error_kind else "info",
|
|
284
264
|
event_name=event_name,
|
|
285
265
|
state=state,
|
|
286
|
-
node_id=self._node_id,
|
|
287
|
-
bcn_session_id=self._session_id,
|
|
288
|
-
runtime_session_id=self._runtime_session_id,
|
|
289
266
|
turn_id=self._turn_id,
|
|
290
267
|
error_kind=error_kind,
|
|
291
|
-
error_type=error_type,
|
|
292
268
|
error_message=error_message,
|
|
293
|
-
runtime=self._runtime,
|
|
294
269
|
metadata=dict(metadata or {}),
|
|
295
270
|
)
|
|
296
271
|
|
|
@@ -30,6 +30,7 @@ from .client import (
|
|
|
30
30
|
CodexAppServerClient,
|
|
31
31
|
parse_thread_response,
|
|
32
32
|
parse_turn_response,
|
|
33
|
+
parse_turn_steer_response,
|
|
33
34
|
)
|
|
34
35
|
from .events import CodexTurnEventStream
|
|
35
36
|
from .process import JsonlProcessSpec, JsonlProcessSupervisor
|
|
@@ -327,8 +328,6 @@ class CodexAppServerRuntime(IRuntime, IAsyncLifecycle):
|
|
|
327
328
|
connection.active_turn_id = turn.turn_id
|
|
328
329
|
return CodexTurnEventStream(
|
|
329
330
|
connection.supervisor,
|
|
330
|
-
node_id=self._context.node_id,
|
|
331
|
-
runtime=session.runtime,
|
|
332
331
|
session_id=session.bcn_session_id,
|
|
333
332
|
runtime_session_id=session.id,
|
|
334
333
|
turn_id=turn.turn_id,
|
|
@@ -382,6 +381,56 @@ class CodexAppServerRuntime(IRuntime, IAsyncLifecycle):
|
|
|
382
381
|
},
|
|
383
382
|
)
|
|
384
383
|
|
|
384
|
+
async def steer_turn(
|
|
385
|
+
self,
|
|
386
|
+
session: RuntimeSession,
|
|
387
|
+
turn: RuntimeTurn,
|
|
388
|
+
input_text: str,
|
|
389
|
+
*,
|
|
390
|
+
timeout: float,
|
|
391
|
+
) -> bool:
|
|
392
|
+
self._ensure_started()
|
|
393
|
+
connection = self._connections.get(session.id)
|
|
394
|
+
provider_turn_id = turn.provider_turn_id
|
|
395
|
+
if (
|
|
396
|
+
connection is None
|
|
397
|
+
or not connection.supervisor.is_running
|
|
398
|
+
or connection.active_turn_id != turn.turn_id
|
|
399
|
+
or provider_turn_id is None
|
|
400
|
+
):
|
|
401
|
+
return False
|
|
402
|
+
try:
|
|
403
|
+
response = await connection.client.steer_turn(
|
|
404
|
+
connection.provider_thread_id,
|
|
405
|
+
provider_turn_id,
|
|
406
|
+
input_text,
|
|
407
|
+
timeout=timeout,
|
|
408
|
+
)
|
|
409
|
+
accepted_turn_id = parse_turn_steer_response(response)
|
|
410
|
+
if accepted_turn_id != provider_turn_id:
|
|
411
|
+
raise CodexAppServerProtocolError(
|
|
412
|
+
"turn/steer returned a different provider turn"
|
|
413
|
+
)
|
|
414
|
+
except asyncio.CancelledError:
|
|
415
|
+
raise
|
|
416
|
+
except Exception as error: # noqa: BLE001
|
|
417
|
+
self._logger.warning(
|
|
418
|
+
"%s",
|
|
419
|
+
json.dumps(
|
|
420
|
+
{
|
|
421
|
+
"event_name": "runtime.turn.steer.not_accepted",
|
|
422
|
+
"metadata": {
|
|
423
|
+
"error_type": type(error).__name__,
|
|
424
|
+
"session_id": session.bcn_session_id,
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
separators=(",", ":"),
|
|
428
|
+
sort_keys=True,
|
|
429
|
+
),
|
|
430
|
+
)
|
|
431
|
+
return False
|
|
432
|
+
return True
|
|
433
|
+
|
|
385
434
|
async def stop_session(
|
|
386
435
|
self, session: RuntimeSession, *, timeout: float
|
|
387
436
|
) -> ProviderCallResult[RuntimeSession]:
|
|
@@ -17,8 +17,6 @@ from ...core.models import (
|
|
|
17
17
|
OutboundDeliveryState,
|
|
18
18
|
OutboundMessage,
|
|
19
19
|
RuntimeAttempt,
|
|
20
|
-
RuntimeEvent,
|
|
21
|
-
RuntimeEventState,
|
|
22
20
|
RuntimeSession,
|
|
23
21
|
)
|
|
24
22
|
|
|
@@ -214,40 +212,6 @@ def _outbound_message_from_row(row: aiosqlite.Row) -> OutboundMessage:
|
|
|
214
212
|
)
|
|
215
213
|
|
|
216
214
|
|
|
217
|
-
def _runtime_event_from_row(row: aiosqlite.Row) -> RuntimeEvent:
|
|
218
|
-
return RuntimeEvent(
|
|
219
|
-
event_seq=_required_non_negative_int(row["event_seq"], "event_seq"),
|
|
220
|
-
event_id=_required_text(row["event_id"], "event_id"),
|
|
221
|
-
created_at_ms=_required_non_negative_int(row["created_at_ms"], "created_at_ms"),
|
|
222
|
-
level=_required_text(row["level"], "level"),
|
|
223
|
-
event_name=_required_text(row["event_name"], "event_name"),
|
|
224
|
-
state=RuntimeEventState(_required_text(row["state"], "runtime_event.state")),
|
|
225
|
-
duration_ms=_optional_non_negative_int(row["duration_ms"], "duration_ms"),
|
|
226
|
-
node_id=_optional_text(row["node_id"], "node_id"),
|
|
227
|
-
channel=_optional_text(row["channel"], "channel"),
|
|
228
|
-
runtime=_optional_text(row["runtime"], "runtime"),
|
|
229
|
-
channel_session_id=_optional_text(
|
|
230
|
-
row["channel_session_id"], "channel_session_id"
|
|
231
|
-
),
|
|
232
|
-
bcn_session_id=_optional_text(row["bcn_session_id"], "bcn_session_id"),
|
|
233
|
-
runtime_session_id=_optional_text(
|
|
234
|
-
row["runtime_session_id"], "runtime_session_id"
|
|
235
|
-
),
|
|
236
|
-
turn_id=_optional_text(row["turn_id"], "turn_id"),
|
|
237
|
-
request_id=_optional_text(row["request_id"], "request_id"),
|
|
238
|
-
command_id=_optional_text(row["command_id"], "command_id"),
|
|
239
|
-
inbound_seq=_optional_non_negative_int(row["inbound_seq"], "inbound_seq"),
|
|
240
|
-
outbound_message_id=_optional_text(
|
|
241
|
-
row["outbound_message_id"], "outbound_message_id"
|
|
242
|
-
),
|
|
243
|
-
error_kind=_optional_text(row["error_kind"], "error_kind"),
|
|
244
|
-
error_type=_optional_text(row["error_type"], "error_type"),
|
|
245
|
-
error_message=_optional_text(row["error_message"], "error_message"),
|
|
246
|
-
traceback_ref=_optional_text(row["traceback_ref"], "traceback_ref"),
|
|
247
|
-
metadata=_decode_metadata(row["metadata_json"], "metadata_json"),
|
|
248
|
-
)
|
|
249
|
-
|
|
250
|
-
|
|
251
215
|
def _consumer_cursor_from_row(row: aiosqlite.Row) -> ConsumerCursor:
|
|
252
216
|
return ConsumerCursor(
|
|
253
217
|
session_id=_required_text(row["session_id"], "session_id"),
|
|
@@ -508,37 +472,6 @@ def _merge_timestamp(
|
|
|
508
472
|
return incoming if incoming is not None else existing
|
|
509
473
|
|
|
510
474
|
|
|
511
|
-
def _validate_runtime_event_input(event: RuntimeEvent) -> None:
|
|
512
|
-
if not isinstance(event, RuntimeEvent):
|
|
513
|
-
raise TypeError("event must be a RuntimeEvent")
|
|
514
|
-
if not isinstance(event.state, RuntimeEventState):
|
|
515
|
-
raise TypeError("runtime event state is invalid")
|
|
516
|
-
for value, field_name in (
|
|
517
|
-
(event.node_id, "node_id"),
|
|
518
|
-
(event.channel, "channel"),
|
|
519
|
-
(event.runtime, "runtime"),
|
|
520
|
-
(event.channel_session_id, "channel_session_id"),
|
|
521
|
-
(event.bcn_session_id, "bcn_session_id"),
|
|
522
|
-
(event.runtime_session_id, "runtime_session_id"),
|
|
523
|
-
(event.turn_id, "turn_id"),
|
|
524
|
-
(event.request_id, "request_id"),
|
|
525
|
-
(event.command_id, "command_id"),
|
|
526
|
-
(event.outbound_message_id, "outbound_message_id"),
|
|
527
|
-
(event.error_kind, "error_kind"),
|
|
528
|
-
(event.error_type, "error_type"),
|
|
529
|
-
(event.error_message, "error_message"),
|
|
530
|
-
(event.traceback_ref, "traceback_ref"),
|
|
531
|
-
):
|
|
532
|
-
_validate_optional_input_text(value, field_name)
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
def _same_runtime_event_payload(
|
|
536
|
-
existing: RuntimeEvent,
|
|
537
|
-
incoming: RuntimeEvent,
|
|
538
|
-
) -> bool:
|
|
539
|
-
return replace(existing, event_seq=incoming.event_seq) == incoming
|
|
540
|
-
|
|
541
|
-
|
|
542
475
|
def _validate_optional_input_text(value: object, field_name: str) -> None:
|
|
543
476
|
if value is not None and (not isinstance(value, str) or not value):
|
|
544
477
|
raise ValueError(f"{field_name} must be a non-empty string when present")
|
|
@@ -13,6 +13,7 @@ import aiosqlite
|
|
|
13
13
|
from ...core.paths import resolve_data_dir
|
|
14
14
|
from ...core.storage import NodeIdentity
|
|
15
15
|
from .migrations import (
|
|
16
|
+
RUNTIME_EVENTS_REMOVAL_MIGRATION,
|
|
16
17
|
MigrationChecksumError,
|
|
17
18
|
MigrationError,
|
|
18
19
|
apply_migrations,
|
|
@@ -117,6 +118,39 @@ class SqliteDatabase:
|
|
|
117
118
|
transaction,
|
|
118
119
|
clock=_current_time_ms,
|
|
119
120
|
)
|
|
121
|
+
compaction_row: aiosqlite.Row | None = None
|
|
122
|
+
async with SqliteTransaction(self) as transaction:
|
|
123
|
+
compaction_row = await transaction.fetchone(
|
|
124
|
+
"SELECT compaction_completed_at_ms "
|
|
125
|
+
"FROM schema_migrations WHERE version = ?",
|
|
126
|
+
(RUNTIME_EVENTS_REMOVAL_MIGRATION.version,),
|
|
127
|
+
)
|
|
128
|
+
if compaction_row is None:
|
|
129
|
+
raise MigrationError(
|
|
130
|
+
"runtime event removal migration is missing from ledger"
|
|
131
|
+
)
|
|
132
|
+
if compaction_row["compaction_completed_at_ms"] is None:
|
|
133
|
+
async with self._transaction_lock:
|
|
134
|
+
await connection.execute("VACUUM")
|
|
135
|
+
async with SqliteTransaction(self) as transaction:
|
|
136
|
+
await transaction.execute(
|
|
137
|
+
"UPDATE schema_migrations "
|
|
138
|
+
"SET compaction_completed_at_ms = ? WHERE version = ?",
|
|
139
|
+
(
|
|
140
|
+
_current_time_ms(),
|
|
141
|
+
RUNTIME_EVENTS_REMOVAL_MIGRATION.version,
|
|
142
|
+
),
|
|
143
|
+
)
|
|
144
|
+
async with self._transaction_lock:
|
|
145
|
+
checkpoint_cursor = await connection.execute(
|
|
146
|
+
"PRAGMA wal_checkpoint(TRUNCATE)"
|
|
147
|
+
)
|
|
148
|
+
try:
|
|
149
|
+
checkpoint_row = await checkpoint_cursor.fetchone()
|
|
150
|
+
finally:
|
|
151
|
+
await checkpoint_cursor.close()
|
|
152
|
+
if checkpoint_row is None or checkpoint_row[0] != 0:
|
|
153
|
+
raise MigrationError("SQLite WAL checkpoint could not complete")
|
|
120
154
|
except BaseException:
|
|
121
155
|
self._connection = None
|
|
122
156
|
self._node_state = None
|
|
@@ -551,6 +551,20 @@ TRANSIENT_STREAM_EVENT_MIGRATION = Migration(
|
|
|
551
551
|
),
|
|
552
552
|
)
|
|
553
553
|
|
|
554
|
+
RUNTIME_EVENTS_REMOVAL_MIGRATION = Migration(
|
|
555
|
+
version=9,
|
|
556
|
+
name="remove_runtime_events",
|
|
557
|
+
statements=(
|
|
558
|
+
"""
|
|
559
|
+
ALTER TABLE schema_migrations
|
|
560
|
+
ADD COLUMN compaction_completed_at_ms INTEGER
|
|
561
|
+
""",
|
|
562
|
+
"""
|
|
563
|
+
DROP TABLE runtime_events
|
|
564
|
+
""",
|
|
565
|
+
),
|
|
566
|
+
)
|
|
567
|
+
|
|
554
568
|
MIGRATIONS: tuple[Migration, ...] = (
|
|
555
569
|
SCHEMA_MIGRATION,
|
|
556
570
|
SESSION_MAPPING_INDEX_MIGRATION,
|
|
@@ -560,6 +574,7 @@ MIGRATIONS: tuple[Migration, ...] = (
|
|
|
560
574
|
INBOUND_MESSAGE_REFERENCE_INTEGRITY_MIGRATION,
|
|
561
575
|
INBOUND_PROVIDER_IDENTITY_MIGRATION,
|
|
562
576
|
TRANSIENT_STREAM_EVENT_MIGRATION,
|
|
577
|
+
RUNTIME_EVENTS_REMOVAL_MIGRATION,
|
|
563
578
|
)
|
|
564
579
|
|
|
565
580
|
|
|
@@ -17,7 +17,6 @@ from ...core.models import (
|
|
|
17
17
|
InboundMessage,
|
|
18
18
|
OutboundMessage,
|
|
19
19
|
RuntimeAttempt,
|
|
20
|
-
RuntimeEvent,
|
|
21
20
|
RuntimeSession,
|
|
22
21
|
)
|
|
23
22
|
from .codec import (
|
|
@@ -31,9 +30,7 @@ from .codec import (
|
|
|
31
30
|
_required_non_negative_int,
|
|
32
31
|
_required_positive_int,
|
|
33
32
|
_runtime_attempt_from_row,
|
|
34
|
-
_runtime_event_from_row,
|
|
35
33
|
_runtime_session_from_row,
|
|
36
|
-
_same_runtime_event_payload,
|
|
37
34
|
_validate_bcn_session_update,
|
|
38
35
|
_validate_channel_session_input,
|
|
39
36
|
_validate_channel_session_update,
|
|
@@ -47,7 +44,6 @@ from .codec import (
|
|
|
47
44
|
_validate_outbound_message_input,
|
|
48
45
|
_validate_outbound_update,
|
|
49
46
|
_validate_positive_int,
|
|
50
|
-
_validate_runtime_event_input,
|
|
51
47
|
_validate_runtime_session_update,
|
|
52
48
|
)
|
|
53
49
|
|
|
@@ -664,212 +660,6 @@ class SqliteTransaction(AbstractAsyncContextManager["SqliteTransaction"]):
|
|
|
664
660
|
)
|
|
665
661
|
return canonical
|
|
666
662
|
|
|
667
|
-
async def append_runtime_event(self, event: RuntimeEvent) -> RuntimeEvent:
|
|
668
|
-
_validate_runtime_event_input(event)
|
|
669
|
-
existing_row = await self._fetch_one_or_conflict(
|
|
670
|
-
"SELECT event_seq, event_id, created_at_ms, level, event_name, state, "
|
|
671
|
-
"duration_ms, node_id, channel, runtime, "
|
|
672
|
-
"channel_session_id, bcn_session_id, runtime_session_id, "
|
|
673
|
-
"turn_id, request_id, command_id, inbound_seq, outbound_message_id, "
|
|
674
|
-
"error_kind, error_type, error_message, traceback_ref, metadata_json "
|
|
675
|
-
"FROM runtime_events WHERE event_id = ? ORDER BY event_seq",
|
|
676
|
-
(event.event_id,),
|
|
677
|
-
"runtime event identity",
|
|
678
|
-
)
|
|
679
|
-
if existing_row is not None:
|
|
680
|
-
existing = _runtime_event_from_row(existing_row)
|
|
681
|
-
if _same_runtime_event_payload(existing, event):
|
|
682
|
-
return existing
|
|
683
|
-
raise ValueError(
|
|
684
|
-
"runtime event id is already bound to different event content"
|
|
685
|
-
)
|
|
686
|
-
|
|
687
|
-
await self._validate_runtime_event_references(event)
|
|
688
|
-
sequence_row = await self.fetchone(
|
|
689
|
-
"SELECT COALESCE(MAX(event_seq), 0) + 1 AS next_event_seq "
|
|
690
|
-
"FROM runtime_events"
|
|
691
|
-
)
|
|
692
|
-
if sequence_row is None:
|
|
693
|
-
raise RuntimeError("SQLite runtime event sequence query returned no row")
|
|
694
|
-
next_event_seq = _required_positive_int(
|
|
695
|
-
sequence_row["next_event_seq"], "next_event_seq"
|
|
696
|
-
)
|
|
697
|
-
canonical = replace(event, event_seq=next_event_seq)
|
|
698
|
-
await self.execute(
|
|
699
|
-
"INSERT INTO runtime_events ("
|
|
700
|
-
"event_seq, event_id, created_at_ms, level, event_name, state, "
|
|
701
|
-
"duration_ms, node_id, channel, runtime, "
|
|
702
|
-
"channel_session_id, bcn_session_id, runtime_session_id, "
|
|
703
|
-
"turn_id, request_id, command_id, inbound_seq, outbound_message_id, "
|
|
704
|
-
"error_kind, error_type, error_message, traceback_ref, metadata_json"
|
|
705
|
-
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
706
|
-
(
|
|
707
|
-
canonical.event_seq,
|
|
708
|
-
canonical.event_id,
|
|
709
|
-
canonical.created_at_ms,
|
|
710
|
-
canonical.level,
|
|
711
|
-
canonical.event_name,
|
|
712
|
-
canonical.state.value,
|
|
713
|
-
canonical.duration_ms,
|
|
714
|
-
canonical.node_id,
|
|
715
|
-
canonical.channel,
|
|
716
|
-
canonical.runtime,
|
|
717
|
-
canonical.channel_session_id,
|
|
718
|
-
canonical.bcn_session_id,
|
|
719
|
-
canonical.runtime_session_id,
|
|
720
|
-
canonical.turn_id,
|
|
721
|
-
canonical.request_id,
|
|
722
|
-
canonical.command_id,
|
|
723
|
-
canonical.inbound_seq,
|
|
724
|
-
canonical.outbound_message_id,
|
|
725
|
-
canonical.error_kind,
|
|
726
|
-
canonical.error_type,
|
|
727
|
-
canonical.error_message,
|
|
728
|
-
canonical.traceback_ref,
|
|
729
|
-
_encode_metadata(canonical.metadata),
|
|
730
|
-
),
|
|
731
|
-
)
|
|
732
|
-
return canonical
|
|
733
|
-
|
|
734
|
-
async def _validate_runtime_event_references(self, event: RuntimeEvent) -> None:
|
|
735
|
-
channel_session = None
|
|
736
|
-
if event.channel_session_id is not None:
|
|
737
|
-
channel_session = await self.get_channel_session(event.channel_session_id)
|
|
738
|
-
if channel_session is None:
|
|
739
|
-
raise ValueError(f"unknown channel session: {event.channel_session_id}")
|
|
740
|
-
if event.channel is not None and event.channel != channel_session.channel:
|
|
741
|
-
raise ValueError("runtime event channel binding does not match")
|
|
742
|
-
|
|
743
|
-
bcn_session = None
|
|
744
|
-
if event.bcn_session_id is not None:
|
|
745
|
-
bcn_session = await self.get_bcn_session(event.bcn_session_id)
|
|
746
|
-
if bcn_session is None:
|
|
747
|
-
raise ValueError(f"unknown bcn session: {event.bcn_session_id}")
|
|
748
|
-
if (
|
|
749
|
-
event.channel_session_id is not None
|
|
750
|
-
and bcn_session.channel_session_id != event.channel_session_id
|
|
751
|
-
):
|
|
752
|
-
raise ValueError("runtime event bcn/channel binding does not match")
|
|
753
|
-
if channel_session is None:
|
|
754
|
-
channel_session = await self.get_channel_session(
|
|
755
|
-
bcn_session.channel_session_id
|
|
756
|
-
)
|
|
757
|
-
if (
|
|
758
|
-
event.channel is not None
|
|
759
|
-
and channel_session is not None
|
|
760
|
-
and event.channel != channel_session.channel
|
|
761
|
-
):
|
|
762
|
-
raise ValueError("runtime event channel binding does not match")
|
|
763
|
-
|
|
764
|
-
runtime_session = None
|
|
765
|
-
if event.runtime_session_id is not None:
|
|
766
|
-
runtime_session = await self.get_runtime_session(event.runtime_session_id)
|
|
767
|
-
if runtime_session is None:
|
|
768
|
-
raise ValueError(f"unknown runtime session: {event.runtime_session_id}")
|
|
769
|
-
if (
|
|
770
|
-
event.bcn_session_id is not None
|
|
771
|
-
and runtime_session.bcn_session_id != event.bcn_session_id
|
|
772
|
-
):
|
|
773
|
-
raise ValueError("runtime event runtime/bcn binding does not match")
|
|
774
|
-
if (
|
|
775
|
-
event.channel_session_id is not None
|
|
776
|
-
and runtime_session.channel_session_id != event.channel_session_id
|
|
777
|
-
):
|
|
778
|
-
raise ValueError("runtime event runtime/channel binding does not match")
|
|
779
|
-
if event.runtime is not None and runtime_session.runtime != event.runtime:
|
|
780
|
-
raise ValueError("runtime event runtime name does not match")
|
|
781
|
-
if event.channel is not None:
|
|
782
|
-
runtime_channel = await self.get_channel_session(
|
|
783
|
-
runtime_session.channel_session_id
|
|
784
|
-
)
|
|
785
|
-
if (
|
|
786
|
-
runtime_channel is not None
|
|
787
|
-
and runtime_channel.channel != event.channel
|
|
788
|
-
):
|
|
789
|
-
raise ValueError("runtime event channel binding does not match")
|
|
790
|
-
|
|
791
|
-
if event.turn_id is not None:
|
|
792
|
-
attempt = await self.get_runtime_attempt(event.turn_id)
|
|
793
|
-
if attempt is None:
|
|
794
|
-
raise ValueError(f"unknown runtime attempt: {event.turn_id}")
|
|
795
|
-
if (
|
|
796
|
-
event.runtime_session_id is not None
|
|
797
|
-
and attempt.session_id != event.runtime_session_id
|
|
798
|
-
):
|
|
799
|
-
raise ValueError("runtime event attempt/runtime binding does not match")
|
|
800
|
-
if runtime_session is None:
|
|
801
|
-
runtime_session = await self.get_runtime_session(attempt.session_id)
|
|
802
|
-
if (
|
|
803
|
-
event.bcn_session_id is not None
|
|
804
|
-
and runtime_session is not None
|
|
805
|
-
and runtime_session.bcn_session_id != event.bcn_session_id
|
|
806
|
-
):
|
|
807
|
-
raise ValueError("runtime event turn/bcn binding does not match")
|
|
808
|
-
if runtime_session is not None:
|
|
809
|
-
if (
|
|
810
|
-
event.channel_session_id is not None
|
|
811
|
-
and runtime_session.channel_session_id != event.channel_session_id
|
|
812
|
-
):
|
|
813
|
-
raise ValueError(
|
|
814
|
-
"runtime event turn/channel binding does not match"
|
|
815
|
-
)
|
|
816
|
-
if (
|
|
817
|
-
event.runtime is not None
|
|
818
|
-
and runtime_session.runtime != event.runtime
|
|
819
|
-
):
|
|
820
|
-
raise ValueError("runtime event turn/runtime name does not match")
|
|
821
|
-
if event.channel is not None:
|
|
822
|
-
turn_channel = await self.get_channel_session(
|
|
823
|
-
runtime_session.channel_session_id
|
|
824
|
-
)
|
|
825
|
-
if (
|
|
826
|
-
turn_channel is not None
|
|
827
|
-
and turn_channel.channel != event.channel
|
|
828
|
-
):
|
|
829
|
-
raise ValueError(
|
|
830
|
-
"runtime event turn/channel binding does not match"
|
|
831
|
-
)
|
|
832
|
-
|
|
833
|
-
if event.outbound_message_id is not None:
|
|
834
|
-
outbound = await self.get_outbound_message(event.outbound_message_id)
|
|
835
|
-
if outbound is None:
|
|
836
|
-
raise ValueError(
|
|
837
|
-
f"unknown outbound message: {event.outbound_message_id}"
|
|
838
|
-
)
|
|
839
|
-
if (
|
|
840
|
-
event.bcn_session_id is not None
|
|
841
|
-
and outbound.session_id != event.bcn_session_id
|
|
842
|
-
):
|
|
843
|
-
raise ValueError("runtime event outbound/bcn binding does not match")
|
|
844
|
-
if (
|
|
845
|
-
event.channel_session_id is not None
|
|
846
|
-
and outbound.channel_session_id != event.channel_session_id
|
|
847
|
-
):
|
|
848
|
-
raise ValueError(
|
|
849
|
-
"runtime event outbound/channel binding does not match"
|
|
850
|
-
)
|
|
851
|
-
if event.channel is not None:
|
|
852
|
-
outbound_channel = await self.get_channel_session(
|
|
853
|
-
outbound.channel_session_id
|
|
854
|
-
)
|
|
855
|
-
if (
|
|
856
|
-
outbound_channel is not None
|
|
857
|
-
and outbound_channel.channel != event.channel
|
|
858
|
-
):
|
|
859
|
-
raise ValueError(
|
|
860
|
-
"runtime event outbound/channel binding does not match"
|
|
861
|
-
)
|
|
862
|
-
|
|
863
|
-
if event.inbound_seq is not None and event.bcn_session_id is not None:
|
|
864
|
-
row = await self.fetchone(
|
|
865
|
-
"SELECT 1 FROM inbound_messages WHERE session_id = ? AND seq = ?",
|
|
866
|
-
(event.bcn_session_id, event.inbound_seq),
|
|
867
|
-
)
|
|
868
|
-
if row is None:
|
|
869
|
-
raise ValueError(
|
|
870
|
-
f"unknown inbound sequence for bcn session: {event.inbound_seq}"
|
|
871
|
-
)
|
|
872
|
-
|
|
873
663
|
async def save_channel_session(self, session: ChannelSession) -> None:
|
|
874
664
|
_validate_channel_session_input(session)
|
|
875
665
|
existing = await self.get_channel_session(session.id)
|
|
@@ -476,39 +476,23 @@ class StreamEvent:
|
|
|
476
476
|
|
|
477
477
|
@dataclass(frozen=True, slots=True)
|
|
478
478
|
class RuntimeEvent:
|
|
479
|
-
event_seq: int
|
|
480
|
-
event_id: str
|
|
481
479
|
created_at_ms: int
|
|
482
|
-
level: str
|
|
483
480
|
event_name: str
|
|
484
481
|
state: RuntimeEventState
|
|
485
|
-
duration_ms: int | None = None
|
|
486
|
-
node_id: str | None = None
|
|
487
|
-
channel: str | None = None
|
|
488
|
-
channel_session_id: str | None = None
|
|
489
|
-
bcn_session_id: str | None = None
|
|
490
|
-
runtime_session_id: str | None = None
|
|
491
482
|
turn_id: str | None = None
|
|
492
|
-
request_id: str | None = None
|
|
493
|
-
command_id: str | None = None
|
|
494
|
-
inbound_seq: int | None = None
|
|
495
|
-
outbound_message_id: str | None = None
|
|
496
483
|
error_kind: str | None = None
|
|
497
|
-
error_type: str | None = None
|
|
498
484
|
error_message: str | None = None
|
|
499
|
-
traceback_ref: str | None = None
|
|
500
|
-
runtime: str | None = None
|
|
501
485
|
metadata: Metadata = field(default_factory=dict)
|
|
502
486
|
|
|
503
487
|
def __post_init__(self) -> None:
|
|
504
|
-
_validate_non_negative(self.event_seq, "event_seq")
|
|
505
|
-
_validate_text(self.event_id, "event_id")
|
|
506
488
|
_validate_non_negative(self.created_at_ms, "created_at_ms")
|
|
507
|
-
_validate_text(self.level, "level")
|
|
508
489
|
_validate_text(self.event_name, "event_name")
|
|
490
|
+
if not isinstance(self.state, RuntimeEventState):
|
|
491
|
+
raise TypeError("state must be a RuntimeEventState")
|
|
509
492
|
for value, field_name in (
|
|
510
|
-
(self.
|
|
511
|
-
(self.
|
|
493
|
+
(self.turn_id, "turn_id"),
|
|
494
|
+
(self.error_kind, "error_kind"),
|
|
495
|
+
(self.error_message, "error_message"),
|
|
512
496
|
):
|
|
513
497
|
if value is not None:
|
|
514
|
-
|
|
498
|
+
_validate_text(value, field_name)
|
|
@@ -332,29 +332,132 @@ class SessionOrchestrator(IAsyncLifecycle):
|
|
|
332
332
|
self,
|
|
333
333
|
queue: asyncio.Queue[_RuntimeNotification],
|
|
334
334
|
) -> None:
|
|
335
|
+
pending: list[_RuntimeNotification] = []
|
|
335
336
|
while True:
|
|
336
|
-
|
|
337
|
+
if pending:
|
|
338
|
+
batch = pending
|
|
339
|
+
pending = []
|
|
340
|
+
else:
|
|
341
|
+
batch = [await queue.get()]
|
|
337
342
|
while True:
|
|
338
343
|
try:
|
|
339
344
|
batch.append(queue.get_nowait())
|
|
340
345
|
except asyncio.QueueEmpty:
|
|
341
346
|
break
|
|
347
|
+
turn_task = asyncio.create_task(
|
|
348
|
+
self._run_notification(batch[0]),
|
|
349
|
+
name=f"bcn-turn-{batch[0].context.bcn_session.id}",
|
|
350
|
+
)
|
|
351
|
+
queue_task = asyncio.create_task(queue.get())
|
|
352
|
+
queue_notification_consumed = False
|
|
342
353
|
try:
|
|
343
|
-
|
|
354
|
+
while True:
|
|
355
|
+
notification: _RuntimeNotification | None = None
|
|
356
|
+
done, _ = await asyncio.wait(
|
|
357
|
+
(turn_task, queue_task),
|
|
358
|
+
return_when=asyncio.FIRST_COMPLETED,
|
|
359
|
+
)
|
|
360
|
+
if queue_task in done:
|
|
361
|
+
notification = queue_task.result()
|
|
362
|
+
pending.append(notification)
|
|
363
|
+
queue_notification_consumed = True
|
|
364
|
+
while True:
|
|
365
|
+
try:
|
|
366
|
+
pending.append(queue.get_nowait())
|
|
367
|
+
except asyncio.QueueEmpty:
|
|
368
|
+
break
|
|
369
|
+
if turn_task in done:
|
|
370
|
+
break
|
|
371
|
+
|
|
372
|
+
async with self._storage.transaction() as transaction:
|
|
373
|
+
runtime_session = await transaction.find_runtime_session(
|
|
374
|
+
batch[0].context.bcn_session.id
|
|
375
|
+
)
|
|
376
|
+
cursor = await transaction.get_consumer_cursor(
|
|
377
|
+
batch[0].context.bcn_session.id
|
|
378
|
+
)
|
|
379
|
+
delivered_through_seq = (
|
|
380
|
+
cursor.delivered_through_seq if cursor is not None else 0
|
|
381
|
+
)
|
|
382
|
+
unread = await transaction.list_inbound_messages(
|
|
383
|
+
batch[0].context.bcn_session.id,
|
|
384
|
+
after_seq=delivered_through_seq,
|
|
385
|
+
notifying_only=True,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
active_turn = next(
|
|
389
|
+
(
|
|
390
|
+
turn
|
|
391
|
+
for turn in self._runtime_turns.values()
|
|
392
|
+
if turn.session_id == batch[0].context.runtime_session.id
|
|
393
|
+
and turn.state is RuntimeTurnState.RUNNING
|
|
394
|
+
and turn.provider_turn_id is not None
|
|
395
|
+
),
|
|
396
|
+
None,
|
|
397
|
+
)
|
|
398
|
+
if (
|
|
399
|
+
runtime_session is not None
|
|
400
|
+
and unread
|
|
401
|
+
and active_turn is not None
|
|
402
|
+
and notification is not None
|
|
403
|
+
):
|
|
404
|
+
context = SessionContext(
|
|
405
|
+
batch[0].context.channel_session,
|
|
406
|
+
batch[0].context.bcn_session,
|
|
407
|
+
runtime_session,
|
|
408
|
+
)
|
|
409
|
+
await self._turns.steer_turn(
|
|
410
|
+
notification.message,
|
|
411
|
+
context,
|
|
412
|
+
active_turn,
|
|
413
|
+
unread_count=len(unread),
|
|
414
|
+
)
|
|
415
|
+
queue_task = asyncio.create_task(queue.get())
|
|
416
|
+
queue_notification_consumed = False
|
|
417
|
+
|
|
418
|
+
result = turn_task.result()
|
|
344
419
|
for notification in batch:
|
|
345
420
|
if not notification.completion.done():
|
|
346
421
|
notification.completion.set_result(result)
|
|
347
422
|
except asyncio.CancelledError:
|
|
348
|
-
|
|
423
|
+
turn_task.cancel()
|
|
424
|
+
queue_task.cancel()
|
|
425
|
+
await asyncio.gather(
|
|
426
|
+
turn_task,
|
|
427
|
+
queue_task,
|
|
428
|
+
return_exceptions=True,
|
|
429
|
+
)
|
|
430
|
+
if (
|
|
431
|
+
not queue_notification_consumed
|
|
432
|
+
and not queue_task.cancelled()
|
|
433
|
+
and queue_task.exception() is None
|
|
434
|
+
):
|
|
435
|
+
pending.append(queue_task.result())
|
|
436
|
+
queue_notification_consumed = True
|
|
437
|
+
for notification in (*batch, *pending):
|
|
349
438
|
if not notification.completion.done():
|
|
350
439
|
notification.completion.cancel()
|
|
440
|
+
for _ in range(len(pending)):
|
|
441
|
+
queue.task_done()
|
|
442
|
+
pending.clear()
|
|
351
443
|
raise
|
|
352
444
|
except Exception as error: # noqa: BLE001
|
|
445
|
+
turn_task.cancel()
|
|
446
|
+
await asyncio.gather(turn_task, return_exceptions=True)
|
|
353
447
|
for notification in batch:
|
|
354
448
|
if not notification.completion.done():
|
|
355
449
|
notification.completion.set_exception(error)
|
|
356
450
|
finally:
|
|
357
|
-
|
|
451
|
+
if not queue_task.done():
|
|
452
|
+
queue_task.cancel()
|
|
453
|
+
try:
|
|
454
|
+
notification = await queue_task
|
|
455
|
+
except asyncio.CancelledError:
|
|
456
|
+
pass
|
|
457
|
+
else:
|
|
458
|
+
if not queue_notification_consumed:
|
|
459
|
+
pending.append(notification)
|
|
460
|
+
for _ in range(len(batch)):
|
|
358
461
|
queue.task_done()
|
|
359
462
|
|
|
360
463
|
async def _receive_loop(self) -> None:
|
|
@@ -81,6 +81,14 @@ def _is_terminal_turn_event(event: RuntimeEvent) -> bool:
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
|
|
84
|
+
def inbox_notice(session_id: str, unread_count: int) -> str:
|
|
85
|
+
return (
|
|
86
|
+
f"[inbox notice session={session_id}]\n"
|
|
87
|
+
f"Inbox update: {unread_count} unread message(s). "
|
|
88
|
+
"Use the message command to read them."
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
|
|
84
92
|
@dataclass(frozen=True, slots=True)
|
|
85
93
|
class SessionContext:
|
|
86
94
|
channel_session: ChannelSession
|
|
@@ -211,9 +219,7 @@ class SessionTurnCoordinator:
|
|
|
211
219
|
stream = await self._runtime.start_turn(
|
|
212
220
|
context.runtime_session,
|
|
213
221
|
turn,
|
|
214
|
-
|
|
215
|
-
f"Inbox update: {unread_count} unread message(s). "
|
|
216
|
-
"Use the message command to read them.",
|
|
222
|
+
inbox_notice(context.bcn_session.id, unread_count),
|
|
217
223
|
approval_handler,
|
|
218
224
|
timeout=self._timeout_budget.provider_call_seconds,
|
|
219
225
|
)
|
|
@@ -284,6 +290,52 @@ class SessionTurnCoordinator:
|
|
|
284
290
|
finally:
|
|
285
291
|
await self._close_stream(stream)
|
|
286
292
|
|
|
293
|
+
async def steer_turn(
|
|
294
|
+
self,
|
|
295
|
+
message: InboundMessage,
|
|
296
|
+
context: SessionContext,
|
|
297
|
+
turn: RuntimeTurn,
|
|
298
|
+
*,
|
|
299
|
+
unread_count: int,
|
|
300
|
+
) -> None:
|
|
301
|
+
try:
|
|
302
|
+
accepted = await self._runtime.steer_turn(
|
|
303
|
+
context.runtime_session,
|
|
304
|
+
turn,
|
|
305
|
+
inbox_notice(context.bcn_session.id, unread_count),
|
|
306
|
+
timeout=self._timeout_budget.provider_call_seconds,
|
|
307
|
+
)
|
|
308
|
+
except asyncio.CancelledError:
|
|
309
|
+
raise
|
|
310
|
+
except Exception as error: # noqa: BLE001
|
|
311
|
+
self._logger.warning(
|
|
312
|
+
"runtime turn steer failed",
|
|
313
|
+
extra={
|
|
314
|
+
"error_type": type(error).__name__,
|
|
315
|
+
"session_id": context.bcn_session.id,
|
|
316
|
+
"turn_id": turn.turn_id,
|
|
317
|
+
},
|
|
318
|
+
)
|
|
319
|
+
accepted = False
|
|
320
|
+
try:
|
|
321
|
+
await self._audit.append(
|
|
322
|
+
event_name=(
|
|
323
|
+
"runtime.request.turn.steer.accepted"
|
|
324
|
+
if accepted
|
|
325
|
+
else "runtime.request.turn.steer.not_accepted"
|
|
326
|
+
),
|
|
327
|
+
state=RuntimeEventState.COMPLETED,
|
|
328
|
+
correlation=self.turn_correlation(message, context, turn),
|
|
329
|
+
metadata={
|
|
330
|
+
"provider_method": "turn/steer",
|
|
331
|
+
"unread_count": unread_count,
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
except asyncio.CancelledError:
|
|
335
|
+
raise
|
|
336
|
+
except Exception:
|
|
337
|
+
self._logger.exception("runtime turn steer audit failed")
|
|
338
|
+
|
|
287
339
|
async def finish_turn(
|
|
288
340
|
self,
|
|
289
341
|
turn: RuntimeTurn,
|
|
@@ -354,58 +406,56 @@ class SessionTurnCoordinator:
|
|
|
354
406
|
if event.turn_id is not None and event.turn_id != turn.turn_id:
|
|
355
407
|
raise ValueError("runtime event turn correlation mismatch")
|
|
356
408
|
async with self._concurrency.for_session(message.session_id):
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
provider_turn_id
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
error_kind =
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
updated_turn,
|
|
400
|
-
provider_turn_id=provider_turn_id,
|
|
401
|
-
)
|
|
402
|
-
agent_tick = AgentTick(
|
|
403
|
-
source=AgentTickSource.RUNTIME,
|
|
404
|
-
signal=agent_signal,
|
|
405
|
-
observed_at_ms=self._clock(),
|
|
406
|
-
error_kind=error_kind,
|
|
407
|
-
error_message=event.error_message,
|
|
409
|
+
agent_signal = _runtime_event_agent_signal(event)
|
|
410
|
+
if not _is_turn_event(event.event_name):
|
|
411
|
+
target_state = turn.state
|
|
412
|
+
elif event.state is RuntimeEventState.STARTED:
|
|
413
|
+
target_state = RuntimeTurnState.RUNNING
|
|
414
|
+
elif event.state is RuntimeEventState.COMPLETED:
|
|
415
|
+
target_state = RuntimeTurnState.COMPLETED
|
|
416
|
+
elif event.state is RuntimeEventState.FAILED:
|
|
417
|
+
target_state = RuntimeTurnState.FAILED
|
|
418
|
+
elif event.state is RuntimeEventState.CANCELLED:
|
|
419
|
+
target_state = RuntimeTurnState.CANCELLED
|
|
420
|
+
else:
|
|
421
|
+
target_state = RuntimeTurnState.UNKNOWN
|
|
422
|
+
provider_turn_id = event.metadata.get("provider_turn_id")
|
|
423
|
+
if provider_turn_id is not None and (
|
|
424
|
+
not isinstance(provider_turn_id, str) or not provider_turn_id
|
|
425
|
+
):
|
|
426
|
+
raise ValueError("runtime event provider_turn_id is invalid")
|
|
427
|
+
if (
|
|
428
|
+
provider_turn_id is not None
|
|
429
|
+
and turn.provider_turn_id is not None
|
|
430
|
+
and turn.provider_turn_id != provider_turn_id
|
|
431
|
+
):
|
|
432
|
+
raise ValueError("runtime event provider turn correlation mismatch")
|
|
433
|
+
error_kind = event.error_kind
|
|
434
|
+
if event.state is RuntimeEventState.FAILED and error_kind is None:
|
|
435
|
+
error_kind = ErrorKind.PROVIDER_FAILED.value
|
|
436
|
+
if event.state is RuntimeEventState.UNKNOWN and error_kind is None:
|
|
437
|
+
error_kind = ErrorKind.PROVIDER_UNKNOWN.value
|
|
438
|
+
if event.state is RuntimeEventState.CANCELLED and error_kind is None:
|
|
439
|
+
error_kind = ErrorKind.CANCELLED.value
|
|
440
|
+
updated_turn = turn.transition_to(
|
|
441
|
+
target_state,
|
|
442
|
+
at_ms=event.created_at_ms,
|
|
443
|
+
error_kind=error_kind,
|
|
444
|
+
error_message=event.error_message,
|
|
445
|
+
latest_event_name=event.event_name,
|
|
446
|
+
)
|
|
447
|
+
if provider_turn_id is not None:
|
|
448
|
+
updated_turn = replace(
|
|
449
|
+
updated_turn,
|
|
450
|
+
provider_turn_id=provider_turn_id,
|
|
408
451
|
)
|
|
452
|
+
agent_tick = AgentTick(
|
|
453
|
+
source=AgentTickSource.RUNTIME,
|
|
454
|
+
signal=agent_signal,
|
|
455
|
+
observed_at_ms=self._clock(),
|
|
456
|
+
error_kind=error_kind,
|
|
457
|
+
error_message=event.error_message,
|
|
458
|
+
)
|
|
409
459
|
if _is_terminal_turn_event(event):
|
|
410
460
|
self._turns.pop(turn.turn_id, None)
|
|
411
461
|
else:
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/runtime.py
RENAMED
|
@@ -101,6 +101,22 @@ class IRuntime(IAsyncLifecycle, Protocol):
|
|
|
101
101
|
"""
|
|
102
102
|
...
|
|
103
103
|
|
|
104
|
+
async def steer_turn(
|
|
105
|
+
self,
|
|
106
|
+
session: RuntimeSession,
|
|
107
|
+
turn: RuntimeTurn,
|
|
108
|
+
input_text: str,
|
|
109
|
+
*,
|
|
110
|
+
timeout: float,
|
|
111
|
+
) -> bool:
|
|
112
|
+
"""Append input to the active turn when the runtime supports steering.
|
|
113
|
+
|
|
114
|
+
Return true only when the runtime confirms that the active turn accepted
|
|
115
|
+
the input. Unsupported or unconfirmed steering returns false without
|
|
116
|
+
changing the turn lifecycle.
|
|
117
|
+
"""
|
|
118
|
+
...
|
|
119
|
+
|
|
104
120
|
async def interrupt_turn(
|
|
105
121
|
self,
|
|
106
122
|
session: RuntimeSession,
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/storage.py
RENAMED
|
@@ -12,7 +12,6 @@ from .models import (
|
|
|
12
12
|
InboundMessage,
|
|
13
13
|
OutboundMessage,
|
|
14
14
|
RuntimeAttempt,
|
|
15
|
-
RuntimeEvent,
|
|
16
15
|
RuntimeSession,
|
|
17
16
|
)
|
|
18
17
|
|
|
@@ -140,10 +139,6 @@ class IStorageTransaction(Protocol):
|
|
|
140
139
|
"""Persist a draft or delivery transition and return its canonical row."""
|
|
141
140
|
...
|
|
142
141
|
|
|
143
|
-
async def append_runtime_event(self, event: RuntimeEvent) -> RuntimeEvent:
|
|
144
|
-
"""Append an operational event and return its canonical local sequence."""
|
|
145
|
-
...
|
|
146
|
-
|
|
147
142
|
|
|
148
143
|
class IStorage(IAsyncLifecycle, Protocol):
|
|
149
144
|
"""Provider-neutral storage lifecycle and explicit transaction factory."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/__init__.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/application.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/attachments.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/command.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/config.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/registry.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/transport.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/windows_pipe.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/wrapper.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/__init__.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/approval.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/audit.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/channel.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/client.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/command.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/concurrency.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/correlation.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/instruction.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/lifecycle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/outcomes.py
RENAMED
|
File without changes
|
{bazaar_compute_node-0.1.3 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/paths.py
RENAMED
|
File without changes
|