meshcode 2.10.36__tar.gz → 2.10.38__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.
- {meshcode-2.10.36 → meshcode-2.10.38}/PKG-INFO +1 -1
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/__init__.py +1 -1
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/comms_v4.py +39 -16
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/backend.py +41 -29
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/realtime.py +17 -5
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/server.py +167 -23
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode.egg-info/PKG-INFO +1 -1
- {meshcode-2.10.36 → meshcode-2.10.38}/pyproject.toml +1 -1
- {meshcode-2.10.36 → meshcode-2.10.38}/README.md +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/ascii_art.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/cli.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/invites.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/launcher.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/launcher_install.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/__init__.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/__main__.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/test_backend.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/test_realtime.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/meshcode_mcp/test_server_wrapper.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/preferences.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/protocol_v2.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/run_agent.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/secrets.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/self_update.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode/setup_clients.py +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode.egg-info/SOURCES.txt +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode.egg-info/dependency_links.txt +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode.egg-info/entry_points.txt +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode.egg-info/requires.txt +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/meshcode.egg-info/top_level.txt +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/setup.cfg +0 -0
- {meshcode-2.10.36 → meshcode-2.10.38}/tests/test_status_enum_coverage.py +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
"""MeshCode — Real-time communication between AI agents."""
|
|
2
|
-
__version__ = "2.10.
|
|
2
|
+
__version__ = "2.10.38"
|
|
@@ -615,7 +615,7 @@ def spawn_headless_agent(project, name, project_id, message_body, from_agent):
|
|
|
615
615
|
# The agent is identity-only at rest; next message wakes it again.
|
|
616
616
|
sb_update("mc_agents",
|
|
617
617
|
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
618
|
-
{"status": "sleeping", "task": "
|
|
618
|
+
{"status": "sleeping", "task": "Waiting for messages...",
|
|
619
619
|
"last_active_at": now_iso(), "last_heartbeat": now_iso()})
|
|
620
620
|
except Exception:
|
|
621
621
|
pass
|
|
@@ -865,7 +865,7 @@ def register(project, name, role=""):
|
|
|
865
865
|
ensure_sessions()
|
|
866
866
|
project_id = get_project_id(project)
|
|
867
867
|
if not project_id:
|
|
868
|
-
print(f"[ERROR]
|
|
868
|
+
print(f"[ERROR] Could not create/find project '{project}'")
|
|
869
869
|
return
|
|
870
870
|
|
|
871
871
|
ppid = os.getppid()
|
|
@@ -946,7 +946,7 @@ def register(project, name, role=""):
|
|
|
946
946
|
order="created_at.desc", limit=10)
|
|
947
947
|
if recent:
|
|
948
948
|
recent.reverse()
|
|
949
|
-
print(f"\n[COMMS]
|
|
949
|
+
print(f"\n[COMMS] Last {len(recent)} team messages:")
|
|
950
950
|
for msg in recent:
|
|
951
951
|
ts = msg.get("created_at", "")[-8:]
|
|
952
952
|
fr = msg["from_agent"]
|
|
@@ -1132,7 +1132,7 @@ def watch(project, name, interval=10, timeout=0):
|
|
|
1132
1132
|
# Update status to standby
|
|
1133
1133
|
sb_update("mc_agents",
|
|
1134
1134
|
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
1135
|
-
{"status": "standby", "task": "
|
|
1135
|
+
{"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
|
|
1136
1136
|
|
|
1137
1137
|
cycle = timeout if timeout > 0 else 600
|
|
1138
1138
|
print(f"[{project}] {name} en watch — poll cada {interval}s, ciclo {cycle}s (auto-loop)")
|
|
@@ -1161,10 +1161,10 @@ def watch(project, name, interval=10, timeout=0):
|
|
|
1161
1161
|
time.sleep(interval)
|
|
1162
1162
|
|
|
1163
1163
|
# Cycle ended without messages — check user context, then AUTO-RESTART watch
|
|
1164
|
-
print(f"[{project}]
|
|
1164
|
+
print(f"[{project}] No messages this cycle. Watch active, still running. Standby.")
|
|
1165
1165
|
sb_update("mc_agents",
|
|
1166
1166
|
f"project_id=eq.{project_id}&name=eq.{quote(name)}",
|
|
1167
|
-
{"status": "standby", "task": "
|
|
1167
|
+
{"status": "standby", "task": "Waiting for messages...", "last_heartbeat": now_iso()})
|
|
1168
1168
|
|
|
1169
1169
|
|
|
1170
1170
|
def update_board(project, name, status, task=""):
|
|
@@ -1183,7 +1183,7 @@ def update_board(project, name, status, task=""):
|
|
|
1183
1183
|
print(f"[{project}] {name}: NO puedes estar idle — tienes {count} mensaje(s) pendientes.")
|
|
1184
1184
|
print(f"[{project}] Ejecuta: python3 ~/Desktop/meshcode/comms_v4.py read {project} {name}")
|
|
1185
1185
|
status = "working"
|
|
1186
|
-
task = f"{count}
|
|
1186
|
+
task = f"{count} pending messages"
|
|
1187
1187
|
|
|
1188
1188
|
updates = {"status": status, "last_heartbeat": now_iso()}
|
|
1189
1189
|
if task:
|
|
@@ -1197,7 +1197,7 @@ def update_board(project, name, status, task=""):
|
|
|
1197
1197
|
def show_board(project):
|
|
1198
1198
|
project_id = get_project_id(project)
|
|
1199
1199
|
if not project_id:
|
|
1200
|
-
print(f"[{project}]
|
|
1200
|
+
print(f"[{project}] Project not found")
|
|
1201
1201
|
return
|
|
1202
1202
|
|
|
1203
1203
|
agents = sb_select("mc_agents", f"project_id=eq.{project_id}", order="registered_at.asc")
|
|
@@ -1221,7 +1221,7 @@ def show_status(project=None):
|
|
|
1221
1221
|
projects_data = sb_select("mc_projects", "", order="created_at.asc")
|
|
1222
1222
|
|
|
1223
1223
|
if not projects_data:
|
|
1224
|
-
print("[COMMS]
|
|
1224
|
+
print("[COMMS] No active projects")
|
|
1225
1225
|
return
|
|
1226
1226
|
|
|
1227
1227
|
print(f"\n{'='*60}")
|
|
@@ -1266,7 +1266,7 @@ def show_history(project, last_n=20, between=None):
|
|
|
1266
1266
|
messages.reverse()
|
|
1267
1267
|
|
|
1268
1268
|
print(f"\n{'='*60}")
|
|
1269
|
-
print(f" {project.upper()} —
|
|
1269
|
+
print(f" {project.upper()} — History ({len(messages)} messages)")
|
|
1270
1270
|
if between:
|
|
1271
1271
|
print(f" Filtro: {between}")
|
|
1272
1272
|
print(f"{'='*60}\n")
|
|
@@ -1472,7 +1472,7 @@ def connect_terminal(project, name, role=""):
|
|
|
1472
1472
|
ensure_sessions()
|
|
1473
1473
|
project_id = get_project_id(project)
|
|
1474
1474
|
if not project_id:
|
|
1475
|
-
print(f"[ERROR]
|
|
1475
|
+
print(f"[ERROR] Could not create/find project '{project}'")
|
|
1476
1476
|
return
|
|
1477
1477
|
|
|
1478
1478
|
tty, owning = _capture_tty_for_pid(os.getppid())
|
|
@@ -1542,7 +1542,7 @@ def disconnect_terminal(project, name):
|
|
|
1542
1542
|
"""Mark agent offline, clear tty/pid, stop heartbeat, remove session file."""
|
|
1543
1543
|
project_id = get_project_id(project)
|
|
1544
1544
|
if not project_id:
|
|
1545
|
-
print(f"[ERROR]
|
|
1545
|
+
print(f"[ERROR] project '{project}' not found")
|
|
1546
1546
|
return
|
|
1547
1547
|
rpc_result = sb_rpc("mc_disconnect_agent", {
|
|
1548
1548
|
"p_project_id": project_id,
|
|
@@ -2177,7 +2177,7 @@ if __name__ == "__main__":
|
|
|
2177
2177
|
sys.exit(1)
|
|
2178
2178
|
project_id = get_project_id(proj)
|
|
2179
2179
|
if not project_id:
|
|
2180
|
-
print(f"[ERROR]
|
|
2180
|
+
print(f"[ERROR] project '{proj}' not found")
|
|
2181
2181
|
sys.exit(1)
|
|
2182
2182
|
ok = spawn_headless_agent(proj, name, project_id,
|
|
2183
2183
|
"ping: synthetic wake-headless test message", "test-harness")
|
|
@@ -2193,7 +2193,7 @@ if __name__ == "__main__":
|
|
|
2193
2193
|
sys.exit(1)
|
|
2194
2194
|
project_id = get_project_id(proj)
|
|
2195
2195
|
if not project_id:
|
|
2196
|
-
print(f"[ERROR]
|
|
2196
|
+
print(f"[ERROR] project '{proj}' not found")
|
|
2197
2197
|
sys.exit(1)
|
|
2198
2198
|
rpc_name = {"kill": "mc_agent_kill", "wake": "mc_agent_wake", "sleep": "mc_agent_sleep"}[cmd]
|
|
2199
2199
|
result = sb_rpc(rpc_name, {"p_project_id": project_id, "p_agent_name": name})
|
|
@@ -2212,7 +2212,7 @@ if __name__ == "__main__":
|
|
|
2212
2212
|
sys.exit(1)
|
|
2213
2213
|
project_id = get_project_id(proj)
|
|
2214
2214
|
if not project_id:
|
|
2215
|
-
print(f"[ERROR]
|
|
2215
|
+
print(f"[ERROR] project '{proj}' not found")
|
|
2216
2216
|
sys.exit(1)
|
|
2217
2217
|
if sub == "get":
|
|
2218
2218
|
result = sb_rpc("mc_agent_get_profile", {"p_project_id": project_id, "p_agent_name": name})
|
|
@@ -2425,7 +2425,30 @@ if __name__ == "__main__":
|
|
|
2425
2425
|
sys.exit(1)
|
|
2426
2426
|
login(key)
|
|
2427
2427
|
|
|
2428
|
-
elif cmd
|
|
2428
|
+
elif cmd == "whoami":
|
|
2429
|
+
# Show the logged-in user identity from profile_meta.json
|
|
2430
|
+
meta_path = Path.home() / ".meshcode" / "profile_meta.json"
|
|
2431
|
+
if meta_path.exists():
|
|
2432
|
+
try:
|
|
2433
|
+
meta = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
2434
|
+
print()
|
|
2435
|
+
print(f"[meshcode] Logged in as:")
|
|
2436
|
+
if meta.get("email"):
|
|
2437
|
+
print(f" email: {meta['email']}")
|
|
2438
|
+
if meta.get("display_name"):
|
|
2439
|
+
print(f" display name: {meta['display_name']}")
|
|
2440
|
+
if meta.get("user_id"):
|
|
2441
|
+
print(f" user ID: {meta['user_id']}")
|
|
2442
|
+
active = os.environ.get("MESHCODE_KEYCHAIN_PROFILE") or "default"
|
|
2443
|
+
print(f" profile: {active}")
|
|
2444
|
+
print()
|
|
2445
|
+
except Exception:
|
|
2446
|
+
print("[meshcode] Could not read profile. Run `meshcode login <api_key>`.")
|
|
2447
|
+
else:
|
|
2448
|
+
print("[meshcode] Not logged in. Run `meshcode login <api_key>`.")
|
|
2449
|
+
sys.exit(0)
|
|
2450
|
+
|
|
2451
|
+
elif cmd == "profiles":
|
|
2429
2452
|
# List all stored keychain profiles with metadata
|
|
2430
2453
|
try:
|
|
2431
2454
|
import importlib as _il
|
|
@@ -105,69 +105,81 @@ SCHEMA = "meshcode"
|
|
|
105
105
|
# ── Persistent HTTPS Connection Pool ──────────────────────────────
|
|
106
106
|
# Each urlopen() opens a new TCP+TLS connection (~1-2s overhead).
|
|
107
107
|
# This pool reuses connections, cutting typical latency from ~3s to ~100ms.
|
|
108
|
+
# Pool of N connections (default 3) to avoid lock contention between the
|
|
109
|
+
# heartbeat thread, tool call handlers, and background recording thread.
|
|
108
110
|
class _ConnectionPool:
|
|
109
|
-
"""Thread-safe
|
|
111
|
+
"""Thread-safe HTTPS connection pool to Supabase (N connections, round-robin)."""
|
|
110
112
|
|
|
111
|
-
def __init__(self, url: str, max_idle: float = 30.0):
|
|
113
|
+
def __init__(self, url: str, pool_size: int = 3, max_idle: float = 30.0):
|
|
112
114
|
parsed = urlparse(url)
|
|
113
115
|
self._host = parsed.hostname
|
|
114
116
|
self._port = parsed.port or 443
|
|
115
|
-
self.
|
|
116
|
-
self.
|
|
117
|
+
self._pool_size = pool_size
|
|
118
|
+
self._conns: List[Optional[http.client.HTTPSConnection]] = [None] * pool_size
|
|
119
|
+
self._locks = [_threading.Lock() for _ in range(pool_size)]
|
|
120
|
+
self._last_used = [0.0] * pool_size
|
|
117
121
|
self._max_idle = max_idle
|
|
118
|
-
self._last_used = 0.0
|
|
119
122
|
self._ctx = ssl.create_default_context()
|
|
123
|
+
self._counter = 0
|
|
124
|
+
self._counter_lock = _threading.Lock()
|
|
125
|
+
|
|
126
|
+
def _pick_slot(self) -> int:
|
|
127
|
+
"""Round-robin slot selection."""
|
|
128
|
+
with self._counter_lock:
|
|
129
|
+
slot = self._counter % self._pool_size
|
|
130
|
+
self._counter += 1
|
|
131
|
+
return slot
|
|
120
132
|
|
|
121
|
-
def _get_conn(self) -> http.client.HTTPSConnection:
|
|
133
|
+
def _get_conn(self, slot: int) -> http.client.HTTPSConnection:
|
|
122
134
|
now = _time.monotonic()
|
|
123
|
-
if self.
|
|
124
|
-
|
|
125
|
-
if now - self._last_used > self._max_idle:
|
|
135
|
+
if self._conns[slot] is not None:
|
|
136
|
+
if now - self._last_used[slot] > self._max_idle:
|
|
126
137
|
try:
|
|
127
|
-
self.
|
|
138
|
+
self._conns[slot].close()
|
|
128
139
|
except Exception:
|
|
129
140
|
pass
|
|
130
|
-
self.
|
|
131
|
-
if self.
|
|
132
|
-
self.
|
|
141
|
+
self._conns[slot] = None
|
|
142
|
+
if self._conns[slot] is None:
|
|
143
|
+
self._conns[slot] = http.client.HTTPSConnection(
|
|
133
144
|
self._host, self._port, timeout=10, context=self._ctx
|
|
134
145
|
)
|
|
135
|
-
return self.
|
|
146
|
+
return self._conns[slot]
|
|
136
147
|
|
|
137
148
|
def request(self, method: str, path: str, body: Optional[bytes], headers: Dict[str, str]) -> tuple:
|
|
138
|
-
"""Returns (status, response_body_str). Thread-safe with
|
|
139
|
-
|
|
149
|
+
"""Returns (status, response_body_str). Thread-safe with per-slot locking."""
|
|
150
|
+
slot = self._pick_slot()
|
|
151
|
+
with self._locks[slot]:
|
|
140
152
|
for attempt in range(2):
|
|
141
|
-
conn = self._get_conn()
|
|
153
|
+
conn = self._get_conn(slot)
|
|
142
154
|
try:
|
|
143
155
|
conn.request(method, path, body=body, headers=headers)
|
|
144
156
|
resp = conn.getresponse()
|
|
145
157
|
data = resp.read().decode("utf-8")
|
|
146
|
-
self._last_used = _time.monotonic()
|
|
158
|
+
self._last_used[slot] = _time.monotonic()
|
|
147
159
|
return resp.status, data
|
|
148
160
|
except (http.client.RemoteDisconnected, BrokenPipeError,
|
|
149
161
|
ConnectionResetError, OSError) as e:
|
|
150
|
-
# Connection went stale — close and retry once
|
|
151
162
|
try:
|
|
152
|
-
self.
|
|
163
|
+
self._conns[slot].close()
|
|
153
164
|
except Exception:
|
|
154
165
|
pass
|
|
155
|
-
self.
|
|
166
|
+
self._conns[slot] = None
|
|
156
167
|
if attempt == 0:
|
|
157
168
|
continue
|
|
158
169
|
raise
|
|
159
170
|
|
|
160
171
|
def close(self):
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
for i in range(self._pool_size):
|
|
173
|
+
with self._locks[i]:
|
|
174
|
+
if self._conns[i]:
|
|
175
|
+
try:
|
|
176
|
+
self._conns[i].close()
|
|
177
|
+
except Exception:
|
|
178
|
+
pass
|
|
179
|
+
self._conns[i] = None
|
|
168
180
|
|
|
169
181
|
|
|
170
|
-
_pool = _ConnectionPool(SUPABASE_URL)
|
|
182
|
+
_pool = _ConnectionPool(SUPABASE_URL, pool_size=3)
|
|
171
183
|
|
|
172
184
|
|
|
173
185
|
def _now_iso() -> str:
|
|
@@ -53,9 +53,12 @@ class RealtimeListener:
|
|
|
53
53
|
self.notify_callback = notify_callback
|
|
54
54
|
self.service_role_key = service_role_key
|
|
55
55
|
|
|
56
|
-
#
|
|
57
|
-
|
|
56
|
+
# Message queue — drained by meshcode_check/meshcode_wait.
|
|
57
|
+
# Cap at 2000 (was 500). When full, oldest messages are dropped
|
|
58
|
+
# and _dropped_count is incremented so the agent knows.
|
|
59
|
+
self.queue: Deque[Dict] = deque(maxlen=2000)
|
|
58
60
|
self._overflow_warned = False
|
|
61
|
+
self._dropped_count = 0
|
|
59
62
|
self._task: Optional[asyncio.Task] = None
|
|
60
63
|
# asyncio.Event() in Py3.10+ no longer requires a running loop, but
|
|
61
64
|
# on older Python or certain Windows event-loop policies it can
|
|
@@ -272,9 +275,14 @@ class RealtimeListener:
|
|
|
272
275
|
"id": record.get("id"),
|
|
273
276
|
"parent_id": record.get("parent_msg_id"),
|
|
274
277
|
}
|
|
275
|
-
if len(self.queue) >=
|
|
276
|
-
|
|
277
|
-
self.
|
|
278
|
+
if len(self.queue) >= self.queue.maxlen:
|
|
279
|
+
# Queue is full — this append will drop the oldest message
|
|
280
|
+
self._dropped_count += 1
|
|
281
|
+
if not self._overflow_warned:
|
|
282
|
+
log.warning(f"Message queue FULL ({self.queue.maxlen}) — dropping oldest messages ({self._dropped_count} dropped so far)")
|
|
283
|
+
self._overflow_warned = True
|
|
284
|
+
elif len(self.queue) >= self.queue.maxlen * 0.8 and not self._overflow_warned:
|
|
285
|
+
log.warning(f"Message queue at {len(self.queue)}/{self.queue.maxlen} — approaching limit")
|
|
278
286
|
self.queue.append(enriched)
|
|
279
287
|
# Wake any meshcode_wait blocked on this event.
|
|
280
288
|
try:
|
|
@@ -327,6 +335,10 @@ class RealtimeListener:
|
|
|
327
335
|
await self.stop()
|
|
328
336
|
await self.start()
|
|
329
337
|
|
|
338
|
+
@property
|
|
339
|
+
def dropped_count(self) -> int:
|
|
340
|
+
return self._dropped_count
|
|
341
|
+
|
|
330
342
|
@property
|
|
331
343
|
def is_connected(self) -> bool:
|
|
332
344
|
return self._connected
|
|
@@ -601,7 +601,9 @@ def with_working_status(func):
|
|
|
601
601
|
global _CONSECUTIVE_IDLE_SECONDS
|
|
602
602
|
_CONSECUTIVE_IDLE_SECONDS = 0 # any non-wait tool resets idle timer
|
|
603
603
|
_set_state("working", name)
|
|
604
|
-
|
|
604
|
+
# Estimate input tokens (chars/4) for usage tracking
|
|
605
|
+
_est_tokens = sum(len(str(v)) for v in kwargs.values()) // 4
|
|
606
|
+
_record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys()), "estimated_tokens": _est_tokens})
|
|
605
607
|
try:
|
|
606
608
|
return await func(*args, **kwargs)
|
|
607
609
|
except Exception as e:
|
|
@@ -622,7 +624,8 @@ def with_working_status(func):
|
|
|
622
624
|
global _CONSECUTIVE_IDLE_SECONDS
|
|
623
625
|
_CONSECUTIVE_IDLE_SECONDS = 0 # any non-wait tool resets idle timer
|
|
624
626
|
_set_state("working", name)
|
|
625
|
-
|
|
627
|
+
_est_tokens = sum(len(str(v)) for v in kwargs.values()) // 4
|
|
628
|
+
_record_event_bg("tool_call", {"tool": name, "args_keys": list(kwargs.keys()), "estimated_tokens": _est_tokens})
|
|
626
629
|
try:
|
|
627
630
|
return func(*args, **kwargs)
|
|
628
631
|
except Exception as e:
|
|
@@ -864,6 +867,19 @@ except Exception as _e:
|
|
|
864
867
|
log.warning(f"could not fetch agent profile: {_e}")
|
|
865
868
|
|
|
866
869
|
|
|
870
|
+
# ── Leader/commander detection (single source of truth) ───────
|
|
871
|
+
_LEADER_KEYWORDS = (
|
|
872
|
+
'commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
|
|
873
|
+
'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
|
|
874
|
+
'chief', 'captain', 'boss', 'head agent',
|
|
875
|
+
)
|
|
876
|
+
|
|
877
|
+
def _is_leader_agent() -> bool:
|
|
878
|
+
"""Check if this agent is a leader/commander based on name + role keywords."""
|
|
879
|
+
haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
|
|
880
|
+
return any(k in haystack for k in _LEADER_KEYWORDS)
|
|
881
|
+
|
|
882
|
+
|
|
867
883
|
def _build_instructions() -> str:
|
|
868
884
|
"""Build the system-instructions block injected via the MCP InitializeResult.
|
|
869
885
|
|
|
@@ -939,15 +955,7 @@ what CLI command to run next (e.g. "meshcode run backend in a new terminal").
|
|
|
939
955
|
Setup help → README.md or https://meshcode.io/docs
|
|
940
956
|
"""
|
|
941
957
|
# Inject commander protocol if this agent is a leader
|
|
942
|
-
|
|
943
|
-
# Substring match: "orchestrator" hits on "orchestrat", "coordinador" hits on "coordinat".
|
|
944
|
-
_leader_haystack = ((_ROLE_DESCRIPTION or '') + ' ' + AGENT_NAME).lower()
|
|
945
|
-
_LEADER_KEYWORDS = (
|
|
946
|
-
'commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
|
|
947
|
-
'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
|
|
948
|
-
'chief', 'captain', 'boss', 'head agent',
|
|
949
|
-
)
|
|
950
|
-
is_leader = any(k in _leader_haystack for k in _LEADER_KEYWORDS)
|
|
958
|
+
is_leader = _is_leader_agent()
|
|
951
959
|
if is_leader:
|
|
952
960
|
base += """
|
|
953
961
|
COMMANDER PROTOCOL (you are the team lead):
|
|
@@ -1507,6 +1515,88 @@ def meshcode_read(include_acks: bool = False) -> Dict[str, Any]:
|
|
|
1507
1515
|
return split
|
|
1508
1516
|
|
|
1509
1517
|
|
|
1518
|
+
@mcp.tool()
|
|
1519
|
+
@with_working_status
|
|
1520
|
+
async def meshcode_call(to: str, function: str, args: Any = None, timeout_seconds: int = 30) -> Dict[str, Any]:
|
|
1521
|
+
"""Synchronous RPC call to another agent. Blocks until response (max 30s).
|
|
1522
|
+
|
|
1523
|
+
Sends a structured request, waits for the callee to respond with a matching
|
|
1524
|
+
call_id. Like gRPC over the mesh — enables agents to ask each other questions
|
|
1525
|
+
and get structured answers.
|
|
1526
|
+
|
|
1527
|
+
Args:
|
|
1528
|
+
to: Target agent name.
|
|
1529
|
+
function: Function/action name the callee should execute.
|
|
1530
|
+
args: Arguments (any JSON-serializable value).
|
|
1531
|
+
timeout_seconds: Max wait time (default 30, max 60).
|
|
1532
|
+
"""
|
|
1533
|
+
import uuid as _uuid
|
|
1534
|
+
if not to or not to.strip():
|
|
1535
|
+
return {"error": "recipient 'to' cannot be empty"}
|
|
1536
|
+
call_id = str(_uuid.uuid4())
|
|
1537
|
+
timeout_seconds = min(max(timeout_seconds, 5), 60)
|
|
1538
|
+
|
|
1539
|
+
# Send the RPC request
|
|
1540
|
+
request_payload = {
|
|
1541
|
+
"type": "rpc_request",
|
|
1542
|
+
"call_id": call_id,
|
|
1543
|
+
"function": function,
|
|
1544
|
+
"args": args,
|
|
1545
|
+
"from": AGENT_NAME,
|
|
1546
|
+
"timeout": timeout_seconds,
|
|
1547
|
+
}
|
|
1548
|
+
send_result = be.send_message(
|
|
1549
|
+
_PROJECT_ID, AGENT_NAME, to, request_payload,
|
|
1550
|
+
msg_type="rpc", api_key=_get_api_key()
|
|
1551
|
+
)
|
|
1552
|
+
if not send_result.get("sent") and send_result.get("error"):
|
|
1553
|
+
return {"error": f"failed to send RPC request: {send_result['error']}"}
|
|
1554
|
+
|
|
1555
|
+
# Poll for response with matching call_id
|
|
1556
|
+
import asyncio as _asyncio
|
|
1557
|
+
poll_interval = 1.0
|
|
1558
|
+
elapsed = 0.0
|
|
1559
|
+
while elapsed < timeout_seconds:
|
|
1560
|
+
await _asyncio.sleep(poll_interval)
|
|
1561
|
+
elapsed += poll_interval
|
|
1562
|
+
|
|
1563
|
+
# Check Realtime buffer first
|
|
1564
|
+
if _REALTIME:
|
|
1565
|
+
for msg in _REALTIME.peek():
|
|
1566
|
+
p = msg.get("payload") or {}
|
|
1567
|
+
if isinstance(p, dict) and p.get("type") == "rpc_response" and p.get("call_id") == call_id:
|
|
1568
|
+
# Found response — drain it
|
|
1569
|
+
_REALTIME.drain()
|
|
1570
|
+
return {
|
|
1571
|
+
"ok": True,
|
|
1572
|
+
"call_id": call_id,
|
|
1573
|
+
"from": msg.get("from"),
|
|
1574
|
+
"result": p.get("result"),
|
|
1575
|
+
"error": p.get("error"),
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
# Fallback: check DB
|
|
1579
|
+
try:
|
|
1580
|
+
raw = be.read_inbox(_PROJECT_ID, AGENT_NAME, mark_read=True, api_key=_get_api_key())
|
|
1581
|
+
for m in (raw if isinstance(raw, list) else raw.get("messages", []) if isinstance(raw, dict) else []):
|
|
1582
|
+
p = m.get("payload") or {}
|
|
1583
|
+
if isinstance(p, dict) and p.get("type") == "rpc_response" and p.get("call_id") == call_id:
|
|
1584
|
+
return {
|
|
1585
|
+
"ok": True,
|
|
1586
|
+
"call_id": call_id,
|
|
1587
|
+
"from": m.get("from_agent") or m.get("from"),
|
|
1588
|
+
"result": p.get("result"),
|
|
1589
|
+
"error": p.get("error"),
|
|
1590
|
+
}
|
|
1591
|
+
except Exception:
|
|
1592
|
+
pass
|
|
1593
|
+
|
|
1594
|
+
# Exponential backoff on polling (1s → 2s → 3s, cap 3s)
|
|
1595
|
+
poll_interval = min(poll_interval + 0.5, 3.0)
|
|
1596
|
+
|
|
1597
|
+
return {"error": f"RPC call to {to}.{function} timed out after {timeout_seconds}s", "call_id": call_id}
|
|
1598
|
+
|
|
1599
|
+
|
|
1510
1600
|
@mcp.tool()
|
|
1511
1601
|
@with_working_status
|
|
1512
1602
|
def meshcode_history(limit: int = 20, agent_filter: Optional[str] = None) -> Dict[str, Any]:
|
|
@@ -1612,11 +1702,7 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
|
|
|
1612
1702
|
]
|
|
1613
1703
|
# For leader agents: also include unclaimed '*' tasks as pending
|
|
1614
1704
|
# so commanders auto-triage them instead of letting them pile up.
|
|
1615
|
-
|
|
1616
|
-
_LEADER_KW = ('commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
|
|
1617
|
-
'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
|
|
1618
|
-
'chief', 'captain', 'boss', 'head agent')
|
|
1619
|
-
is_leader = any(k in _leader_haystack for k in _LEADER_KW)
|
|
1705
|
+
is_leader = _is_leader_agent()
|
|
1620
1706
|
if is_leader:
|
|
1621
1707
|
wildcard_tasks = [
|
|
1622
1708
|
{"id": t["id"][:8], "title": t["title"][:80], "priority": t.get("priority", "normal"), "status": t["status"]}
|
|
@@ -1626,6 +1712,9 @@ def _get_pending_tasks_summary() -> Optional[List[Dict[str, str]]]:
|
|
|
1626
1712
|
and not t.get("claimed_by")
|
|
1627
1713
|
]
|
|
1628
1714
|
pending.extend(wildcard_tasks)
|
|
1715
|
+
# Sort by priority so urgent tasks surface first
|
|
1716
|
+
_PRIORITY_ORDER = {"urgent": 0, "high": 1, "normal": 2, "low": 3}
|
|
1717
|
+
pending.sort(key=lambda t: _PRIORITY_ORDER.get(t.get("priority", "normal"), 2))
|
|
1629
1718
|
return pending if pending else None
|
|
1630
1719
|
except Exception:
|
|
1631
1720
|
return None
|
|
@@ -1655,12 +1744,7 @@ async def meshcode_wait(timeout_seconds: int = 20, include_acks: bool = False) -
|
|
|
1655
1744
|
# delegate tasks and need to stay in the wait loop to receive reports.
|
|
1656
1745
|
pending_tasks = _get_pending_tasks_summary()
|
|
1657
1746
|
if pending_tasks:
|
|
1658
|
-
|
|
1659
|
-
_LEADER_KW = ('commander', 'lead', 'orchestrat', 'coordinator', 'coordinat',
|
|
1660
|
-
'coordinad', 'jefe', 'líder', 'lider', 'director', 'manager',
|
|
1661
|
-
'chief', 'captain', 'boss', 'head agent')
|
|
1662
|
-
_is_leader = any(k in _leader_haystack for k in _LEADER_KW)
|
|
1663
|
-
if not _is_leader:
|
|
1747
|
+
if not _is_leader_agent():
|
|
1664
1748
|
return {
|
|
1665
1749
|
"refused": True,
|
|
1666
1750
|
"reason": "You have open tasks. Work them before entering wait.",
|
|
@@ -2267,6 +2351,49 @@ def meshcode_task_reassign(task_id: str, new_assignee: str) -> Dict[str, Any]:
|
|
|
2267
2351
|
})
|
|
2268
2352
|
|
|
2269
2353
|
|
|
2354
|
+
# ----------------- SCHEDULED TASKS -----------------
|
|
2355
|
+
|
|
2356
|
+
@mcp.tool()
|
|
2357
|
+
@with_working_status
|
|
2358
|
+
def meshcode_schedule(title: str, cron_expression: str, assignee: str = "*",
|
|
2359
|
+
description: str = "", priority: str = "normal") -> Dict[str, Any]:
|
|
2360
|
+
"""Create a recurring scheduled task. Fires on cron schedule.
|
|
2361
|
+
|
|
2362
|
+
Args:
|
|
2363
|
+
title: Task title (created each time it fires).
|
|
2364
|
+
cron_expression: Standard cron (e.g. "0 9 * * 1-5" = 9am weekdays).
|
|
2365
|
+
assignee: Agent name or "*" for any.
|
|
2366
|
+
description: Task description template.
|
|
2367
|
+
priority: low/normal/high/urgent.
|
|
2368
|
+
"""
|
|
2369
|
+
if not title or not title.strip():
|
|
2370
|
+
return {"error": "title cannot be empty"}
|
|
2371
|
+
if not cron_expression or not cron_expression.strip():
|
|
2372
|
+
return {"error": "cron_expression cannot be empty"}
|
|
2373
|
+
api_key = _get_api_key()
|
|
2374
|
+
return be.sb_rpc("mc_schedule_create", {
|
|
2375
|
+
"p_api_key": api_key,
|
|
2376
|
+
"p_project_id": _PROJECT_ID,
|
|
2377
|
+
"p_creator_agent": AGENT_NAME,
|
|
2378
|
+
"p_title": title.strip(),
|
|
2379
|
+
"p_cron_expression": cron_expression.strip(),
|
|
2380
|
+
"p_description": description,
|
|
2381
|
+
"p_assignee": assignee,
|
|
2382
|
+
"p_priority": priority,
|
|
2383
|
+
})
|
|
2384
|
+
|
|
2385
|
+
|
|
2386
|
+
@mcp.tool()
|
|
2387
|
+
@with_working_status
|
|
2388
|
+
def meshcode_schedule_list() -> Dict[str, Any]:
|
|
2389
|
+
"""List all scheduled/recurring tasks for this meshwork."""
|
|
2390
|
+
api_key = _get_api_key()
|
|
2391
|
+
return be.sb_rpc("mc_schedule_list", {
|
|
2392
|
+
"p_api_key": api_key,
|
|
2393
|
+
"p_project_id": _PROJECT_ID,
|
|
2394
|
+
})
|
|
2395
|
+
|
|
2396
|
+
|
|
2270
2397
|
# ----------------- PROACTIVE HEALTH SCAN -----------------
|
|
2271
2398
|
|
|
2272
2399
|
@mcp.tool()
|
|
@@ -2671,12 +2798,29 @@ def meshcode_recall(key: Optional[str] = None) -> Dict[str, Any]:
|
|
|
2671
2798
|
"p_key": key,
|
|
2672
2799
|
})
|
|
2673
2800
|
else:
|
|
2674
|
-
|
|
2801
|
+
# Return critical tier (full content) + counts of other tiers.
|
|
2802
|
+
# Episodic memories are large and should be searched on demand
|
|
2803
|
+
# via meshcode_recall_search(), not dumped into context.
|
|
2804
|
+
critical = be.sb_rpc("mc_memory_list", {
|
|
2675
2805
|
"p_api_key": api_key,
|
|
2676
2806
|
"p_agent_name": AGENT_NAME,
|
|
2677
2807
|
"p_tier": "critical",
|
|
2678
2808
|
"p_project_name": PROJECT_NAME,
|
|
2679
2809
|
})
|
|
2810
|
+
# Also fetch reference tier keys (lightweight, useful context)
|
|
2811
|
+
reference = be.sb_rpc("mc_memory_list", {
|
|
2812
|
+
"p_api_key": api_key,
|
|
2813
|
+
"p_agent_name": AGENT_NAME,
|
|
2814
|
+
"p_tier": "reference",
|
|
2815
|
+
"p_project_name": PROJECT_NAME,
|
|
2816
|
+
})
|
|
2817
|
+
if isinstance(critical, dict) and critical.get("ok"):
|
|
2818
|
+
ref_keys = []
|
|
2819
|
+
if isinstance(reference, dict) and reference.get("ok"):
|
|
2820
|
+
ref_keys = [m.get("key") for m in reference.get("memories", [])]
|
|
2821
|
+
critical["reference_keys"] = ref_keys
|
|
2822
|
+
critical["tip"] = "Use meshcode_recall_search(query) to search episodic memories."
|
|
2823
|
+
return critical
|
|
2680
2824
|
|
|
2681
2825
|
|
|
2682
2826
|
@mcp.tool()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|