zwarm 1.3.9__py3-none-any.whl → 1.3.11__py3-none-any.whl

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.
@@ -443,9 +443,55 @@ class MCPClient:
443
443
  if text:
444
444
  agent_messages.append(text)
445
445
 
446
+ elif msg_type == "agent_message":
447
+ # Agent message output (common in newer codex versions)
448
+ text = msg.get("text", "") or msg.get("content", "") or msg.get("message", "")
449
+ if text:
450
+ agent_messages.append(text)
451
+
452
+ elif msg_type == "output":
453
+ # Direct output event
454
+ text = msg.get("output", "") or msg.get("text", "") or msg.get("content", "")
455
+ if text:
456
+ agent_messages.append(text)
457
+
458
+ elif msg_type in ("item.completed", "response.completed"):
459
+ # Completion events may contain the final response
460
+ item = msg.get("item", {})
461
+ if item.get("type") == "agent_message":
462
+ text = item.get("text", "")
463
+ if text:
464
+ agent_messages.append(text)
465
+ elif "text" in msg:
466
+ agent_messages.append(msg["text"])
467
+ elif "content" in msg:
468
+ content = msg["content"]
469
+ if isinstance(content, str):
470
+ agent_messages.append(content)
471
+ elif isinstance(content, list):
472
+ for block in content:
473
+ if isinstance(block, dict) and block.get("text"):
474
+ agent_messages.append(block["text"])
475
+
446
476
  else:
447
- # Log unknown event types at debug level to help diagnose
448
- if msg_type and msg_type not in ("session_started", "thinking", "tool_call", "function_call"):
477
+ # Try to extract text from unknown event types as fallback
478
+ extracted = None
479
+ for key in ("text", "content", "message", "output", "response"):
480
+ if key in msg:
481
+ val = msg[key]
482
+ if isinstance(val, str) and val.strip():
483
+ extracted = val
484
+ break
485
+ elif isinstance(val, list):
486
+ texts = [b.get("text", "") if isinstance(b, dict) else str(b) for b in val]
487
+ if any(texts):
488
+ extracted = "\n".join(t for t in texts if t)
489
+ break
490
+
491
+ if extracted:
492
+ agent_messages.append(extracted)
493
+ logger.debug(f"Extracted text from event type '{msg_type}': {len(extracted)} chars")
494
+ elif msg_type and msg_type not in ("session_started", "thinking", "tool_call", "function_call", "reasoning", "function_call_output"):
449
495
  logger.debug(f"Unhandled MCP event type: {msg_type}, msg keys: {list(msg.keys())}")
450
496
 
451
497
  # Merge streaming text into messages if we got any
zwarm/cli/main.py CHANGED
@@ -2023,6 +2023,15 @@ def session_clean(
2023
2023
  # Main callback and entry point
2024
2024
  # =============================================================================
2025
2025
 
2026
+ def _get_version() -> str:
2027
+ """Get version from package metadata."""
2028
+ try:
2029
+ from importlib.metadata import version as get_pkg_version
2030
+ return get_pkg_version("zwarm")
2031
+ except Exception:
2032
+ return "0.0.0"
2033
+
2034
+
2026
2035
  @app.callback(invoke_without_command=True)
2027
2036
  def main_callback(
2028
2037
  ctx: typer.Context,
@@ -2030,7 +2039,7 @@ def main_callback(
2030
2039
  ):
2031
2040
  """Main callback for version flag."""
2032
2041
  if version:
2033
- console.print("[bold cyan]zwarm[/] version [green]0.1.0[/]")
2042
+ console.print(f"[bold cyan]zwarm[/] version [green]{_get_version()}[/]")
2034
2043
  raise typer.Exit()
2035
2044
 
2036
2045
 
zwarm/watchers/builtin.py CHANGED
@@ -7,10 +7,99 @@ from __future__ import annotations
7
7
  import re
8
8
  from typing import Any
9
9
 
10
+ from wbal.helper import TOOL_CALL_TYPE, TOOL_RESULT_TYPE
10
11
  from zwarm.watchers.base import Watcher, WatcherContext, WatcherResult, WatcherAction
11
12
  from zwarm.watchers.registry import register_watcher
12
13
 
13
14
 
15
+ def _get_field(item: Any, name: str, default: Any = None) -> Any:
16
+ if isinstance(item, dict):
17
+ return item.get(name, default)
18
+ return getattr(item, name, default)
19
+
20
+
21
+ def _content_to_text(content: Any) -> str:
22
+ if content is None:
23
+ return ""
24
+ if isinstance(content, str):
25
+ return content
26
+ if isinstance(content, list):
27
+ parts = []
28
+ for part in content:
29
+ text = _content_to_text(part)
30
+ if text:
31
+ parts.append(text)
32
+ return "\n".join(parts)
33
+ if isinstance(content, dict):
34
+ text = content.get("text") or content.get("content") or content.get("refusal")
35
+ return str(text) if text is not None else ""
36
+ text = getattr(content, "text", None)
37
+ if text is None:
38
+ text = getattr(content, "refusal", None)
39
+ return str(text) if text is not None else ""
40
+
41
+
42
+ def _normalize_tool_call(tool_call: Any) -> dict[str, Any]:
43
+ if isinstance(tool_call, dict):
44
+ if isinstance(tool_call.get("function"), dict):
45
+ return tool_call
46
+ name = tool_call.get("name", "")
47
+ arguments = tool_call.get("arguments", "")
48
+ call_id = tool_call.get("call_id")
49
+ else:
50
+ name = getattr(tool_call, "name", "")
51
+ arguments = getattr(tool_call, "arguments", "")
52
+ call_id = getattr(tool_call, "call_id", None)
53
+
54
+ normalized = {
55
+ "type": "function",
56
+ "function": {"name": name, "arguments": arguments},
57
+ }
58
+ if call_id:
59
+ normalized["call_id"] = call_id
60
+ return normalized
61
+
62
+
63
+ def _normalize_messages(messages: list[Any]) -> list[dict[str, Any]]:
64
+ normalized: list[dict[str, Any]] = []
65
+ for item in messages:
66
+ item_type = _get_field(item, "type")
67
+ role = _get_field(item, "role")
68
+ content = ""
69
+ tool_calls: list[dict[str, Any]] = []
70
+
71
+ if item_type in (TOOL_CALL_TYPE, "function_call"):
72
+ tool_calls = [_normalize_tool_call(item)]
73
+ role = role or "assistant"
74
+ else:
75
+ raw_tool_calls = _get_field(item, "tool_calls") or []
76
+ if raw_tool_calls and not isinstance(raw_tool_calls, list):
77
+ raw_tool_calls = [raw_tool_calls]
78
+ if raw_tool_calls:
79
+ tool_calls = [
80
+ _normalize_tool_call(tc) for tc in raw_tool_calls
81
+ ]
82
+
83
+ if role or item_type == "message" or item_type is None:
84
+ content = _content_to_text(_get_field(item, "content"))
85
+
86
+ if item_type == TOOL_RESULT_TYPE and not content:
87
+ content = _content_to_text(_get_field(item, "output"))
88
+ role = role or "tool"
89
+
90
+ if not role and not content and not tool_calls:
91
+ continue
92
+
93
+ normalized.append(
94
+ {
95
+ "role": role,
96
+ "content": content or "",
97
+ "tool_calls": tool_calls,
98
+ }
99
+ )
100
+ return normalized
101
+
102
+
14
103
  @register_watcher("progress")
15
104
  class ProgressWatcher(Watcher):
16
105
  """
@@ -26,14 +115,15 @@ class ProgressWatcher(Watcher):
26
115
  description = "Detects when agent is stuck or spinning"
27
116
 
28
117
  async def observe(self, ctx: WatcherContext) -> WatcherResult:
118
+ messages = _normalize_messages(ctx.messages)
29
119
  config = self.config
30
120
  max_same_calls = config.get("max_same_calls", 3)
31
121
  min_progress_steps = config.get("min_progress_steps", 5)
32
122
 
33
123
  # Check for repeated tool calls
34
- if len(ctx.messages) >= max_same_calls * 2:
124
+ if len(messages) >= max_same_calls * 2:
35
125
  recent_assistant = [
36
- m for m in ctx.messages[-max_same_calls * 2 :]
126
+ m for m in messages[-max_same_calls * 2 :]
37
127
  if m.get("role") == "assistant"
38
128
  ]
39
129
  if len(recent_assistant) >= max_same_calls:
@@ -144,9 +234,10 @@ class ScopeWatcher(Watcher):
144
234
 
145
235
  # Check last few messages for avoid keywords
146
236
  if avoid_keywords:
237
+ messages = _normalize_messages(ctx.messages)
147
238
  recent_content = " ".join(
148
239
  m.get("content", "") or ""
149
- for m in ctx.messages[-max_tangent_steps * 2:]
240
+ for m in messages[-max_tangent_steps * 2:]
150
241
  ).lower()
151
242
 
152
243
  for keyword in avoid_keywords:
@@ -174,6 +265,7 @@ class PatternWatcher(Watcher):
174
265
  description = "Watches for configurable patterns in output"
175
266
 
176
267
  async def observe(self, ctx: WatcherContext) -> WatcherResult:
268
+ messages = _normalize_messages(ctx.messages)
177
269
  config = self.config
178
270
  patterns = config.get("patterns", [])
179
271
 
@@ -189,7 +281,7 @@ class PatternWatcher(Watcher):
189
281
  continue
190
282
 
191
283
  # Check recent messages
192
- for msg in ctx.messages[-10:]:
284
+ for msg in messages[-10:]:
193
285
  content = msg.get("content", "") or ""
194
286
  if compiled.search(content):
195
287
  action = pattern_config.get("action", "nudge")
@@ -236,11 +328,12 @@ class DelegationWatcher(Watcher):
236
328
  ]
237
329
 
238
330
  async def observe(self, ctx: WatcherContext) -> WatcherResult:
331
+ messages = _normalize_messages(ctx.messages)
239
332
  config = self.config
240
333
  strict = config.get("strict", True) # If True, nudge. If False, just warn.
241
334
 
242
335
  # Check recent messages for bash tool calls
243
- for msg in ctx.messages[-10:]:
336
+ for msg in messages[-10:]:
244
337
  if msg.get("role") != "assistant":
245
338
  continue
246
339
 
@@ -370,6 +463,7 @@ class DelegationReminderWatcher(Watcher):
370
463
  }
371
464
 
372
465
  async def observe(self, ctx: WatcherContext) -> WatcherResult:
466
+ messages = _normalize_messages(ctx.messages)
373
467
  config = self.config
374
468
  threshold = config.get("threshold", 10) # Max consecutive non-delegation calls
375
469
  lookback = config.get("lookback", 30) # How many messages to check
@@ -378,7 +472,7 @@ class DelegationReminderWatcher(Watcher):
378
472
  consecutive_non_delegation = 0
379
473
 
380
474
  # Look through recent messages in reverse order
381
- for msg in reversed(ctx.messages[-lookback:]):
475
+ for msg in reversed(messages[-lookback:]):
382
476
  if msg.get("role") != "assistant":
383
477
  continue
384
478
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: zwarm
3
- Version: 1.3.9
3
+ Version: 1.3.11
4
4
  Summary: Multi-Agent CLI Orchestration Research Platform
5
5
  Requires-Python: <3.14,>=3.13
6
6
  Requires-Dist: python-dotenv>=1.0.0
@@ -4,12 +4,12 @@ zwarm/test_orchestrator_watchers.py,sha256=QpoaehPU7ekT4XshbTOWnJ2H0wRveV3QOZjxb
4
4
  zwarm/adapters/__init__.py,sha256=O0b-SfZpb6txeNqFkXZ2aaf34yLFYreznyrAV25jF_Q,656
5
5
  zwarm/adapters/base.py,sha256=fZlQviTgVvOcwnxduTla6WuM6FzQJ_yoHMW5SxwVgQg,2527
6
6
  zwarm/adapters/claude_code.py,sha256=vAjsjD-_JjARmC4_FBSILQZmQCBrk_oNHo18a9ubuqk,11481
7
- zwarm/adapters/codex_mcp.py,sha256=qX6blFC_rZwl3JaS9TLIG1yxIvWhgX42Goq2DdcJjbU,38474
7
+ zwarm/adapters/codex_mcp.py,sha256=MSv01sz_ECdyafZRCMkw-2U62yCGJmxq3YaWog5S4lU,40939
8
8
  zwarm/adapters/registry.py,sha256=EdyHECaNA5Kv1od64pYFBJyA_r_6I1r_eJTNP1XYLr4,1781
9
9
  zwarm/adapters/test_codex_mcp.py,sha256=0qhVzxn_KF-XUS30gXSJKwMdR3kWGsDY9iPk1Ihqn3w,10698
10
10
  zwarm/adapters/test_registry.py,sha256=otxcVDONwFCMisyANToF3iy7Y8dSbCL8bTmZNhxNuF4,2383
11
11
  zwarm/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- zwarm/cli/main.py,sha256=kGrwxAHkpJqF6YCki1DLJpOzjkk05P-1AXGzPBuUOnw,73266
12
+ zwarm/cli/main.py,sha256=NKTq-PxIu6cy9MaPzNPr3T7T62BL7cZdGWU7lT9FNvg,73512
13
13
  zwarm/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  zwarm/core/compact.py,sha256=Y8C7Gs-5-WOU43WRvQ863Qzd5xtuEqR6Aw3r2p8_-i8,10907
15
15
  zwarm/core/config.py,sha256=7mzxrWvHmTjwiUWAoE4NYS_1yWj85-vWkpT6X6kiMIg,11579
@@ -27,11 +27,11 @@ zwarm/tools/__init__.py,sha256=FpqxwXJA6-fQ7C-oLj30jjK_0qqcE7MbI0dQuaB56kU,290
27
27
  zwarm/tools/delegation.py,sha256=dV-fDoxA01RqjW90SLCLhBFQ2kVn8-68JdL0ssYYg_k,19583
28
28
  zwarm/watchers/__init__.py,sha256=yYGTbhuImQLESUdtfrYbHYBJNvCNX3B-Ei-vY5BizX8,760
29
29
  zwarm/watchers/base.py,sha256=r1GoPlj06nOT2xp4fghfSjxbRyFFFQUB6HpZbEyO2OY,3834
30
- zwarm/watchers/builtin.py,sha256=k1pCnQBEmLHeuCo8t6UXoenJUpfWY7AuGt_aEk8syew,15828
30
+ zwarm/watchers/builtin.py,sha256=IL5QwwKOIqWEfJ_uQWb321Px4i5OLtI_vnWQMudqKoA,19064
31
31
  zwarm/watchers/manager.py,sha256=XZjBVeHjgCUlkTUeHqdvBvHoBC862U1ik0fG6nlRGog,5587
32
32
  zwarm/watchers/registry.py,sha256=A9iBIVIFNtO7KPX0kLpUaP8dAK7ozqWLA44ocJGnOw4,1219
33
33
  zwarm/watchers/test_watchers.py,sha256=zOsxumBqKfR5ZVGxrNlxz6KcWjkcdp0QhW9WB0_20zM,7855
34
- zwarm-1.3.9.dist-info/METADATA,sha256=ZU-UJO9Mj_oUeG__0m9uYChVRYfQgtjSOXZapT5pk4Y,16114
35
- zwarm-1.3.9.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
36
- zwarm-1.3.9.dist-info/entry_points.txt,sha256=u0OXq4q8d3yJ3EkUXwZfkS-Y8Lcy0F8cWrcQfoRxM6Q,46
37
- zwarm-1.3.9.dist-info/RECORD,,
34
+ zwarm-1.3.11.dist-info/METADATA,sha256=y7GW-bz4u9LPfPXqsHf1BnifTQGEN-ceEioy-UdK0MQ,16115
35
+ zwarm-1.3.11.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
36
+ zwarm-1.3.11.dist-info/entry_points.txt,sha256=u0OXq4q8d3yJ3EkUXwZfkS-Y8Lcy0F8cWrcQfoRxM6Q,46
37
+ zwarm-1.3.11.dist-info/RECORD,,
File without changes