claw-msg 0.1.0__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 (45) hide show
  1. claw_msg-0.1.0/PKG-INFO +106 -0
  2. claw_msg-0.1.0/README.md +74 -0
  3. claw_msg-0.1.0/pyproject.toml +54 -0
  4. claw_msg-0.1.0/setup.cfg +4 -0
  5. claw_msg-0.1.0/src/claw_msg/__init__.py +6 -0
  6. claw_msg-0.1.0/src/claw_msg/cli/__init__.py +0 -0
  7. claw_msg-0.1.0/src/claw_msg/cli/main.py +211 -0
  8. claw_msg-0.1.0/src/claw_msg/client/__init__.py +0 -0
  9. claw_msg-0.1.0/src/claw_msg/client/agent.py +148 -0
  10. claw_msg-0.1.0/src/claw_msg/client/connection.py +132 -0
  11. claw_msg-0.1.0/src/claw_msg/client/credentials.py +43 -0
  12. claw_msg-0.1.0/src/claw_msg/client/http.py +95 -0
  13. claw_msg-0.1.0/src/claw_msg/common/__init__.py +0 -0
  14. claw_msg-0.1.0/src/claw_msg/common/errors.py +21 -0
  15. claw_msg-0.1.0/src/claw_msg/common/models.py +96 -0
  16. claw_msg-0.1.0/src/claw_msg/common/protocol.py +21 -0
  17. claw_msg-0.1.0/src/claw_msg/daemon/__init__.py +0 -0
  18. claw_msg-0.1.0/src/claw_msg/daemon/runner.py +25 -0
  19. claw_msg-0.1.0/src/claw_msg/daemon/service.py +76 -0
  20. claw_msg-0.1.0/src/claw_msg/daemon/webhook.py +22 -0
  21. claw_msg-0.1.0/src/claw_msg/server/__init__.py +0 -0
  22. claw_msg-0.1.0/src/claw_msg/server/app.py +49 -0
  23. claw_msg-0.1.0/src/claw_msg/server/auth.py +69 -0
  24. claw_msg-0.1.0/src/claw_msg/server/broker.py +64 -0
  25. claw_msg-0.1.0/src/claw_msg/server/config.py +14 -0
  26. claw_msg-0.1.0/src/claw_msg/server/database.py +95 -0
  27. claw_msg-0.1.0/src/claw_msg/server/offline_queue.py +75 -0
  28. claw_msg-0.1.0/src/claw_msg/server/presence.py +43 -0
  29. claw_msg-0.1.0/src/claw_msg/server/rate_limit.py +39 -0
  30. claw_msg-0.1.0/src/claw_msg/server/routes_agents.py +130 -0
  31. claw_msg-0.1.0/src/claw_msg/server/routes_messages.py +114 -0
  32. claw_msg-0.1.0/src/claw_msg/server/routes_rooms.py +129 -0
  33. claw_msg-0.1.0/src/claw_msg/server/routes_ws.py +190 -0
  34. claw_msg-0.1.0/src/claw_msg.egg-info/PKG-INFO +106 -0
  35. claw_msg-0.1.0/src/claw_msg.egg-info/SOURCES.txt +43 -0
  36. claw_msg-0.1.0/src/claw_msg.egg-info/dependency_links.txt +1 -0
  37. claw_msg-0.1.0/src/claw_msg.egg-info/entry_points.txt +2 -0
  38. claw_msg-0.1.0/src/claw_msg.egg-info/requires.txt +14 -0
  39. claw_msg-0.1.0/src/claw_msg.egg-info/top_level.txt +1 -0
  40. claw_msg-0.1.0/tests/test_direct_messaging.py +53 -0
  41. claw_msg-0.1.0/tests/test_offline_queue.py +49 -0
  42. claw_msg-0.1.0/tests/test_registration.py +50 -0
  43. claw_msg-0.1.0/tests/test_rooms.py +93 -0
  44. claw_msg-0.1.0/tests/test_sdk_agent.py +62 -0
  45. claw_msg-0.1.0/tests/test_websocket.py +92 -0
@@ -0,0 +1,106 @@
1
+ Metadata-Version: 2.4
2
+ Name: claw-msg
3
+ Version: 0.1.0
4
+ Summary: Agent-to-agent messaging layer — pip install → 5 lines → agents talk
5
+ Author-email: Sangbum <snuhsb@snu.ac.kr>
6
+ License: MIT
7
+ Keywords: agent,messaging,websocket,ai,multi-agent
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Framework :: FastAPI
15
+ Classifier: Topic :: Communications
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ Requires-Dist: fastapi>=0.110.0
20
+ Requires-Dist: uvicorn[standard]>=0.27.0
21
+ Requires-Dist: aiosqlite>=0.20.0
22
+ Requires-Dist: bcrypt>=4.1.0
23
+ Requires-Dist: websockets>=12.0
24
+ Requires-Dist: httpx>=0.27.0
25
+ Requires-Dist: pydantic>=2.0.0
26
+ Requires-Dist: click>=8.1.0
27
+ Requires-Dist: python-dotenv>=1.0.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
30
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
31
+ Requires-Dist: httpx>=0.27.0; extra == "dev"
32
+
33
+ # claw-msg
34
+
35
+ Agent-to-agent messaging layer. `pip install → 5 lines → agents talk`.
36
+
37
+ ## Quick Start
38
+
39
+ ```bash
40
+ pip install -e .
41
+
42
+ # Start the broker
43
+ claw-msg serve --port 8000
44
+
45
+ # Register agents (in separate terminals)
46
+ claw-msg register --name agent-a --broker http://localhost:8000
47
+ claw-msg register --name agent-b --broker http://localhost:8000
48
+
49
+ # Send a message
50
+ claw-msg send --to <agent-b-id> --broker http://localhost:8000 --token <token-a> "hello!"
51
+
52
+ # Listen for messages
53
+ claw-msg listen --broker http://localhost:8000 --token <token-b>
54
+ ```
55
+
56
+ ## SDK Usage
57
+
58
+ ```python
59
+ from claw_msg import Agent
60
+ import asyncio
61
+
62
+ async def main():
63
+ agent = Agent("http://localhost:8000", name="my-agent")
64
+ await agent.register()
65
+ print(f"registered: {agent.agent_id}")
66
+
67
+ # Send a direct message
68
+ await agent.send("<other-agent-id>", "hello!")
69
+
70
+ # Create and use rooms
71
+ room = await agent.create_room("general")
72
+ await agent.send_to_room(room["id"], "hello room!")
73
+
74
+ asyncio.run(main())
75
+ ```
76
+
77
+ ## Features
78
+
79
+ - **WebSocket real-time messaging** with auto-reconnect
80
+ - **HTTP polling fallback** for stateless agents
81
+ - **Rooms** for group messaging
82
+ - **Offline queue** with 7-day TTL and at-least-once delivery
83
+ - **Agent discovery** by name/capabilities
84
+ - **Rate limiting** (60 msg/min token bucket)
85
+ - **Presence tracking** (online/offline/last_seen)
86
+ - **CLI** for all operations
87
+ - **Daemon mode** with webhook forwarding + systemd/launchd service generation
88
+
89
+ ## Architecture
90
+
91
+ Same package provides both **broker** (server) and **SDK** (client):
92
+
93
+ ```
94
+ ┌──────────┐ WebSocket ┌──────────┐ WebSocket ┌──────────┐
95
+ │ Agent A │◄────────────►│ Broker │◄────────────►│ Agent B │
96
+ └──────────┘ └──────────┘ └──────────┘
97
+
98
+ SQLite + WAL
99
+ ```
100
+
101
+ ## Development
102
+
103
+ ```bash
104
+ pip install -e ".[dev]"
105
+ pytest tests/ -v
106
+ ```
@@ -0,0 +1,74 @@
1
+ # claw-msg
2
+
3
+ Agent-to-agent messaging layer. `pip install → 5 lines → agents talk`.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ pip install -e .
9
+
10
+ # Start the broker
11
+ claw-msg serve --port 8000
12
+
13
+ # Register agents (in separate terminals)
14
+ claw-msg register --name agent-a --broker http://localhost:8000
15
+ claw-msg register --name agent-b --broker http://localhost:8000
16
+
17
+ # Send a message
18
+ claw-msg send --to <agent-b-id> --broker http://localhost:8000 --token <token-a> "hello!"
19
+
20
+ # Listen for messages
21
+ claw-msg listen --broker http://localhost:8000 --token <token-b>
22
+ ```
23
+
24
+ ## SDK Usage
25
+
26
+ ```python
27
+ from claw_msg import Agent
28
+ import asyncio
29
+
30
+ async def main():
31
+ agent = Agent("http://localhost:8000", name="my-agent")
32
+ await agent.register()
33
+ print(f"registered: {agent.agent_id}")
34
+
35
+ # Send a direct message
36
+ await agent.send("<other-agent-id>", "hello!")
37
+
38
+ # Create and use rooms
39
+ room = await agent.create_room("general")
40
+ await agent.send_to_room(room["id"], "hello room!")
41
+
42
+ asyncio.run(main())
43
+ ```
44
+
45
+ ## Features
46
+
47
+ - **WebSocket real-time messaging** with auto-reconnect
48
+ - **HTTP polling fallback** for stateless agents
49
+ - **Rooms** for group messaging
50
+ - **Offline queue** with 7-day TTL and at-least-once delivery
51
+ - **Agent discovery** by name/capabilities
52
+ - **Rate limiting** (60 msg/min token bucket)
53
+ - **Presence tracking** (online/offline/last_seen)
54
+ - **CLI** for all operations
55
+ - **Daemon mode** with webhook forwarding + systemd/launchd service generation
56
+
57
+ ## Architecture
58
+
59
+ Same package provides both **broker** (server) and **SDK** (client):
60
+
61
+ ```
62
+ ┌──────────┐ WebSocket ┌──────────┐ WebSocket ┌──────────┐
63
+ │ Agent A │◄────────────►│ Broker │◄────────────►│ Agent B │
64
+ └──────────┘ └──────────┘ └──────────┘
65
+
66
+ SQLite + WAL
67
+ ```
68
+
69
+ ## Development
70
+
71
+ ```bash
72
+ pip install -e ".[dev]"
73
+ pytest tests/ -v
74
+ ```
@@ -0,0 +1,54 @@
1
+ [project]
2
+ name = "claw-msg"
3
+ version = "0.1.0"
4
+ description = "Agent-to-agent messaging layer — pip install → 5 lines → agents talk"
5
+ readme = "README.md"
6
+ license = {text = "MIT"}
7
+ authors = [
8
+ {name = "Sangbum", email = "snuhsb@snu.ac.kr"}
9
+ ]
10
+ keywords = ["agent", "messaging", "websocket", "ai", "multi-agent"]
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3.11",
14
+ "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Framework :: FastAPI",
19
+ "Topic :: Communications",
20
+ "Topic :: Software Development :: Libraries :: Python Modules",
21
+ ]
22
+ requires-python = ">=3.11"
23
+ dependencies = [
24
+ "fastapi>=0.110.0",
25
+ "uvicorn[standard]>=0.27.0",
26
+ "aiosqlite>=0.20.0",
27
+ "bcrypt>=4.1.0",
28
+ "websockets>=12.0",
29
+ "httpx>=0.27.0",
30
+ "pydantic>=2.0.0",
31
+ "click>=8.1.0",
32
+ "python-dotenv>=1.0.0",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ dev = [
37
+ "pytest>=8.0.0",
38
+ "pytest-asyncio>=0.23.0",
39
+ "httpx>=0.27.0",
40
+ ]
41
+
42
+ [project.scripts]
43
+ claw-msg = "claw_msg.cli.main:cli"
44
+
45
+ [build-system]
46
+ requires = ["setuptools>=68.0"]
47
+ build-backend = "setuptools.build_meta"
48
+
49
+ [tool.setuptools.packages.find]
50
+ where = ["src"]
51
+
52
+ [tool.pytest.ini_options]
53
+ asyncio_mode = "auto"
54
+ testpaths = ["tests"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,6 @@
1
+ """claw-msg: Agent-to-agent messaging layer."""
2
+
3
+ from claw_msg.client.agent import Agent
4
+
5
+ __all__ = ["Agent"]
6
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,211 @@
1
+ """claw-msg CLI — register, send, listen, serve, rooms, daemon."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import json
7
+ import sys
8
+
9
+ import click
10
+
11
+
12
+ @click.group()
13
+ def cli():
14
+ """claw-msg — Agent-to-agent messaging layer."""
15
+ pass
16
+
17
+
18
+ @cli.command()
19
+ @click.option("--host", default="0.0.0.0", help="Host to bind to")
20
+ @click.option("--port", default=8000, type=int, help="Port to bind to")
21
+ def serve(host: str, port: int):
22
+ """Start the claw-msg broker server."""
23
+ import uvicorn
24
+ from claw_msg.server.app import app
25
+
26
+ uvicorn.run(app, host=host, port=port)
27
+
28
+
29
+ @cli.command()
30
+ @click.option("--name", required=True, help="Agent name")
31
+ @click.option("--broker", required=True, help="Broker URL")
32
+ @click.option("--capabilities", default="", help="Comma-separated capabilities")
33
+ @click.option("--application", is_flag=True, help="Register as application agent")
34
+ def register(name: str, broker: str, capabilities: str, application: bool):
35
+ """Register a new agent with the broker."""
36
+
37
+ async def _register():
38
+ from claw_msg.client.agent import Agent
39
+
40
+ caps = [c.strip() for c in capabilities.split(",") if c.strip()] if capabilities else []
41
+ agent = Agent(broker, name=name, capabilities=caps)
42
+ agent_id = await agent.register()
43
+ click.echo(f"Registered: {agent_id}")
44
+ click.echo(f"Token: {agent.token}")
45
+ click.echo(f"Credentials saved to ~/.claw-msg/credentials.json")
46
+
47
+ asyncio.run(_register())
48
+
49
+
50
+ @cli.command()
51
+ @click.option("--to", "to_agent", required=True, help="Recipient agent ID")
52
+ @click.option("--broker", required=True, help="Broker URL")
53
+ @click.option("--token", required=True, help="Your agent token")
54
+ @click.argument("message")
55
+ def send(to_agent: str, broker: str, token: str, message: str):
56
+ """Send a message to another agent."""
57
+
58
+ async def _send():
59
+ from claw_msg.client.agent import Agent
60
+
61
+ agent = Agent(broker, token=token)
62
+ result = await agent.send(to_agent, message)
63
+ click.echo(f"Sent: {result.get('id', 'ok') if result else 'ok'}")
64
+
65
+ asyncio.run(_send())
66
+
67
+
68
+ @cli.command()
69
+ @click.option("--broker", required=True, help="Broker URL")
70
+ @click.option("--token", required=True, help="Your agent token")
71
+ def listen(broker: str, token: str):
72
+ """Listen for incoming messages via WebSocket."""
73
+
74
+ async def _listen():
75
+ from claw_msg.client.agent import Agent
76
+
77
+ agent = Agent(broker, token=token)
78
+
79
+ @agent.on_message
80
+ async def handle(msg):
81
+ click.echo(f"[{msg.get('from_agent', '?')}] {msg.get('content', '')}")
82
+
83
+ click.echo("Listening for messages (Ctrl+C to stop)...")
84
+ await agent.listen()
85
+
86
+ try:
87
+ asyncio.run(_listen())
88
+ except KeyboardInterrupt:
89
+ click.echo("\nStopped.")
90
+
91
+
92
+ @cli.group()
93
+ def rooms():
94
+ """Room management commands."""
95
+ pass
96
+
97
+
98
+ @rooms.command("create")
99
+ @click.option("--broker", required=True, help="Broker URL")
100
+ @click.option("--token", required=True, help="Your agent token")
101
+ @click.option("--name", required=True, help="Room name")
102
+ @click.option("--description", default="", help="Room description")
103
+ def room_create(broker: str, token: str, name: str, description: str):
104
+ """Create a new room."""
105
+
106
+ async def _create():
107
+ from claw_msg.client.agent import Agent
108
+
109
+ agent = Agent(broker, token=token)
110
+ result = await agent.create_room(name=name, description=description)
111
+ click.echo(f"Room created: {result.get('id', '')}")
112
+
113
+ asyncio.run(_create())
114
+
115
+
116
+ @rooms.command("join")
117
+ @click.option("--broker", required=True, help="Broker URL")
118
+ @click.option("--token", required=True, help="Your agent token")
119
+ @click.option("--room-id", required=True, help="Room ID to join")
120
+ def room_join(broker: str, token: str, room_id: str):
121
+ """Join an existing room."""
122
+
123
+ async def _join():
124
+ from claw_msg.client.agent import Agent
125
+
126
+ agent = Agent(broker, token=token)
127
+ await agent.join_room(room_id)
128
+ click.echo(f"Joined room: {room_id}")
129
+
130
+ asyncio.run(_join())
131
+
132
+
133
+ @rooms.command("leave")
134
+ @click.option("--broker", required=True, help="Broker URL")
135
+ @click.option("--token", required=True, help="Your agent token")
136
+ @click.option("--room-id", required=True, help="Room ID to leave")
137
+ def room_leave(broker: str, token: str, room_id: str):
138
+ """Leave a room."""
139
+
140
+ async def _leave():
141
+ from claw_msg.client.agent import Agent
142
+
143
+ agent = Agent(broker, token=token)
144
+ await agent.leave_room(room_id)
145
+ click.echo(f"Left room: {room_id}")
146
+
147
+ asyncio.run(_leave())
148
+
149
+
150
+ @cli.command()
151
+ @click.option("--broker", required=True, help="Broker URL")
152
+ @click.option("--token", required=True, help="Your agent token")
153
+ @click.option("--webhook", required=True, help="Webhook URL to forward messages to")
154
+ def daemon(broker: str, token: str, webhook: str):
155
+ """Run as a daemon — forward messages to a webhook URL."""
156
+
157
+ async def _run():
158
+ from claw_msg.daemon.runner import run_daemon
159
+
160
+ await run_daemon(broker, token, webhook)
161
+
162
+ try:
163
+ asyncio.run(_run())
164
+ except KeyboardInterrupt:
165
+ click.echo("\nDaemon stopped.")
166
+
167
+
168
+ @cli.command("install-service")
169
+ @click.option("--broker", required=True, help="Broker URL")
170
+ @click.option("--token", required=True, help="Your agent token")
171
+ @click.option("--webhook", required=True, help="Webhook URL")
172
+ @click.option("--type", "svc_type", type=click.Choice(["systemd", "launchd"]), default="systemd")
173
+ def install_service(broker: str, token: str, webhook: str, svc_type: str):
174
+ """Install a system service for the daemon."""
175
+ from claw_msg.daemon.service import install_launchd_service, install_systemd_service
176
+
177
+ if svc_type == "systemd":
178
+ path = install_systemd_service(broker, token, webhook)
179
+ click.echo(f"Systemd service installed: {path}")
180
+ click.echo("Run: systemctl --user enable --now claw-msg-daemon")
181
+ else:
182
+ path = install_launchd_service(broker, token, webhook)
183
+ click.echo(f"Launchd plist installed: {path}")
184
+ click.echo("Run: launchctl load ~/Library/LaunchAgents/com.claw-msg.daemon.plist")
185
+
186
+
187
+ @cli.command("agents")
188
+ @click.option("--broker", required=True, help="Broker URL")
189
+ @click.option("--name", default=None, help="Search by name")
190
+ def list_agents(broker: str, name: str | None):
191
+ """Search for agents on the broker."""
192
+
193
+ async def _search():
194
+ from claw_msg.client.http import HttpClient
195
+
196
+ http = HttpClient(broker, "")
197
+ async with __import__("httpx").AsyncClient() as c:
198
+ params = {}
199
+ if name:
200
+ params["name"] = name
201
+ resp = await c.get(f"{broker.rstrip('/')}/agents/", params=params)
202
+ resp.raise_for_status()
203
+ for agent in resp.json():
204
+ status = agent.get("status", "?")
205
+ click.echo(f" {agent['id']} {agent['name']} [{status}]")
206
+
207
+ asyncio.run(_search())
208
+
209
+
210
+ if __name__ == "__main__":
211
+ cli()
File without changes
@@ -0,0 +1,148 @@
1
+ """High-level Agent SDK — the main public interface for claw-msg."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from typing import Any, Callable, Coroutine
7
+
8
+ from claw_msg.client.connection import Connection
9
+ from claw_msg.client.credentials import store_credentials
10
+ from claw_msg.client.http import HttpClient
11
+
12
+
13
+ class Agent:
14
+ """
15
+ Agent SDK — register, send/receive messages, join rooms.
16
+
17
+ Usage::
18
+
19
+ agent = Agent("http://localhost:8000", name="my-agent")
20
+ await agent.register()
21
+ await agent.send("agent-id-here", "hello!")
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ broker_url: str,
27
+ name: str = "unnamed-agent",
28
+ capabilities: list[str] | None = None,
29
+ metadata: dict[str, Any] | None = None,
30
+ token: str | None = None,
31
+ agent_id: str | None = None,
32
+ ):
33
+ self._broker_url = broker_url.rstrip("/")
34
+ self._name = name
35
+ self._capabilities = capabilities or []
36
+ self._metadata = metadata or {}
37
+ self._token = token
38
+ self._agent_id = agent_id
39
+ self._connection: Connection | None = None
40
+ self._http: HttpClient | None = None
41
+ self._message_handlers: list[Callable[[dict], Coroutine]] = []
42
+
43
+ @property
44
+ def agent_id(self) -> str | None:
45
+ return self._agent_id
46
+
47
+ @property
48
+ def token(self) -> str | None:
49
+ return self._token
50
+
51
+ @property
52
+ def connected(self) -> bool:
53
+ return self._connection is not None and self._connection.connected
54
+
55
+ async def register(self) -> str:
56
+ """Register this agent with the broker. Returns agent_id."""
57
+ http = HttpClient(self._broker_url, "")
58
+ result = await http.register(
59
+ name=self._name,
60
+ capabilities=self._capabilities,
61
+ metadata=self._metadata,
62
+ )
63
+ self._agent_id = result["agent_id"]
64
+ self._token = result["token"]
65
+ self._http = HttpClient(self._broker_url, self._token)
66
+
67
+ store_credentials(self._broker_url, self._agent_id, self._token, self._name)
68
+ return self._agent_id
69
+
70
+ def on_message(self, handler: Callable[[dict], Coroutine]):
71
+ """Register a message handler (decorator-style)."""
72
+ self._message_handlers.append(handler)
73
+ return handler
74
+
75
+ async def _dispatch_message(self, msg: dict):
76
+ for handler in self._message_handlers:
77
+ await handler(msg)
78
+
79
+ async def connect(self):
80
+ """Establish WebSocket connection to the broker."""
81
+ if not self._token:
82
+ raise RuntimeError("Must register or provide a token before connecting")
83
+ self._connection = Connection(
84
+ self._broker_url,
85
+ self._token,
86
+ on_message=self._dispatch_message,
87
+ )
88
+ self._agent_id = await self._connection.connect()
89
+ self._http = HttpClient(self._broker_url, self._token)
90
+
91
+ async def listen(self):
92
+ """Start listening for messages (blocking). Auto-reconnects."""
93
+ if not self._token:
94
+ raise RuntimeError("Must register or provide a token before listening")
95
+ self._connection = Connection(
96
+ self._broker_url,
97
+ self._token,
98
+ on_message=self._dispatch_message,
99
+ )
100
+ await self._connection.listen()
101
+
102
+ async def send(self, to: str, content: str, content_type: str = "text/plain", reply_to: str | None = None) -> dict | None:
103
+ """Send a direct message. Uses WebSocket if connected, HTTP otherwise."""
104
+ if self._connection and self._connection.connected:
105
+ await self._connection.send_message(to=to, content=content, content_type=content_type, reply_to=reply_to)
106
+ return None
107
+ else:
108
+ return await self._get_http().send_message(to=to, content=content, content_type=content_type, reply_to=reply_to)
109
+
110
+ async def send_to_room(self, room_id: str, content: str, content_type: str = "text/plain") -> dict | None:
111
+ """Send a message to a room."""
112
+ if self._connection and self._connection.connected:
113
+ await self._connection.send_message(room_id=room_id, content=content, content_type=content_type)
114
+ return None
115
+ else:
116
+ return await self._get_http().send_message(room_id=room_id, content=content, content_type=content_type)
117
+
118
+ async def get_messages(self, since: str | None = None, limit: int = 50) -> list[dict]:
119
+ """Fetch message history via HTTP."""
120
+ return await self._get_http().get_messages(since=since, limit=limit)
121
+
122
+ async def search_agents(self, name: str | None = None, capability: str | None = None) -> list[dict]:
123
+ """Search for agents."""
124
+ return await self._get_http().search_agents(name=name, capability=capability)
125
+
126
+ async def create_room(self, name: str, description: str = "", max_members: int = 50) -> dict:
127
+ """Create a room."""
128
+ return await self._get_http().create_room(name=name, description=description, max_members=max_members)
129
+
130
+ async def join_room(self, room_id: str) -> dict:
131
+ """Join a room."""
132
+ return await self._get_http().join_room(room_id)
133
+
134
+ async def leave_room(self, room_id: str) -> dict:
135
+ """Leave a room."""
136
+ return await self._get_http().leave_room(room_id)
137
+
138
+ async def close(self):
139
+ """Close connection."""
140
+ if self._connection:
141
+ await self._connection.close()
142
+
143
+ def _get_http(self) -> HttpClient:
144
+ if not self._http:
145
+ if not self._token:
146
+ raise RuntimeError("Must register or provide a token")
147
+ self._http = HttpClient(self._broker_url, self._token)
148
+ return self._http