test(slice-2): integration test + Python echo brain + LEARNING pointers
- Integration test: smoke test against in-process EchoServer (hello handshake round-trip). Full end-to-end + reconnect-path tests adapt to slice-1's existing integration-test harness. - examples/echo_brain/: Python reference brain (~80 lines, websockets lib). README documents runtime posture + why a Python brain complements the Rust one (language-agnosticism vs wire-types-reusability). - LEARNING.md: 5 new pointers (mpsc/oneshot, VecDeque ring, async WS, Box<dyn Trait> field widening, zero-sized marker newtype). Spec ref: 2026-06-28-slice-2-agent-tap-design.md §8.4, §8.5 #7.
This commit is contained in:
93
examples/echo_brain/echo_brain.py
Normal file
93
examples/echo_brain/echo_brain.py
Normal file
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Rutster slice-2 reference echo brain (foreign-language canonical demo).
|
||||
|
||||
Speaks the slice-2 tap wire protocol (spec §3):
|
||||
- ws:// text JSON frames, v=1 envelope.
|
||||
- On `hello`: ack with `hello`.
|
||||
- On `audio_in`: echo the same PCM back as `audio_out`.
|
||||
- On `bye` / `session_end`: close cleanly.
|
||||
|
||||
Run:
|
||||
pip install websockets
|
||||
python examples/echo_brain/echo_brain.py
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import base64
|
||||
|
||||
import websockets
|
||||
|
||||
PROTOCOL_VERSION = 1
|
||||
SAMPLES = 480
|
||||
|
||||
|
||||
async def echo_handler(ws):
|
||||
seq_egress = 0
|
||||
# Wait for hello.
|
||||
raw = await ws.recv()
|
||||
env = json.loads(raw)
|
||||
assert env["type"] == "hello", f"first frame != hello: {env}"
|
||||
session_id = env["session_id"]
|
||||
hello_ack = {
|
||||
"v": PROTOCOL_VERSION, "type": "hello", "seq": seq_egress, "ts": 0,
|
||||
"session_id": session_id,
|
||||
}
|
||||
seq_egress += 1
|
||||
await ws.send(json.dumps(hello_ack))
|
||||
print(f"[echo_brain] hello acked: session_id={session_id}")
|
||||
|
||||
async for raw in ws:
|
||||
try:
|
||||
env = json.loads(raw)
|
||||
except json.JSONDecodeError as e:
|
||||
await ws.send(json.dumps({
|
||||
"v": PROTOCOL_VERSION, "type": "error", "seq": seq_egress, "ts": 0,
|
||||
"code": "decode_failed", "message": str(e),
|
||||
}))
|
||||
seq_egress += 1
|
||||
continue
|
||||
|
||||
kind = env.get("type")
|
||||
if kind == "audio_in":
|
||||
pcm_b64 = env["pcm"]
|
||||
samples = env.get("samples", SAMPLES)
|
||||
if samples != SAMPLES:
|
||||
await ws.send(json.dumps({
|
||||
"v": PROTOCOL_VERSION, "type": "error", "seq": seq_egress, "ts": 0,
|
||||
"code": "bad_samples", "message": f"expected {SAMPLES}, got {samples}",
|
||||
}))
|
||||
seq_egress += 1
|
||||
continue
|
||||
# Echo: same PCM, same samples.
|
||||
out = {
|
||||
"v": PROTOCOL_VERSION, "type": "audio_out", "seq": seq_egress,
|
||||
"ts": env.get("ts", 0),
|
||||
"pcm": pcm_b64, "samples": samples,
|
||||
}
|
||||
seq_egress += 1
|
||||
await ws.send(json.dumps(out))
|
||||
elif kind == "bye":
|
||||
print(f"[echo_brain] bye received: {env.get('reason')}")
|
||||
await ws.send(json.dumps({
|
||||
"v": PROTOCOL_VERSION, "type": "bye", "seq": seq_egress, "ts": 0,
|
||||
"reason": "brain_ack",
|
||||
}))
|
||||
await ws.close()
|
||||
return
|
||||
elif kind == "session_end":
|
||||
print(f"[echo_brain] session_end received: {env.get('reason')}")
|
||||
await ws.close()
|
||||
return
|
||||
else:
|
||||
print(f"[echo_brain] unknown frame type: {kind}; ignoring")
|
||||
|
||||
|
||||
async def main():
|
||||
print("[echo_brain] listening on ws://127.0.0.1:8081/echo")
|
||||
async with websockets.serve(echo_handler, "127.0.0.1", 8081):
|
||||
await asyncio.Future() # run forever
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user