zwarm 1.0.0__py3-none-any.whl → 1.1.1__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/core/compact.py +31 -14
- zwarm/core/test_compact.py +46 -0
- {zwarm-1.0.0.dist-info → zwarm-1.1.1.dist-info}/METADATA +1 -1
- {zwarm-1.0.0.dist-info → zwarm-1.1.1.dist-info}/RECORD +6 -6
- {zwarm-1.0.0.dist-info → zwarm-1.1.1.dist-info}/WHEEL +0 -0
- {zwarm-1.0.0.dist-info → zwarm-1.1.1.dist-info}/entry_points.txt +0 -0
zwarm/core/compact.py
CHANGED
|
@@ -16,6 +16,13 @@ from typing import Any
|
|
|
16
16
|
logger = logging.getLogger(__name__)
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def _get_attr(obj: Any, key: str, default: Any = None) -> Any:
|
|
20
|
+
"""Get attribute from dict or object (handles both Pydantic models and dicts)."""
|
|
21
|
+
if isinstance(obj, dict):
|
|
22
|
+
return obj.get(key, default)
|
|
23
|
+
return getattr(obj, key, default)
|
|
24
|
+
|
|
25
|
+
|
|
19
26
|
@dataclass
|
|
20
27
|
class CompactionResult:
|
|
21
28
|
"""Result of a compaction operation."""
|
|
@@ -30,16 +37,17 @@ class CompactionResult:
|
|
|
30
37
|
return self.removed_count > 0
|
|
31
38
|
|
|
32
39
|
|
|
33
|
-
def estimate_tokens(messages: list[
|
|
40
|
+
def estimate_tokens(messages: list[Any]) -> int:
|
|
34
41
|
"""
|
|
35
42
|
Rough token estimate for messages.
|
|
36
43
|
|
|
37
44
|
Uses ~4 chars per token as a simple heuristic.
|
|
38
45
|
This is intentionally conservative.
|
|
46
|
+
Handles both dict messages and Pydantic model messages.
|
|
39
47
|
"""
|
|
40
48
|
total_chars = 0
|
|
41
49
|
for msg in messages:
|
|
42
|
-
content = msg
|
|
50
|
+
content = _get_attr(msg, "content", "")
|
|
43
51
|
if isinstance(content, str):
|
|
44
52
|
total_chars += len(content)
|
|
45
53
|
elif isinstance(content, list):
|
|
@@ -50,16 +58,22 @@ def estimate_tokens(messages: list[dict[str, Any]]) -> int:
|
|
|
50
58
|
total_chars += len(str(block.get("input", "")))
|
|
51
59
|
elif isinstance(block, str):
|
|
52
60
|
total_chars += len(block)
|
|
61
|
+
else:
|
|
62
|
+
# Pydantic model block
|
|
63
|
+
total_chars += len(str(_get_attr(block, "text", "")))
|
|
64
|
+
total_chars += len(str(_get_attr(block, "input", "")))
|
|
53
65
|
|
|
54
66
|
# Tool calls add tokens too
|
|
55
|
-
tool_calls = msg
|
|
67
|
+
tool_calls = _get_attr(msg, "tool_calls", []) or []
|
|
56
68
|
for tc in tool_calls:
|
|
57
|
-
|
|
69
|
+
func = _get_attr(tc, "function", {}) or {}
|
|
70
|
+
args = _get_attr(func, "arguments", "") if isinstance(func, dict) else getattr(func, "arguments", "")
|
|
71
|
+
total_chars += len(str(args))
|
|
58
72
|
|
|
59
73
|
return total_chars // 4
|
|
60
74
|
|
|
61
75
|
|
|
62
|
-
def find_tool_groups(messages: list[
|
|
76
|
+
def find_tool_groups(messages: list[Any]) -> list[tuple[int, int]]:
|
|
63
77
|
"""
|
|
64
78
|
Find message index ranges that form tool call groups.
|
|
65
79
|
|
|
@@ -69,6 +83,7 @@ def find_tool_groups(messages: list[dict[str, Any]]) -> list[tuple[int, int]]:
|
|
|
69
83
|
|
|
70
84
|
This handles both OpenAI format (role="tool") and Anthropic format
|
|
71
85
|
(role="user" with tool_result content).
|
|
86
|
+
Also handles Pydantic model messages.
|
|
72
87
|
|
|
73
88
|
Returns list of (start_idx, end_idx) tuples (inclusive).
|
|
74
89
|
"""
|
|
@@ -82,15 +97,16 @@ def find_tool_groups(messages: list[dict[str, Any]]) -> list[tuple[int, int]]:
|
|
|
82
97
|
has_tool_calls = False
|
|
83
98
|
|
|
84
99
|
# OpenAI format: tool_calls field
|
|
85
|
-
if msg
|
|
100
|
+
if _get_attr(msg, "role") == "assistant" and _get_attr(msg, "tool_calls"):
|
|
86
101
|
has_tool_calls = True
|
|
87
102
|
|
|
88
103
|
# Anthropic format: content blocks with type="tool_use"
|
|
89
|
-
if msg
|
|
90
|
-
content = msg
|
|
104
|
+
if _get_attr(msg, "role") == "assistant":
|
|
105
|
+
content = _get_attr(msg, "content", [])
|
|
91
106
|
if isinstance(content, list):
|
|
92
107
|
for block in content:
|
|
93
|
-
|
|
108
|
+
block_type = _get_attr(block, "type", None)
|
|
109
|
+
if block_type == "tool_use":
|
|
94
110
|
has_tool_calls = True
|
|
95
111
|
break
|
|
96
112
|
|
|
@@ -101,7 +117,7 @@ def find_tool_groups(messages: list[dict[str, Any]]) -> list[tuple[int, int]]:
|
|
|
101
117
|
# Find all following tool responses
|
|
102
118
|
while j < len(messages):
|
|
103
119
|
next_msg = messages[j]
|
|
104
|
-
role = next_msg
|
|
120
|
+
role = _get_attr(next_msg, "role", "")
|
|
105
121
|
|
|
106
122
|
# OpenAI format: tool role
|
|
107
123
|
if role == "tool":
|
|
@@ -110,10 +126,10 @@ def find_tool_groups(messages: list[dict[str, Any]]) -> list[tuple[int, int]]:
|
|
|
110
126
|
|
|
111
127
|
# Anthropic format: user message with tool_result
|
|
112
128
|
if role == "user":
|
|
113
|
-
content = next_msg
|
|
129
|
+
content = _get_attr(next_msg, "content", [])
|
|
114
130
|
if isinstance(content, list):
|
|
115
131
|
has_tool_result = any(
|
|
116
|
-
|
|
132
|
+
_get_attr(b, "type", None) == "tool_result"
|
|
117
133
|
for b in content
|
|
118
134
|
)
|
|
119
135
|
if has_tool_result:
|
|
@@ -132,7 +148,7 @@ def find_tool_groups(messages: list[dict[str, Any]]) -> list[tuple[int, int]]:
|
|
|
132
148
|
|
|
133
149
|
|
|
134
150
|
def compact_messages(
|
|
135
|
-
messages: list[
|
|
151
|
+
messages: list[Any],
|
|
136
152
|
keep_first_n: int = 2,
|
|
137
153
|
keep_last_n: int = 10,
|
|
138
154
|
max_tokens: int | None = None,
|
|
@@ -298,7 +314,7 @@ def compact_messages(
|
|
|
298
314
|
|
|
299
315
|
|
|
300
316
|
def should_compact(
|
|
301
|
-
messages: list[
|
|
317
|
+
messages: list[Any],
|
|
302
318
|
max_tokens: int,
|
|
303
319
|
threshold_pct: float = 0.85,
|
|
304
320
|
) -> bool:
|
|
@@ -306,6 +322,7 @@ def should_compact(
|
|
|
306
322
|
Check if messages should be compacted.
|
|
307
323
|
|
|
308
324
|
Returns True if estimated tokens exceed threshold percentage of max.
|
|
325
|
+
Handles both dict messages and Pydantic model messages.
|
|
309
326
|
"""
|
|
310
327
|
current = estimate_tokens(messages)
|
|
311
328
|
threshold = int(max_tokens * threshold_pct)
|
zwarm/core/test_compact.py
CHANGED
|
@@ -264,3 +264,49 @@ class TestEdgeCases:
|
|
|
264
264
|
result = compact_messages(messages, keep_first_n=2, keep_last_n=2)
|
|
265
265
|
assert not result.was_compacted
|
|
266
266
|
assert result.messages == messages
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class TestPydanticModelMessages:
|
|
270
|
+
"""Test handling of Pydantic model messages (not just dicts)."""
|
|
271
|
+
|
|
272
|
+
def test_estimate_tokens_with_objects(self):
|
|
273
|
+
"""estimate_tokens should handle objects with attributes."""
|
|
274
|
+
class MockMessage:
|
|
275
|
+
def __init__(self, role, content):
|
|
276
|
+
self.role = role
|
|
277
|
+
self.content = content
|
|
278
|
+
|
|
279
|
+
messages = [
|
|
280
|
+
MockMessage("user", "Hello world"),
|
|
281
|
+
MockMessage("assistant", "Hi there!"),
|
|
282
|
+
]
|
|
283
|
+
tokens = estimate_tokens(messages)
|
|
284
|
+
assert tokens > 0
|
|
285
|
+
|
|
286
|
+
def test_should_compact_with_objects(self):
|
|
287
|
+
"""should_compact should handle objects with attributes."""
|
|
288
|
+
class MockMessage:
|
|
289
|
+
def __init__(self, role, content):
|
|
290
|
+
self.role = role
|
|
291
|
+
self.content = content
|
|
292
|
+
|
|
293
|
+
messages = [MockMessage("user", "x" * 4000)]
|
|
294
|
+
# Should not crash
|
|
295
|
+
result = should_compact(messages, max_tokens=500, threshold_pct=0.85)
|
|
296
|
+
assert result is True
|
|
297
|
+
|
|
298
|
+
def test_find_tool_groups_with_objects(self):
|
|
299
|
+
"""find_tool_groups should handle objects with attributes."""
|
|
300
|
+
class MockMessage:
|
|
301
|
+
def __init__(self, role, content=None, tool_calls=None):
|
|
302
|
+
self.role = role
|
|
303
|
+
self.content = content
|
|
304
|
+
self.tool_calls = tool_calls
|
|
305
|
+
|
|
306
|
+
messages = [
|
|
307
|
+
MockMessage("user", "Task"),
|
|
308
|
+
MockMessage("assistant", "Done"),
|
|
309
|
+
]
|
|
310
|
+
# Should not crash
|
|
311
|
+
groups = find_tool_groups(messages)
|
|
312
|
+
assert groups == []
|
|
@@ -9,12 +9,12 @@ zwarm/adapters/test_codex_mcp.py,sha256=vodQF5VUrM_F1GygUADtXYrA0kvAc7dWpT_Ff-Uo
|
|
|
9
9
|
zwarm/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
10
|
zwarm/cli/main.py,sha256=dU_8XLIYzcxikl_MgT6BmChNbqYq94dJ4Ra5STzm_KI,33109
|
|
11
11
|
zwarm/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
12
|
-
zwarm/core/compact.py,sha256=
|
|
12
|
+
zwarm/core/compact.py,sha256=Y8C7Gs-5-WOU43WRvQ863Qzd5xtuEqR6Aw3r2p8_-i8,10907
|
|
13
13
|
zwarm/core/config.py,sha256=H8XFsWeEehZnUsMf7nsM_YqjljFYaC43DLs8Xw4sOuQ,10360
|
|
14
14
|
zwarm/core/environment.py,sha256=HVDpDZEpDSfyh9-wHZMzMKVUPKvioBkPVWeiME2JmFo,5435
|
|
15
15
|
zwarm/core/models.py,sha256=PrC3okRBVJxISUa1Fax4KkagqLT6Xub-kTxC9drN0sY,10083
|
|
16
16
|
zwarm/core/state.py,sha256=wMryIvXP-VDLh2b76A5taeL_9dm5k4jk4HnvHWgLqGE,7658
|
|
17
|
-
zwarm/core/test_compact.py,sha256=
|
|
17
|
+
zwarm/core/test_compact.py,sha256=WSdjCB5t4YMcknsrkmJIUsVOPY28s4y9GnDmu3Z4BFw,11878
|
|
18
18
|
zwarm/core/test_config.py,sha256=26ozyiFOdjFF2c9Q-HDfFM6GOLfgw_5FZ55nTDMNYA8,4888
|
|
19
19
|
zwarm/core/test_models.py,sha256=sWTIhMZvuLP5AooGR6y8OR2EyWydqVfhmGrE7NPBBnk,8450
|
|
20
20
|
zwarm/prompts/__init__.py,sha256=FiaIOniLrIyfD3_osxT6I7FfyKjtctbf8jNs5QTPs_s,213
|
|
@@ -27,7 +27,7 @@ zwarm/watchers/builtin.py,sha256=52hyRREYYDsSuG-YKElXViSTyMmGySZaFreHc0pz-A4,124
|
|
|
27
27
|
zwarm/watchers/manager.py,sha256=XZjBVeHjgCUlkTUeHqdvBvHoBC862U1ik0fG6nlRGog,5587
|
|
28
28
|
zwarm/watchers/registry.py,sha256=A9iBIVIFNtO7KPX0kLpUaP8dAK7ozqWLA44ocJGnOw4,1219
|
|
29
29
|
zwarm/watchers/test_watchers.py,sha256=zOsxumBqKfR5ZVGxrNlxz6KcWjkcdp0QhW9WB0_20zM,7855
|
|
30
|
-
zwarm-1.
|
|
31
|
-
zwarm-1.
|
|
32
|
-
zwarm-1.
|
|
33
|
-
zwarm-1.
|
|
30
|
+
zwarm-1.1.1.dist-info/METADATA,sha256=9GoAYbxRjMSdbHIwrZPS_zdrrDSz7-Avnwy_8AqNEBM,14995
|
|
31
|
+
zwarm-1.1.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
32
|
+
zwarm-1.1.1.dist-info/entry_points.txt,sha256=u0OXq4q8d3yJ3EkUXwZfkS-Y8Lcy0F8cWrcQfoRxM6Q,46
|
|
33
|
+
zwarm-1.1.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|