@team-agent/installer 0.2.2 → 0.2.3

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.
@@ -0,0 +1,145 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import time
5
+ from dataclasses import dataclass, field
6
+ from pathlib import Path
7
+ from typing import Any, Callable
8
+
9
+ from team_agent.message_store import MessageStore
10
+ from team_agent.paths import logs_dir, runtime_dir
11
+ from team_agent.status.queries import result_summary_from_row
12
+
13
+
14
+ @dataclass
15
+ class WatchCursor:
16
+ event_offset: int = 0
17
+ seen_result_ids: set[str] = field(default_factory=set)
18
+ initialized: bool = False
19
+ archive_signature: tuple[int, int] | None = None
20
+
21
+
22
+ ROTATION_MARKER = "[watch] log rotated; archived segment events.jsonl.1 not replayed — historical replay deferred to a future --replay flag"
23
+
24
+
25
+ def run_watch(
26
+ workspace: Path,
27
+ *,
28
+ team: str | None = None,
29
+ interval: float = 0.5,
30
+ output: Callable[[str], None] = print,
31
+ sleep: Callable[[float], None] = time.sleep,
32
+ ) -> None:
33
+ cursor = WatchCursor()
34
+ while True:
35
+ for line in collect_watch_lines(workspace, cursor, team=team):
36
+ output(line)
37
+ sleep(interval)
38
+
39
+
40
+ def collect_watch_lines(workspace: Path, cursor: WatchCursor, *, team: str | None = None) -> list[str]:
41
+ lines = _collect_event_lines(workspace, cursor, team=team)
42
+ lines.extend(_collect_result_lines(workspace, cursor, team=team))
43
+ return lines
44
+
45
+
46
+ def render_event_line(event: dict[str, Any]) -> str | None:
47
+ kind = event.get("event")
48
+ if kind == "result_received":
49
+ return _result_line(event.get("agent_id"), event.get("summary"))
50
+ if kind in {"leader_receiver.injected", "leader_receiver.submitted"}:
51
+ return f"leader_receiver.injected: {_message_snippet(event)} -> {_recipient(event)}"
52
+ if kind == "send.failed":
53
+ return f"send.failed: {_recipient(event)} reason={_clean(event.get('reason') or event.get('error') or '-')}"
54
+ if kind == "leader_receiver.rebind_required":
55
+ pane = event.get("old_pane_id") or event.get("pane_id") or event.get("target") or "-"
56
+ reason = event.get("reason") or event.get("rediscovery_status") or "-"
57
+ return f"leader_receiver.rebind_required: pane={pane} reason={_clean(reason)}"
58
+ if kind == "leader.api_error":
59
+ error_class = event.get("error_class") or "Unknown"
60
+ provider = event.get("provider") or "-"
61
+ snippet = _clean(event.get("matched_pattern_snippet") or event.get("snippet") or "-")
62
+ return f"leader.api_error: {error_class} provider={provider} snippet={snippet}"
63
+ return None
64
+
65
+
66
+ def _collect_event_lines(workspace: Path, cursor: WatchCursor, *, team: str | None = None) -> list[str]:
67
+ path = logs_dir(workspace) / "events.jsonl"
68
+ archive_signature = _archive_signature(path.with_name("events.jsonl.1"))
69
+ lines: list[str] = []
70
+ if not cursor.initialized:
71
+ cursor.archive_signature = archive_signature
72
+ cursor.initialized = True
73
+ elif archive_signature and archive_signature != cursor.archive_signature:
74
+ lines.append(ROTATION_MARKER)
75
+ cursor.archive_signature = archive_signature
76
+ cursor.event_offset = 0
77
+ if not path.exists():
78
+ return lines
79
+ size = path.stat().st_size
80
+ if cursor.event_offset > size:
81
+ if ROTATION_MARKER not in lines:
82
+ lines.append(ROTATION_MARKER)
83
+ cursor.event_offset = 0
84
+ with path.open("r", encoding="utf-8") as handle:
85
+ handle.seek(cursor.event_offset)
86
+ for raw in handle:
87
+ try:
88
+ event = json.loads(raw)
89
+ except json.JSONDecodeError:
90
+ continue
91
+ if team and _event_team_id(event) != team:
92
+ continue
93
+ rendered = render_event_line(event)
94
+ if rendered:
95
+ lines.append(rendered)
96
+ cursor.event_offset = handle.tell()
97
+ return lines
98
+
99
+
100
+ def _collect_result_lines(workspace: Path, cursor: WatchCursor, *, team: str | None = None) -> list[str]:
101
+ if not (runtime_dir(workspace) / "team.db").exists():
102
+ return []
103
+ store = MessageStore(workspace)
104
+ lines: list[str] = []
105
+ for row in store.latest_results(limit=20, owner_team_id=team):
106
+ result_id = str(row.get("result_id") or "")
107
+ if not result_id or result_id in cursor.seen_result_ids:
108
+ continue
109
+ cursor.seen_result_ids.add(result_id)
110
+ summary = result_summary_from_row(row) or {}
111
+ lines.append(_result_line(summary.get("agent_id"), summary.get("summary")))
112
+ return lines
113
+
114
+
115
+ def _result_line(agent_id: Any, summary: Any) -> str:
116
+ return f"result_received: {agent_id or '-'} -> {_clean(summary or '-')[:80]}"
117
+
118
+
119
+ def _message_snippet(event: dict[str, Any]) -> str:
120
+ message_id = str(event.get("message_id") or event.get("msg_id") or "-")
121
+ return message_id[:12] if message_id != "-" else "-"
122
+
123
+
124
+ def _recipient(event: dict[str, Any]) -> str:
125
+ return str(event.get("recipient") or event.get("to") or event.get("target") or "-")
126
+
127
+
128
+ def _clean(value: Any) -> str:
129
+ return " ".join(str(value).split())
130
+
131
+
132
+ def _event_team_id(event: dict[str, Any]) -> str | None:
133
+ value = event.get("team_id") or event.get("owner_team_id") or event.get("team")
134
+ return str(value) if value else None
135
+
136
+
137
+ def _archive_signature(path: Path) -> tuple[int, int] | None:
138
+ try:
139
+ stat = path.stat()
140
+ except FileNotFoundError:
141
+ return None
142
+ return (stat.st_size, stat.st_mtime_ns)
143
+
144
+
145
+ __all__ = ["ROTATION_MARKER", "WatchCursor", "collect_watch_lines", "render_event_line", "run_watch"]