feat: ChannelStore outbox (agent->graph) with consume + blocking receive
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
@@ -61,3 +62,30 @@ class ChannelStore:
|
||||
return {"turn": 0, "text": None, "image_path": None}
|
||||
s = ch.inbox
|
||||
return {"turn": s.turn, "text": s.text, "image_path": s.image_path}
|
||||
|
||||
# --- outbox: agent -> graph ---
|
||||
def push(self, channel: str, text: Optional[str] = None,
|
||||
image_path: Optional[str] = None) -> int:
|
||||
with self._cond:
|
||||
ch = self._chan(channel)
|
||||
ch.outbox.turn += 1
|
||||
ch.outbox.text = text
|
||||
ch.outbox.image_path = image_path
|
||||
self._cond.notify_all()
|
||||
return ch.outbox.turn
|
||||
|
||||
def receive(self, channel: str, wait_seconds: float = 0.0) -> dict:
|
||||
empty = {"turn": 0, "text": None, "image_path": None}
|
||||
deadline = time.monotonic() + max(0.0, wait_seconds)
|
||||
with self._cond:
|
||||
while True:
|
||||
ch = self._channels.get(channel)
|
||||
if ch is not None and ch.outbox.turn > ch.last_consumed_out_turn:
|
||||
ch.last_consumed_out_turn = ch.outbox.turn
|
||||
s = ch.outbox
|
||||
return {"turn": s.turn, "text": s.text,
|
||||
"image_path": s.image_path}
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
return empty
|
||||
self._cond.wait(timeout=remaining)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import threading
|
||||
import time
|
||||
from src.bridge.store import ChannelStore
|
||||
|
||||
def test_push_then_receive_consumes_once():
|
||||
s = ChannelStore.instance()
|
||||
s.push("main", text="result", image_path="/tmp/out.png")
|
||||
got = s.receive("main", wait_seconds=0)
|
||||
assert got["text"] == "result"
|
||||
assert got["image_path"] == "/tmp/out.png"
|
||||
# second receive with no new push -> empty (already consumed)
|
||||
again = s.receive("main", wait_seconds=0)
|
||||
assert again["text"] is None and again["image_path"] is None
|
||||
|
||||
def test_receive_nonblocking_empty_channel():
|
||||
s = ChannelStore.instance()
|
||||
got = s.receive("nope", wait_seconds=0)
|
||||
assert got == {"turn": 0, "text": None, "image_path": None}
|
||||
|
||||
def test_receive_blocks_until_push():
|
||||
s = ChannelStore.instance()
|
||||
|
||||
def push_later():
|
||||
time.sleep(0.2)
|
||||
s.push("main", text="late")
|
||||
|
||||
threading.Thread(target=push_later, daemon=True).start()
|
||||
start = time.monotonic()
|
||||
got = s.receive("main", wait_seconds=2.0)
|
||||
elapsed = time.monotonic() - start
|
||||
assert got["text"] == "late"
|
||||
assert 0.15 < elapsed < 1.5 # returned shortly after the push, not at timeout
|
||||
|
||||
def test_receive_times_out_returns_empty():
|
||||
s = ChannelStore.instance()
|
||||
start = time.monotonic()
|
||||
got = s.receive("main", wait_seconds=0.3)
|
||||
elapsed = time.monotonic() - start
|
||||
assert got["text"] is None and got["image_path"] is None
|
||||
assert elapsed >= 0.3
|
||||
Reference in New Issue
Block a user