syncmill 0.2.0__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.
- syncmill/__init__.py +11 -0
- syncmill/agents.py +2064 -0
- syncmill/artifact_io.py +201 -0
- syncmill/artifacts.py +1946 -0
- syncmill/board.py +2060 -0
- syncmill/breaker.py +327 -0
- syncmill/cli.py +4986 -0
- syncmill/command_errors.py +32 -0
- syncmill/config.py +1118 -0
- syncmill/dashboard.py +935 -0
- syncmill/gates.py +179 -0
- syncmill/gateway.py +234 -0
- syncmill/git_env.py +99 -0
- syncmill/isolated_session.py +431 -0
- syncmill/matcher.py +127 -0
- syncmill/memory.py +1067 -0
- syncmill/memory_backend.py +1907 -0
- syncmill/memory_migration.py +962 -0
- syncmill/model_routing.py +482 -0
- syncmill/models.py +244 -0
- syncmill/monitor.py +775 -0
- syncmill/phoenix_exporter.py +339 -0
- syncmill/planning.py +512 -0
- syncmill/platform_support.py +14 -0
- syncmill/preflight.py +1129 -0
- syncmill/project.py +1067 -0
- syncmill/report.py +502 -0
- syncmill/review_candidates.py +107 -0
- syncmill/room_compare.py +2380 -0
- syncmill/room_raw.py +484 -0
- syncmill/room_viewer.py +604 -0
- syncmill/rooms.py +4044 -0
- syncmill/session.py +444 -0
- syncmill/strategies/__init__.py +82 -0
- syncmill/strategies/base.py +910 -0
- syncmill/strategies/compete.py +233 -0
- syncmill/strategies/council.py +429 -0
- syncmill/strategies/decompose.py +570 -0
- syncmill/strategies/pipeline.py +221 -0
- syncmill/strategies/route.py +130 -0
- syncmill/supervisor.py +1423 -0
- syncmill/teamfile.py +571 -0
- syncmill/telemetry_hook.py +231 -0
- syncmill/terminal.py +245 -0
- syncmill/tracer.py +548 -0
- syncmill/tui.py +3537 -0
- syncmill/tui_collaboration.py +499 -0
- syncmill/tui_jobs.py +1407 -0
- syncmill/tui_render.py +156 -0
- syncmill/workflow.py +3746 -0
- syncmill/worktree.py +1045 -0
- syncmill-0.2.0.dist-info/METADATA +853 -0
- syncmill-0.2.0.dist-info/RECORD +56 -0
- syncmill-0.2.0.dist-info/WHEEL +4 -0
- syncmill-0.2.0.dist-info/entry_points.txt +2 -0
- syncmill-0.2.0.dist-info/licenses/LICENSE +191 -0
syncmill/models.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Data models for tasks and results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from enum import StrEnum
|
|
7
|
+
from uuid import UUID, uuid4
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskStatus(StrEnum):
|
|
11
|
+
PENDING = "pending"
|
|
12
|
+
RUNNING = "running"
|
|
13
|
+
COMPLETED = "completed"
|
|
14
|
+
FAILED = "failed"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ToolOutcome(StrEnum):
|
|
18
|
+
"""Body-free terminal outcome for one observed MCP tool call."""
|
|
19
|
+
|
|
20
|
+
SUCCEEDED = "succeeded"
|
|
21
|
+
FAILED = "failed"
|
|
22
|
+
UNKNOWN = "unknown"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class ToolCallRecord:
|
|
27
|
+
"""The minimum safe projection of an observed MCP call."""
|
|
28
|
+
|
|
29
|
+
sequence: int
|
|
30
|
+
tool_key: str
|
|
31
|
+
outcome: ToolOutcome
|
|
32
|
+
# Explicit producer-authored retry causality. Built-in parsers leave this unset.
|
|
33
|
+
retry_of_sequence: int | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class ToolTelemetry:
|
|
38
|
+
"""Provider capture summary attached in-memory to a ``TaskResult`` only."""
|
|
39
|
+
|
|
40
|
+
source: str
|
|
41
|
+
complete: bool
|
|
42
|
+
calls: tuple[ToolCallRecord, ...] = ()
|
|
43
|
+
malformed_count: int = 0
|
|
44
|
+
unqualified_count: int = 0
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(frozen=True)
|
|
48
|
+
class TokenUsage:
|
|
49
|
+
"""Best-effort provider token/cost telemetry parsed from a CLI's output.
|
|
50
|
+
|
|
51
|
+
Normalized across providers so display is uniform: ``total_input_tokens`` is
|
|
52
|
+
the FULL prompt size including cached tokens (Claude reports cache tokens
|
|
53
|
+
beside ``input_tokens``, so they are summed in; codex's ``input_tokens``
|
|
54
|
+
already includes ``cached_input_tokens``). Every field is optional — a
|
|
55
|
+
provider that emits no usage (kimi) or malformed usage yields ``None`` at
|
|
56
|
+
the ``TaskResult.usage`` level, never a partial guess.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
total_input_tokens: int | None = None
|
|
60
|
+
output_tokens: int | None = None
|
|
61
|
+
cache_read_tokens: int | None = None
|
|
62
|
+
cache_creation_tokens: int | None = None
|
|
63
|
+
total_cost_usd: float | None = None
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_mapping(cls, raw: object) -> "TokenUsage | None":
|
|
67
|
+
"""Validated construction from an untrusted usage dict (a manifest's
|
|
68
|
+
``usage`` block, provider telemetry). Each field is dropped
|
|
69
|
+
individually when malformed — negative, boolean, non-int (non-finite
|
|
70
|
+
or negative for cost), or ``cache_read_tokens`` exceeding
|
|
71
|
+
``total_input_tokens`` — so bad telemetry renders as omitted, never
|
|
72
|
+
as a misleading number. ``None`` when ``raw`` is not a dict."""
|
|
73
|
+
import math
|
|
74
|
+
|
|
75
|
+
if not isinstance(raw, dict):
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
def _count(value: object) -> int | None:
|
|
79
|
+
if isinstance(value, bool) or not isinstance(value, int) or value < 0:
|
|
80
|
+
return None
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
counts = {
|
|
84
|
+
key: _count(raw.get(key))
|
|
85
|
+
for key in (
|
|
86
|
+
"total_input_tokens",
|
|
87
|
+
"output_tokens",
|
|
88
|
+
"cache_read_tokens",
|
|
89
|
+
"cache_creation_tokens",
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
if (
|
|
93
|
+
counts["cache_read_tokens"] is not None
|
|
94
|
+
and counts["total_input_tokens"] is not None
|
|
95
|
+
and counts["cache_read_tokens"] > counts["total_input_tokens"]
|
|
96
|
+
):
|
|
97
|
+
counts["cache_read_tokens"] = None
|
|
98
|
+
cost = raw.get("total_cost_usd")
|
|
99
|
+
try:
|
|
100
|
+
if (
|
|
101
|
+
isinstance(cost, bool)
|
|
102
|
+
or not isinstance(cost, (int, float))
|
|
103
|
+
or not math.isfinite(cost) # OverflowError on an absurd int
|
|
104
|
+
or cost < 0
|
|
105
|
+
):
|
|
106
|
+
cost = None
|
|
107
|
+
else:
|
|
108
|
+
cost = float(cost)
|
|
109
|
+
except OverflowError:
|
|
110
|
+
cost = None
|
|
111
|
+
return cls(**counts, total_cost_usd=cost)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def cache_hit_ratio(self) -> float | None:
|
|
115
|
+
"""Fraction of the input the provider served from its prompt cache
|
|
116
|
+
(None when either side of the ratio is unknown or the input is empty)."""
|
|
117
|
+
if self.cache_read_tokens is None or not self.total_input_tokens:
|
|
118
|
+
return None
|
|
119
|
+
return self.cache_read_tokens / self.total_input_tokens
|
|
120
|
+
|
|
121
|
+
def format_compact(self) -> str | None:
|
|
122
|
+
"""Short human string for tables — e.g. ``cache 85% (34.2k/40.1k)`` —
|
|
123
|
+
or ``None`` when there is nothing meaningful to show."""
|
|
124
|
+
|
|
125
|
+
def _k(n: int) -> str:
|
|
126
|
+
return f"{n / 1000:.1f}k" if n >= 1000 else str(n)
|
|
127
|
+
|
|
128
|
+
ratio = self.cache_hit_ratio
|
|
129
|
+
if ratio is not None:
|
|
130
|
+
assert self.cache_read_tokens is not None # implied by ratio
|
|
131
|
+
assert self.total_input_tokens is not None
|
|
132
|
+
return (
|
|
133
|
+
f"cache {ratio:.0%} "
|
|
134
|
+
f"({_k(self.cache_read_tokens)}/{_k(self.total_input_tokens)})"
|
|
135
|
+
)
|
|
136
|
+
if self.total_input_tokens:
|
|
137
|
+
return f"in {_k(self.total_input_tokens)}"
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class Task:
|
|
143
|
+
"""A unit of work to be executed by an agent."""
|
|
144
|
+
|
|
145
|
+
prompt: str
|
|
146
|
+
id: UUID = field(default_factory=uuid4)
|
|
147
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
148
|
+
tags: list[str] = field(default_factory=list)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class GateCheck:
|
|
153
|
+
"""Result of one quality-gate command run against a worktree."""
|
|
154
|
+
|
|
155
|
+
command: str
|
|
156
|
+
passed: bool
|
|
157
|
+
exit_code: int
|
|
158
|
+
output: str = ""
|
|
159
|
+
duration_seconds: float = 0.0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class GateReport:
|
|
164
|
+
"""Outcome of running the quality gate against an agent's worktree.
|
|
165
|
+
|
|
166
|
+
``passed`` is the accept/reject signal (every check passed); ``score`` is the
|
|
167
|
+
fraction that passed, the ranking signal CompeteStrategy's ``_select_winner``
|
|
168
|
+
can order candidates by (currently pass/priority; ``score`` is the seam for a
|
|
169
|
+
finer-grained future ranking).
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
passed: bool
|
|
173
|
+
score: float
|
|
174
|
+
checks: list[GateCheck] = field(default_factory=list)
|
|
175
|
+
|
|
176
|
+
@classmethod
|
|
177
|
+
def from_checks(cls, checks: list[GateCheck]) -> GateReport:
|
|
178
|
+
"""Aggregate per-command checks. No checks → trivially passed (score 1.0),
|
|
179
|
+
so an empty gate never blocks (defensive; ``build_gate`` returns ``None``
|
|
180
|
+
for the disabled case instead of an empty gate)."""
|
|
181
|
+
if not checks:
|
|
182
|
+
return cls(passed=True, score=1.0, checks=[])
|
|
183
|
+
n_pass = sum(1 for c in checks if c.passed)
|
|
184
|
+
return cls(
|
|
185
|
+
passed=n_pass == len(checks),
|
|
186
|
+
score=round(n_pass / len(checks), 2),
|
|
187
|
+
checks=list(checks),
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
def summary(self) -> str:
|
|
191
|
+
"""One-line human summary, naming the failing commands — used in gate
|
|
192
|
+
failure errors and CLI output."""
|
|
193
|
+
n_pass = sum(1 for c in self.checks if c.passed)
|
|
194
|
+
head = f"{n_pass}/{len(self.checks)} checks passed"
|
|
195
|
+
failed = [c.command for c in self.checks if not c.passed]
|
|
196
|
+
if failed:
|
|
197
|
+
head += "; failed: " + ", ".join(failed)
|
|
198
|
+
return head
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
@dataclass
|
|
202
|
+
class TaskResult:
|
|
203
|
+
"""Outcome of an agent executing a task.
|
|
204
|
+
|
|
205
|
+
``agent_id`` is normally a real agent id, but Supervisor-level failures use
|
|
206
|
+
sentinel values that are NOT agents: ``"none"`` (no agents available),
|
|
207
|
+
``"artifact"`` (mandatory run-artifact persistence failed), ``"routing"``
|
|
208
|
+
(model-routing validation failed — all-unmapped explicit tier or corrupt
|
|
209
|
+
inherited metadata), and ``"preflight"`` (strict preflight blocked the
|
|
210
|
+
run). Consumers keying on ``agent_id`` must not treat these as agents.
|
|
211
|
+
"""
|
|
212
|
+
|
|
213
|
+
task_id: UUID
|
|
214
|
+
agent_id: str
|
|
215
|
+
status: TaskStatus
|
|
216
|
+
output: str
|
|
217
|
+
exit_code: int
|
|
218
|
+
duration_seconds: float
|
|
219
|
+
files_changed: list[str] = field(default_factory=list)
|
|
220
|
+
error: str | None = None
|
|
221
|
+
# Quality-gate verdict, attached by the strategy when a gate ran against this
|
|
222
|
+
# result's worktree (None when no gate is configured). Like files_changed,
|
|
223
|
+
# it's an annotation set post-hoc by the strategy, not produced by the agent.
|
|
224
|
+
gate: GateReport | None = None
|
|
225
|
+
# Full applyable diff of the winning worktree vs HEAD (untracked files
|
|
226
|
+
# included), collected alongside files_changed BEFORE the Supervisor tears the
|
|
227
|
+
# worktree down — the recovery path `run --emit-patch` exports. Like
|
|
228
|
+
# files_changed/gate, an annotation set post-hoc by the strategy; None for a
|
|
229
|
+
# failed result, a non-git workspace, or a diff-collection failure ("" means
|
|
230
|
+
# a clean worktree: the agent changed nothing).
|
|
231
|
+
patch: str | None = None
|
|
232
|
+
# Deliberately omitted from result.json/the envelope; strategies project it
|
|
233
|
+
# into body-free OTLP TOOL spans instead. Kept last to preserve TaskResult's
|
|
234
|
+
# established positional constructor contract.
|
|
235
|
+
tool_telemetry: ToolTelemetry | None = None
|
|
236
|
+
# Durable orchestration metadata. Kept optional and last for compatibility
|
|
237
|
+
# with integrations constructing TaskResult positionally.
|
|
238
|
+
run_id: str | None = None
|
|
239
|
+
artifact_path: str | None = None
|
|
240
|
+
conversation_id: str | None = None
|
|
241
|
+
provider_session_id: str | None = None
|
|
242
|
+
# Best-effort provider token/cache telemetry (None when the CLI emitted none
|
|
243
|
+
# or it was malformed). Appended last to preserve the positional contract.
|
|
244
|
+
usage: TokenUsage | None = None
|