@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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/team_agent/abnormal_track.py +253 -0
  3. package/src/team_agent/compiler.py +1 -1
  4. package/src/team_agent/coordinator/lifecycle.py +20 -2
  5. package/src/team_agent/display/__init__.py +31 -0
  6. package/src/team_agent/display/adaptive.py +425 -0
  7. package/src/team_agent/display/backend.py +46 -0
  8. package/src/team_agent/display/close.py +6 -0
  9. package/src/team_agent/display/rebuild.py +102 -0
  10. package/src/team_agent/display/tiling.py +156 -0
  11. package/src/team_agent/display/worker_window.py +4 -0
  12. package/src/team_agent/display/workspace.py +36 -127
  13. package/src/team_agent/idle_predicate.py +200 -0
  14. package/src/team_agent/idle_takeover.py +59 -0
  15. package/src/team_agent/idle_takeover_wiring.py +111 -0
  16. package/src/team_agent/launch/core.py +13 -4
  17. package/src/team_agent/leader/__init__.py +444 -61
  18. package/src/team_agent/message_store/core.py +30 -4
  19. package/src/team_agent/message_store/leader_notification_log.py +47 -26
  20. package/src/team_agent/messaging/delivery.py +45 -2
  21. package/src/team_agent/messaging/leader_panes.py +115 -21
  22. package/src/team_agent/messaging/send.py +33 -0
  23. package/src/team_agent/messaging/tmux_io.py +49 -10
  24. package/src/team_agent/messaging/trust_auto_answer.py +11 -3
  25. package/src/team_agent/provider_state/README.md +78 -0
  26. package/src/team_agent/provider_state/__init__.py +86 -0
  27. package/src/team_agent/provider_state/claude.py +86 -0
  28. package/src/team_agent/provider_state/codex.py +84 -0
  29. package/src/team_agent/provider_state/common.py +207 -0
  30. package/src/team_agent/provider_state/registry.py +118 -0
  31. package/src/team_agent/restart/orchestration.py +9 -9
  32. package/src/team_agent/runtime.py +62 -12
  33. package/src/team_agent/spec.py +4 -3
  34. 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": spec.get("runtime", {}).get("display_backend", "none"),
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 = adapter.handle_startup_prompts(session_name, agent["id"], checks=20, sleep_s=0.5)
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 GHOSTTY_DISPLAY_BACKENDS:
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(