@team-agent/installer 0.2.3 → 0.2.4
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.
- package/package.json +1 -1
- package/src/team_agent/abnormal_track.py +253 -0
- package/src/team_agent/compiler.py +1 -1
- package/src/team_agent/coordinator/lifecycle.py +20 -2
- package/src/team_agent/display/__init__.py +31 -0
- package/src/team_agent/display/adaptive.py +425 -0
- package/src/team_agent/display/backend.py +46 -0
- package/src/team_agent/display/close.py +6 -0
- package/src/team_agent/display/rebuild.py +102 -0
- package/src/team_agent/display/tiling.py +156 -0
- package/src/team_agent/display/worker_window.py +4 -0
- package/src/team_agent/display/workspace.py +36 -127
- package/src/team_agent/idle_predicate.py +200 -0
- package/src/team_agent/idle_takeover.py +59 -0
- package/src/team_agent/idle_takeover_wiring.py +111 -0
- package/src/team_agent/launch/core.py +13 -4
- package/src/team_agent/leader/__init__.py +444 -61
- package/src/team_agent/message_store/core.py +30 -4
- package/src/team_agent/message_store/leader_notification_log.py +47 -26
- package/src/team_agent/messaging/delivery.py +45 -2
- package/src/team_agent/messaging/leader_panes.py +115 -21
- package/src/team_agent/messaging/send.py +33 -0
- package/src/team_agent/messaging/tmux_io.py +49 -10
- package/src/team_agent/messaging/trust_auto_answer.py +11 -3
- package/src/team_agent/provider_state/README.md +78 -0
- package/src/team_agent/provider_state/__init__.py +86 -0
- package/src/team_agent/provider_state/claude.py +86 -0
- package/src/team_agent/provider_state/codex.py +84 -0
- package/src/team_agent/provider_state/common.py +207 -0
- package/src/team_agent/provider_state/registry.py +118 -0
- package/src/team_agent/restart/orchestration.py +9 -9
- package/src/team_agent/runtime.py +62 -12
- package/src/team_agent/spec.py +4 -3
- package/src/team_agent/wake.py +58 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Gap 32 runtime wiring: drive the file-fact idle/takeover reminder from the
|
|
2
|
+
coordinator tick. This is the glue that replaces the legacy screen-scrape
|
|
3
|
+
`detect_idle_fallbacks` path — it classifies each node from its provider
|
|
4
|
+
session-log file (never the pane) and runs the provider-neutral predicate.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
_TAIL_BYTES = 131072
|
|
14
|
+
_DEBOUNCE_SECONDS = 60.0
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
IDLE_DEBOUNCE_SECONDS = _DEBOUNCE_SECONDS
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def build_idle_nodes(state: dict[str, Any]) -> list[dict[str, Any]]:
|
|
21
|
+
"""Classify every live node from its provider session-log file fact (never
|
|
22
|
+
the pane screen, never message-row status). The leader is read via its own
|
|
23
|
+
transcript when its path is tracked (C13), else omitted rather than guessed.
|
|
24
|
+
"""
|
|
25
|
+
from team_agent.provider_state import read_turn_state
|
|
26
|
+
|
|
27
|
+
nodes: list[dict[str, Any]] = []
|
|
28
|
+
for agent_id, agent_state in (state.get("agents") or {}).items():
|
|
29
|
+
if str(agent_state.get("status") or "") in {"stopped", "paused"}:
|
|
30
|
+
continue
|
|
31
|
+
provider = str(agent_state.get("provider") or "")
|
|
32
|
+
classification = read_turn_state(provider, _read_session_tail(agent_state.get("rollout_path")))
|
|
33
|
+
nodes.append({
|
|
34
|
+
"node_id": agent_id,
|
|
35
|
+
"role": "worker",
|
|
36
|
+
"state": classification.get("state"),
|
|
37
|
+
"turn_id": classification.get("turn_id"),
|
|
38
|
+
"annotations": classification.get("annotations"),
|
|
39
|
+
})
|
|
40
|
+
leader_node = _leader_node(state)
|
|
41
|
+
if leader_node is not None:
|
|
42
|
+
nodes.append(leader_node)
|
|
43
|
+
return nodes
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def push_idle_reminder(workspace: Path, state: dict[str, Any], event_log: Any, result: dict[str, Any]) -> None:
|
|
47
|
+
"""Deliver the one neutral take-over reminder to the leader when the
|
|
48
|
+
predicate fired. No-op otherwise."""
|
|
49
|
+
if not result.get("should_ping"):
|
|
50
|
+
return
|
|
51
|
+
from team_agent.messaging.internal_delivery import deliver_stored_message
|
|
52
|
+
from team_agent.state import team_state_key
|
|
53
|
+
|
|
54
|
+
leader_id = (state.get("leader") or {}).get("id") or "leader"
|
|
55
|
+
try:
|
|
56
|
+
deliver_stored_message(
|
|
57
|
+
workspace,
|
|
58
|
+
leader_id,
|
|
59
|
+
result["message"],
|
|
60
|
+
sender="coordinator",
|
|
61
|
+
requires_ack=False,
|
|
62
|
+
wait_visible=False,
|
|
63
|
+
team=team_state_key(state),
|
|
64
|
+
)
|
|
65
|
+
except Exception as exc:
|
|
66
|
+
event_log.write("idle_takeover.push_failed", error=str(exc))
|
|
67
|
+
event_log.write(
|
|
68
|
+
"idle_takeover.reminder",
|
|
69
|
+
interrupted=result.get("interrupted_nodes"),
|
|
70
|
+
reason=result.get("reason"),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _leader_node(state: dict[str, Any]) -> dict[str, Any] | None:
|
|
75
|
+
"""Best-effort leader node from its own provider transcript (C13). If the
|
|
76
|
+
leader's session file path is not tracked, the leader is omitted rather than
|
|
77
|
+
guessed — the predicate then evaluates the workers (the leader is the ping
|
|
78
|
+
recipient and acts on the reminder regardless)."""
|
|
79
|
+
from team_agent.provider_state import read_turn_state
|
|
80
|
+
|
|
81
|
+
leader = state.get("leader") if isinstance(state.get("leader"), dict) else {}
|
|
82
|
+
receiver = state.get("leader_receiver") if isinstance(state.get("leader_receiver"), dict) else {}
|
|
83
|
+
path = leader.get("rollout_path") or receiver.get("rollout_path")
|
|
84
|
+
provider = str(leader.get("provider") or receiver.get("provider") or "")
|
|
85
|
+
if not path or not provider:
|
|
86
|
+
return None
|
|
87
|
+
classification = read_turn_state(provider, _read_session_tail(path))
|
|
88
|
+
return {
|
|
89
|
+
"node_id": leader.get("id") or "leader",
|
|
90
|
+
"role": "leader",
|
|
91
|
+
"state": classification.get("state"),
|
|
92
|
+
"turn_id": classification.get("turn_id"),
|
|
93
|
+
"annotations": classification.get("annotations"),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _read_session_tail(path: Any, max_bytes: int = _TAIL_BYTES) -> str:
|
|
98
|
+
if not path:
|
|
99
|
+
return ""
|
|
100
|
+
try:
|
|
101
|
+
p = Path(str(path))
|
|
102
|
+
size = p.stat().st_size
|
|
103
|
+
with p.open("rb") as handle:
|
|
104
|
+
if size > max_bytes:
|
|
105
|
+
handle.seek(size - max_bytes)
|
|
106
|
+
# drop a possibly-partial first line after seeking mid-file
|
|
107
|
+
handle.readline()
|
|
108
|
+
data = handle.read()
|
|
109
|
+
return data.decode("utf-8", errors="ignore")
|
|
110
|
+
except (OSError, ValueError):
|
|
111
|
+
return ""
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
from typing import Any
|
|
6
6
|
|
|
7
7
|
from team_agent.events import EventLog
|
|
8
|
+
from team_agent.display.backend import DISPLAY_BACKENDS_WITH_WORKER_VIEWS, resolve_display_backend
|
|
8
9
|
from team_agent.launch.bootstrap import (
|
|
9
10
|
attach_team_profile_dirs,
|
|
10
11
|
spec_team_dir,
|
|
@@ -32,7 +33,6 @@ def launch(
|
|
|
32
33
|
skip_profile_smoke: bool = False,
|
|
33
34
|
) -> dict[str, Any]:
|
|
34
35
|
from team_agent.runtime import (
|
|
35
|
-
GHOSTTY_DISPLAY_BACKENDS,
|
|
36
36
|
RuntimeError,
|
|
37
37
|
_attach_leader_to_state,
|
|
38
38
|
_capture_agent_session,
|
|
@@ -54,6 +54,11 @@ def launch(
|
|
|
54
54
|
ensure_workspace_dirs(workspace)
|
|
55
55
|
event_log = EventLog(workspace)
|
|
56
56
|
session_name = spec.get("runtime", {}).get("session_name") or f"team-{spec['team']['name']}"
|
|
57
|
+
display_backend = resolve_display_backend(
|
|
58
|
+
spec.get("runtime", {}).get("display_backend"),
|
|
59
|
+
event_log=event_log,
|
|
60
|
+
source="launch",
|
|
61
|
+
)
|
|
57
62
|
state = {
|
|
58
63
|
"spec_path": str(spec_path.resolve()),
|
|
59
64
|
"workspace": str(workspace),
|
|
@@ -62,7 +67,7 @@ def launch(
|
|
|
62
67
|
"leader": spec.get("leader"),
|
|
63
68
|
"agents": {},
|
|
64
69
|
"tasks": [dict(task) for task in spec.get("tasks", [])],
|
|
65
|
-
"display_backend":
|
|
70
|
+
"display_backend": display_backend,
|
|
66
71
|
}
|
|
67
72
|
runtime_cfg = effective_runtime_config(spec.get("runtime", {}))
|
|
68
73
|
dangerous_auto_approve = bool(runtime_cfg.get("dangerous_auto_approve"))
|
|
@@ -215,7 +220,11 @@ def launch(
|
|
|
215
220
|
stdout=proc.stdout,
|
|
216
221
|
)
|
|
217
222
|
raise RuntimeError(f"Failed to start agent {agent['id']}: {proc.stderr.strip()}")
|
|
218
|
-
handled_prompts =
|
|
223
|
+
handled_prompts = (
|
|
224
|
+
adapter.handle_startup_prompts(session_name, agent["id"], checks=20, sleep_s=0.5)
|
|
225
|
+
if hasattr(adapter, "handle_startup_prompts")
|
|
226
|
+
else []
|
|
227
|
+
)
|
|
219
228
|
for prompt_event in handled_prompts:
|
|
220
229
|
event_log.write(
|
|
221
230
|
"launch.startup_prompt_handled",
|
|
@@ -263,7 +272,7 @@ def launch(
|
|
|
263
272
|
exclude_session_ids=known_session_ids,
|
|
264
273
|
raise_on_missed=False,
|
|
265
274
|
)
|
|
266
|
-
if state.get("display_backend") in
|
|
275
|
+
if state.get("display_backend") in DISPLAY_BACKENDS_WITH_WORKER_VIEWS:
|
|
267
276
|
display_jobs.append((agent["id"], agent))
|
|
268
277
|
started.append({"agent_id": agent["id"], "provider": agent["provider"], "window": agent["id"]})
|
|
269
278
|
for agent_id, display in _open_worker_displays(
|