bazaar-compute-node 0.1.4__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.
Files changed (62) hide show
  1. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/PKG-INFO +1 -1
  2. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/pyproject.toml +1 -1
  3. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/pyproject.toml.orig +1 -1
  4. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/__init__.py +4 -0
  5. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/client.py +34 -0
  6. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/runtime.py +51 -0
  7. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/orchestration/session.py +107 -4
  8. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/orchestration/turn.py +55 -3
  9. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/runtime.py +16 -0
  10. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/LICENSE +0 -0
  11. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/README.md +0 -0
  12. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/__init__.py +0 -0
  13. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/__init__.py +0 -0
  14. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/application.py +0 -0
  15. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/attachments.py +0 -0
  16. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/command.py +0 -0
  17. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/config.py +0 -0
  18. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/registry.py +0 -0
  19. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/transport.py +0 -0
  20. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/windows_pipe.py +0 -0
  21. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/app/wrapper.py +0 -0
  22. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/bcc.py +0 -0
  23. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/cli.py +0 -0
  24. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/__init__.py +0 -0
  25. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/approval.py +0 -0
  26. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/events.py +0 -0
  27. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/plugin.py +0 -0
  28. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/process.py +0 -0
  29. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/codex_app_server/protocol.py +0 -0
  30. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/logging/__init__.py +0 -0
  31. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/logging/audit.py +0 -0
  32. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/logging/plugin.py +0 -0
  33. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/sqlite/__init__.py +0 -0
  34. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/sqlite/codec.py +0 -0
  35. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/sqlite/database.py +0 -0
  36. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/sqlite/migrations.py +0 -0
  37. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/sqlite/plugin.py +0 -0
  38. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/sqlite/repository.py +0 -0
  39. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/wecom/__init__.py +0 -0
  40. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/wecom/channel.py +0 -0
  41. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/wecom/markdown.py +0 -0
  42. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/contrib/wecom/plugin.py +0 -0
  43. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/__init__.py +0 -0
  44. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/approval.py +0 -0
  45. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/audit.py +0 -0
  46. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/channel.py +0 -0
  47. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/client.py +0 -0
  48. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/command.py +0 -0
  49. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/concurrency.py +0 -0
  50. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/correlation.py +0 -0
  51. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/instruction.py +0 -0
  52. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/lifecycle.py +0 -0
  53. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/models/__init__.py +0 -0
  54. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/models/entities.py +0 -0
  55. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/models/states.py +0 -0
  56. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/observability.py +0 -0
  57. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/orchestration/__init__.py +0 -0
  58. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/orchestration/command.py +0 -0
  59. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/orchestration/services.py +0 -0
  60. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/outcomes.py +0 -0
  61. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/paths.py +0 -0
  62. {bazaar_compute_node-0.1.4 → bazaar_compute_node-0.1.5}/src/bazaar_compute_node/core/storage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bazaar-compute-node
3
- Version: 0.1.4
3
+ Version: 0.1.5
4
4
  Summary: Runtime-agnostic computer node daemon for agents and channels.
5
5
  License-Expression: AGPL-3.0-only
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "bazaar-compute-node"
3
- version = "0.1.4"
3
+ version = "0.1.5"
4
4
  description = "Runtime-agnostic computer node daemon for agents and channels."
5
5
  readme = "README.md"
6
6
  license = "AGPL-3.0-only"
@@ -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")
@@ -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
@@ -380,6 +381,56 @@ class CodexAppServerRuntime(IRuntime, IAsyncLifecycle):
380
381
  },
381
382
  )
382
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
+
383
434
  async def stop_session(
384
435
  self, session: RuntimeSession, *, timeout: float
385
436
  ) -> ProviderCallResult[RuntimeSession]:
@@ -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
- batch = [await queue.get()]
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
- result = await self._run_notification(batch[0])
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
- for notification in batch:
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
- for _notification in batch:
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
- f"[inbox notice session={context.bcn_session.id}]\n"
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,
@@ -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,