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.
- zwarm/adapters/codex_mcp.py +48 -2
- zwarm/cli/main.py +10 -1
- zwarm/watchers/builtin.py +100 -6
- {zwarm-1.3.9.dist-info → zwarm-1.3.11.dist-info}/METADATA +1 -1
- {zwarm-1.3.9.dist-info → zwarm-1.3.11.dist-info}/RECORD +7 -7
- {zwarm-1.3.9.dist-info → zwarm-1.3.11.dist-info}/WHEEL +0 -0
- {zwarm-1.3.9.dist-info → zwarm-1.3.11.dist-info}/entry_points.txt +0 -0
zwarm/adapters/codex_mcp.py
CHANGED
|
@@ -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
|
-
#
|
|
448
|
-
|
|
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]
|
|
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(
|
|
124
|
+
if len(messages) >= max_same_calls * 2:
|
|
35
125
|
recent_assistant = [
|
|
36
|
-
m for m in
|
|
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
|
|
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
|
|
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
|
|
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(
|
|
475
|
+
for msg in reversed(messages[-lookback:]):
|
|
382
476
|
if msg.get("role") != "assistant":
|
|
383
477
|
continue
|
|
384
478
|
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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.
|
|
35
|
-
zwarm-1.3.
|
|
36
|
-
zwarm-1.3.
|
|
37
|
-
zwarm-1.3.
|
|
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
|
|
File without changes
|