Skip to content

Commit 2e5d6bb

Browse files
committed
fix(client): add run_forever()+stop() to ZhubConnection and ZhubExposure
run_forever() was referenced in the module docstring and the publish() docstring but never implemented — any user following the quickstart got AttributeError before a connection was even attempted. ZhubConnection and ZhubExposure also lacked stop(), making them impossible to shut down cleanly without touching private _task/_stop_event. Add async run_forever() to all three classes (awaits _stop_event, unblocks when stop() is called). Add stop() to ZhubConnection and ZhubExposure mirroring ZhubPublication.stop(). Fix the __init__.py quickstart to use the correct asyncio.run(main()) + await pub.run_forever() pattern. Update the publish() docstring to describe the auto-start behaviour.
1 parent cf34f87 commit 2e5d6bb

2 files changed

Lines changed: 54 additions & 16 deletions

File tree

zhub/__init__.py

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,27 +18,35 @@
1818
Quickstart:
1919
2020
# Inside your AI
21+
import asyncio
2122
from zhub import publish
2223
23-
pub = publish(
24-
name="my-ai",
25-
description="A custom AI agent",
26-
hub_url="https://hub.example.com",
27-
chat_handler=lambda messages: "...",
28-
)
29-
pub.run_forever()
24+
async def main():
25+
pub = publish(
26+
name="my-ai",
27+
description="A custom AI agent",
28+
hub_url="https://hub.example.com",
29+
chat_handler=lambda messages: "...",
30+
)
31+
await pub.run_forever()
32+
33+
asyncio.run(main())
3034
3135
# Inside your client (e.g., a generic device)
36+
import asyncio
3237
from zhub import connect
3338
34-
conn = connect(
35-
endpoint="https://hub.example.com/my-ai",
36-
api_key="zk_...",
37-
capabilities={
38-
"send_whatsapp": (schema, handler),
39-
},
40-
)
41-
conn.run_forever()
39+
async def main():
40+
conn = connect(
41+
endpoint="https://hub.example.com/my-ai",
42+
api_key="zk_...",
43+
capabilities={
44+
"send_whatsapp": (schema, handler),
45+
},
46+
)
47+
await conn.run_forever()
48+
49+
asyncio.run(main())
4250
"""
4351

4452
from .client import (

zhub/client.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ async def stop(self) -> None:
112112
if self._task:
113113
self._task.cancel()
114114

115+
async def run_forever(self) -> None:
116+
"""Block until stop() is called. Useful as the final await in an
117+
async main: asyncio.run(main()) where main() ends with
118+
``await pub.run_forever()``."""
119+
await self._stop_event.wait()
120+
115121

116122
def publish(
117123
name: str,
@@ -129,7 +135,11 @@ def publish(
129135
resources: Optional[list[dict[str, Any]]] = None,
130136
prompts: Optional[list[dict[str, Any]]] = None,
131137
) -> ZhubPublication:
132-
"""Create a ZhubPublication. Call .run_forever() to actually start serving.
138+
"""Create a ZhubPublication and start the background reconnect loop.
139+
140+
In an async context the task runs immediately; call ``await pub.run_forever()``
141+
to block until stop() is called. In a script: ``asyncio.run(main())``
142+
where ``main()`` ends with ``await pub.run_forever()``.
133143
134144
If `api_key` is supplied AND the hub has a stored publisher with the
135145
same name and matching key hash, this is a re-registration after a hub
@@ -481,6 +491,16 @@ async def chat_stream(self, messages: list[dict[str, Any]],
481491
finally:
482492
self._streams.pop(env.request_id, None)
483493

494+
async def stop(self) -> None:
495+
"""Stop the connection loop and cancel the background task."""
496+
self._stop_event.set()
497+
if self._task:
498+
self._task.cancel()
499+
500+
async def run_forever(self) -> None:
501+
"""Block until stop() is called."""
502+
await self._stop_event.wait()
503+
484504

485505
def connect(
486506
ai_name: str,
@@ -616,6 +636,16 @@ class ZhubExposure:
616636
_ws: Any = None
617637
_stop_event: asyncio.Event = field(default_factory=asyncio.Event)
618638

639+
async def stop(self) -> None:
640+
"""Stop the exposure loop and cancel the background task."""
641+
self._stop_event.set()
642+
if self._task:
643+
self._task.cancel()
644+
645+
async def run_forever(self) -> None:
646+
"""Block until stop() is called."""
647+
await self._stop_event.wait()
648+
619649

620650
def expose(
621651
name: str,

0 commit comments

Comments
 (0)