@team-agent/installer 0.1.11 → 0.2.1
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/crates/team-agent-core/src/lib.rs +50 -5
- package/package.json +1 -1
- package/schemas/team.schema.json +1 -0
- package/src/team_agent/approvals/__init__.py +65 -0
- package/src/team_agent/approvals/constants.py +6 -0
- package/src/team_agent/approvals/parsing.py +176 -0
- package/src/team_agent/approvals/runtime_prompts.py +171 -0
- package/src/team_agent/approvals/status.py +165 -0
- package/src/team_agent/cli/__init__.py +137 -0
- package/src/team_agent/cli/commands.py +339 -0
- package/src/team_agent/cli/e2e.py +202 -0
- package/src/team_agent/cli/helpers.py +137 -0
- package/src/team_agent/cli/parser.py +477 -0
- package/src/team_agent/compiler.py +98 -33
- package/src/team_agent/coordinator/__init__.py +53 -0
- package/src/team_agent/{coordinator.py → coordinator/__main__.py} +3 -1
- package/src/team_agent/coordinator/lifecycle.py +334 -0
- package/src/team_agent/coordinator/metadata.py +61 -0
- package/src/team_agent/coordinator/paths.py +17 -0
- package/src/team_agent/diagnose/__init__.py +48 -0
- package/src/team_agent/diagnose/checks.py +101 -0
- package/src/team_agent/diagnose/health.py +241 -0
- package/src/team_agent/diagnose/preflight.py +194 -0
- package/src/team_agent/diagnose/quick_start.py +233 -0
- package/src/team_agent/display/__init__.py +61 -0
- package/src/team_agent/display/close.py +147 -0
- package/src/team_agent/display/ghostty.py +77 -0
- package/src/team_agent/display/worker_window.py +110 -0
- package/src/team_agent/display/workspace.py +473 -0
- package/src/team_agent/launch/__init__.py +41 -0
- package/src/team_agent/launch/bootstrap.py +85 -0
- package/src/team_agent/launch/config.py +106 -0
- package/src/team_agent/launch/core.py +291 -0
- package/src/team_agent/launch/requirements.py +57 -0
- package/src/team_agent/leader/__init__.py +320 -0
- package/src/team_agent/lifecycle/__init__.py +5 -0
- package/src/team_agent/lifecycle/agents.py +226 -0
- package/src/team_agent/lifecycle/operations.py +321 -0
- package/src/team_agent/lifecycle/paste_buffer_hygiene.py +39 -0
- package/src/team_agent/lifecycle/start.py +363 -0
- package/src/team_agent/mcp_server/__init__.py +42 -0
- package/src/team_agent/mcp_server/__main__.py +7 -0
- package/src/team_agent/mcp_server/contracts.py +148 -0
- package/src/team_agent/mcp_server/normalize.py +257 -0
- package/src/team_agent/mcp_server/server.py +150 -0
- package/src/team_agent/mcp_server/tools.py +205 -0
- package/src/team_agent/message_store/__init__.py +23 -0
- package/src/team_agent/message_store/agent_health.py +109 -0
- package/src/team_agent/{message_store.py → message_store/core.py} +188 -245
- package/src/team_agent/message_store/result_watchers.py +102 -0
- package/src/team_agent/message_store/schema.py +266 -0
- package/src/team_agent/messaging/__init__.py +1 -0
- package/src/team_agent/messaging/activity_detector.py +190 -0
- package/src/team_agent/messaging/delivery.py +138 -0
- package/src/team_agent/messaging/deps.py +263 -0
- package/src/team_agent/messaging/idle_alerts.py +323 -0
- package/src/team_agent/messaging/internal_delivery.py +46 -0
- package/src/team_agent/messaging/leader.py +317 -0
- package/src/team_agent/messaging/leader_panes.py +343 -0
- package/src/team_agent/messaging/owner_bypass.py +29 -0
- package/src/team_agent/messaging/result_delivery.py +300 -0
- package/src/team_agent/messaging/results.py +456 -0
- package/src/team_agent/messaging/scheduler.py +428 -0
- package/src/team_agent/messaging/send.py +500 -0
- package/src/team_agent/messaging/session_drift.py +94 -0
- package/src/team_agent/messaging/tmux_io.py +337 -0
- package/src/team_agent/messaging/tmux_prompt.py +229 -0
- package/src/team_agent/orchestrator/__init__.py +376 -0
- package/src/team_agent/orchestrator/plan.py +122 -0
- package/src/team_agent/orchestrator/state.py +128 -0
- package/src/team_agent/profiles/__init__.py +82 -0
- package/src/team_agent/profiles/constants.py +19 -0
- package/src/team_agent/profiles/core.py +407 -0
- package/src/team_agent/profiles/helpers.py +69 -0
- package/src/team_agent/profiles/provider_env.py +188 -0
- package/src/team_agent/profiles/smoke.py +201 -0
- package/src/team_agent/provider_cli/__init__.py +43 -0
- package/src/team_agent/provider_cli/adapter.py +167 -0
- package/src/team_agent/provider_cli/base.py +48 -0
- package/src/team_agent/provider_cli/claude.py +457 -0
- package/src/team_agent/provider_cli/codex.py +319 -0
- package/src/team_agent/provider_cli/copilot.py +8 -0
- package/src/team_agent/provider_cli/fake.py +39 -0
- package/src/team_agent/provider_cli/gemini.py +95 -0
- package/src/team_agent/provider_cli/opencode.py +8 -0
- package/src/team_agent/provider_cli/prompt.py +62 -0
- package/src/team_agent/provider_cli/registry.py +18 -0
- package/src/team_agent/provider_cli/unsupported.py +32 -0
- package/src/team_agent/providers.py +67 -949
- package/src/team_agent/quality_gates.py +104 -0
- package/src/team_agent/restart/__init__.py +34 -0
- package/src/team_agent/restart/orchestration.py +328 -0
- package/src/team_agent/restart/selection.py +89 -0
- package/src/team_agent/restart/snapshot.py +70 -0
- package/src/team_agent/runtime.py +809 -5892
- package/src/team_agent/rust_core.py +22 -5
- package/src/team_agent/sessions/__init__.py +25 -0
- package/src/team_agent/sessions/capture.py +93 -0
- package/src/team_agent/sessions/inventory.py +44 -0
- package/src/team_agent/sessions/resume.py +135 -0
- package/src/team_agent/spec.py +3 -1
- package/src/team_agent/state.py +218 -4
- package/src/team_agent/status/__init__.py +63 -0
- package/src/team_agent/status/approvals.py +52 -0
- package/src/team_agent/status/compact.py +158 -0
- package/src/team_agent/status/constants.py +18 -0
- package/src/team_agent/status/inbox.py +28 -0
- package/src/team_agent/status/peek.py +117 -0
- package/src/team_agent/status/queries.py +168 -0
- package/src/team_agent/terminal.py +57 -0
- package/src/team_agent/cli.py +0 -858
- package/src/team_agent/mcp_server.py +0 -579
- package/src/team_agent/profiles.py +0 -882
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from team_agent.messaging.deps import (
|
|
4
|
+
TMUX_PASTE_BYTES_PER_SECOND,
|
|
5
|
+
TMUX_PASTE_MAX_READY_TIMEOUT,
|
|
6
|
+
TMUX_PASTE_MIN_READY_TIMEOUT,
|
|
7
|
+
TMUX_STDIN_BUFFER_THRESHOLD,
|
|
8
|
+
TMUX_SUBMIT_BYTES_PER_SECOND,
|
|
9
|
+
TMUX_SUBMIT_MAX_SETTLE_TIMEOUT,
|
|
10
|
+
TMUX_SUBMIT_MIN_SETTLE_TIMEOUT,
|
|
11
|
+
_capture_tmux_pane_text,
|
|
12
|
+
_tmux_load_buffer_stdin as _runtime_tmux_load_buffer_stdin,
|
|
13
|
+
_submit_worker_prompt,
|
|
14
|
+
_wait_for_message_ready,
|
|
15
|
+
re,
|
|
16
|
+
run_cmd,
|
|
17
|
+
subprocess,
|
|
18
|
+
time,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
def _tmux_inject_text(
|
|
25
|
+
target: str,
|
|
26
|
+
text: str,
|
|
27
|
+
submit_key: str,
|
|
28
|
+
buffer_name: str,
|
|
29
|
+
attempts: int = 3,
|
|
30
|
+
provider: str = "fake",
|
|
31
|
+
) -> dict[str, Any]:
|
|
32
|
+
token_match = re.search(r"\[team-agent-token:([^\]]+)\]", text)
|
|
33
|
+
token = token_match.group(1) if token_match else ""
|
|
34
|
+
attempt_log: list[dict[str, Any]] = []
|
|
35
|
+
last_verification = "not_checked"
|
|
36
|
+
ready_timeout = _tmux_paste_ready_timeout(text)
|
|
37
|
+
submit_settle_timeout = _tmux_submit_settle_timeout(text)
|
|
38
|
+
text_bytes = _tmux_text_size(text)
|
|
39
|
+
for attempt in range(1, max(attempts, 1) + 1):
|
|
40
|
+
prepared = _prepare_tmux_pane_for_input(target)
|
|
41
|
+
if not prepared["ok"]:
|
|
42
|
+
attempt_log.append({"attempt": attempt, "visible": False, "verification": prepared["verification"]})
|
|
43
|
+
return {
|
|
44
|
+
"ok": False,
|
|
45
|
+
"stage": prepared["stage"],
|
|
46
|
+
"error": prepared.get("error"),
|
|
47
|
+
"attempts": attempt_log,
|
|
48
|
+
"verification": prepared["verification"],
|
|
49
|
+
}
|
|
50
|
+
baseline = _capture_tmux_pane_text(target)
|
|
51
|
+
if not baseline["ok"]:
|
|
52
|
+
return {
|
|
53
|
+
"ok": False,
|
|
54
|
+
"stage": "pre-paste-capture",
|
|
55
|
+
"error": baseline.get("error"),
|
|
56
|
+
"attempts": attempt_log,
|
|
57
|
+
"verification": "pre_paste_capture_failed",
|
|
58
|
+
}
|
|
59
|
+
baseline_capture = baseline["capture"]
|
|
60
|
+
buffered = _tmux_set_buffer_text(buffer_name, text)
|
|
61
|
+
if not buffered["ok"]:
|
|
62
|
+
return {"ok": False, "stage": buffered["stage"], "error": buffered.get("error"), "attempts": attempt_log}
|
|
63
|
+
proc = run_cmd(["tmux", "paste-buffer", "-t", target, "-b", buffer_name, "-p"], timeout=10)
|
|
64
|
+
deleted = _tmux_delete_buffer(buffer_name)
|
|
65
|
+
if proc.returncode != 0:
|
|
66
|
+
return {
|
|
67
|
+
"ok": False,
|
|
68
|
+
"stage": "paste-buffer",
|
|
69
|
+
"error": proc.stderr.strip(),
|
|
70
|
+
"attempts": attempt_log,
|
|
71
|
+
"buffer_deleted": deleted.get("ok"),
|
|
72
|
+
"buffer_delete_error": deleted.get("error"),
|
|
73
|
+
}
|
|
74
|
+
time.sleep(0.25)
|
|
75
|
+
if token:
|
|
76
|
+
visible, verification, capture_text = _wait_for_message_ready(
|
|
77
|
+
target,
|
|
78
|
+
token,
|
|
79
|
+
ready_timeout,
|
|
80
|
+
expected_text=text,
|
|
81
|
+
baseline_capture=baseline_capture,
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
visible, verification, capture_text = True, "no_token", ""
|
|
85
|
+
last_verification = verification
|
|
86
|
+
attempt_entry = {
|
|
87
|
+
"attempt": attempt,
|
|
88
|
+
"visible": visible,
|
|
89
|
+
"verification": verification,
|
|
90
|
+
"buffer_method": buffered.get("method"),
|
|
91
|
+
"buffer_name": buffer_name,
|
|
92
|
+
"buffer_deleted": deleted.get("ok"),
|
|
93
|
+
"text_bytes": buffered.get("text_bytes"),
|
|
94
|
+
"ready_timeout_sec": ready_timeout,
|
|
95
|
+
}
|
|
96
|
+
if deleted.get("error"):
|
|
97
|
+
attempt_entry["buffer_delete_error"] = deleted.get("error")
|
|
98
|
+
if prepared.get("recovered_from_mode"):
|
|
99
|
+
attempt_entry["recovered_from_mode"] = True
|
|
100
|
+
attempt_log.append(attempt_entry)
|
|
101
|
+
if not visible:
|
|
102
|
+
time.sleep(0.2)
|
|
103
|
+
continue
|
|
104
|
+
submit = _submit_worker_prompt(
|
|
105
|
+
target,
|
|
106
|
+
capture_text,
|
|
107
|
+
submit_key=submit_key,
|
|
108
|
+
settle_timeout=submit_settle_timeout,
|
|
109
|
+
)
|
|
110
|
+
if not submit["ok"]:
|
|
111
|
+
return {
|
|
112
|
+
"ok": False,
|
|
113
|
+
"stage": submit.get("stage", "submit"),
|
|
114
|
+
"error": submit.get("error"),
|
|
115
|
+
"attempts": attempt_log,
|
|
116
|
+
"verification": verification,
|
|
117
|
+
"submit_verification": submit.get("verification"),
|
|
118
|
+
"submit_attempts": submit.get("attempts"),
|
|
119
|
+
}
|
|
120
|
+
submit_verification = _leader_submit_verification(submit.get("verification"), verification, submit_key)
|
|
121
|
+
turn_visible, turn_verification, turn_capture = _wait_for_leader_new_turn(
|
|
122
|
+
target,
|
|
123
|
+
text,
|
|
124
|
+
token,
|
|
125
|
+
provider=provider,
|
|
126
|
+
timeout=2.0,
|
|
127
|
+
)
|
|
128
|
+
if not turn_visible:
|
|
129
|
+
return {
|
|
130
|
+
"ok": False,
|
|
131
|
+
"stage": "turn-boundary-verification",
|
|
132
|
+
"error": f"leader turn boundary not verified: {turn_verification}",
|
|
133
|
+
"attempts": attempt_log,
|
|
134
|
+
"verification": verification,
|
|
135
|
+
"submit_verification": submit_verification,
|
|
136
|
+
"turn_verification": turn_verification,
|
|
137
|
+
"submit_attempts": submit.get("attempts"),
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
"ok": True,
|
|
141
|
+
"stage": "submitted",
|
|
142
|
+
"visible": True,
|
|
143
|
+
"submitted": True,
|
|
144
|
+
"verification": verification,
|
|
145
|
+
"submit_verification": submit_verification,
|
|
146
|
+
"turn_verification": turn_verification,
|
|
147
|
+
"attempts": attempt_log,
|
|
148
|
+
"submit_attempts": submit.get("attempts"),
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
"ok": False,
|
|
152
|
+
"stage": "visible-check",
|
|
153
|
+
"error": f"visible token not found after {max(attempts, 1)} attempts: {last_verification}",
|
|
154
|
+
"attempts": attempt_log,
|
|
155
|
+
"verification": last_verification,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _leader_submit_verification(submit_verification: str | None, verification: str, submit_key: str) -> str | None:
|
|
160
|
+
if submit_verification != "enter_sent_without_placeholder_check":
|
|
161
|
+
return submit_verification
|
|
162
|
+
if verification == "capture_contains_token":
|
|
163
|
+
return f"{submit_key}_sent_after_visible_token"
|
|
164
|
+
if verification == "capture_contains_message_fragment":
|
|
165
|
+
return f"{submit_key}_sent_after_visible_fragment"
|
|
166
|
+
return submit_verification
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _wait_for_leader_new_turn(
|
|
170
|
+
target: str,
|
|
171
|
+
expected_text: str,
|
|
172
|
+
token: str,
|
|
173
|
+
provider: str,
|
|
174
|
+
timeout: float,
|
|
175
|
+
) -> tuple[bool, str, str]:
|
|
176
|
+
deadline = time.monotonic() + max(timeout, 0.0)
|
|
177
|
+
last = "not_checked"
|
|
178
|
+
last_capture = ""
|
|
179
|
+
while True:
|
|
180
|
+
capture = _capture_tmux_pane_text(target)
|
|
181
|
+
if capture["ok"]:
|
|
182
|
+
capture_text = capture["capture"]
|
|
183
|
+
last_capture = capture_text
|
|
184
|
+
if _capture_has_leader_new_turn(capture_text, expected_text, token, provider):
|
|
185
|
+
return True, "leader_new_turn_boundary_verified", capture_text
|
|
186
|
+
last = "leader_new_turn_boundary_missing"
|
|
187
|
+
else:
|
|
188
|
+
last = f"capture_failed: {capture.get('error')}"
|
|
189
|
+
if time.monotonic() >= deadline:
|
|
190
|
+
return False, last, last_capture
|
|
191
|
+
time.sleep(0.1)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _capture_has_leader_new_turn(capture_text: str, expected_text: str, token: str, provider: str) -> bool:
|
|
195
|
+
if provider == "fake":
|
|
196
|
+
return True
|
|
197
|
+
lines = capture_text.splitlines()
|
|
198
|
+
marker_indexes = [index for index, line in enumerate(lines) if re.match(r"^\s*[❯›>]\s*", line)]
|
|
199
|
+
for index in marker_indexes:
|
|
200
|
+
window = "\n".join(lines[index : index + 12])
|
|
201
|
+
if token and token in window:
|
|
202
|
+
return True
|
|
203
|
+
if _leader_turn_contains_message_fragment(window, expected_text):
|
|
204
|
+
return True
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _leader_turn_contains_message_fragment(capture_text: str, expected_text: str) -> bool:
|
|
209
|
+
haystack = re.sub(r"\s+", "", capture_text)
|
|
210
|
+
for line in expected_text.splitlines():
|
|
211
|
+
compact = re.sub(r"\s+", "", re.sub(r"\[team-agent-token:[^\]]+\]", "", line))
|
|
212
|
+
if len(compact) >= 18 and compact in haystack:
|
|
213
|
+
return True
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _tmux_text_size(text: str) -> int:
|
|
218
|
+
return len(text.encode("utf-8"))
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _tmux_paste_ready_timeout(text: str) -> float:
|
|
222
|
+
size = _tmux_text_size(text)
|
|
223
|
+
return min(
|
|
224
|
+
TMUX_PASTE_MAX_READY_TIMEOUT,
|
|
225
|
+
max(TMUX_PASTE_MIN_READY_TIMEOUT, size / TMUX_PASTE_BYTES_PER_SECOND),
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _tmux_submit_settle_timeout(text: str) -> float:
|
|
230
|
+
size = _tmux_text_size(text)
|
|
231
|
+
return min(
|
|
232
|
+
TMUX_SUBMIT_MAX_SETTLE_TIMEOUT,
|
|
233
|
+
max(TMUX_SUBMIT_MIN_SETTLE_TIMEOUT, size / TMUX_SUBMIT_BYTES_PER_SECOND),
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _tmux_set_buffer_text(buffer_name: str, text: str) -> dict[str, Any]:
|
|
238
|
+
size = _tmux_text_size(text)
|
|
239
|
+
if size >= TMUX_STDIN_BUFFER_THRESHOLD:
|
|
240
|
+
proc = _runtime_tmux_load_buffer_stdin(buffer_name, text)
|
|
241
|
+
return {
|
|
242
|
+
"ok": proc.returncode == 0,
|
|
243
|
+
"stage": "load-buffer",
|
|
244
|
+
"method": "stdin_load_buffer",
|
|
245
|
+
"text_bytes": size,
|
|
246
|
+
"error": proc.stderr.strip() if proc.returncode != 0 else None,
|
|
247
|
+
}
|
|
248
|
+
proc = run_cmd(["tmux", "set-buffer", "-b", buffer_name, text], timeout=10)
|
|
249
|
+
return {
|
|
250
|
+
"ok": proc.returncode == 0,
|
|
251
|
+
"stage": "set-buffer",
|
|
252
|
+
"method": "set_buffer_arg",
|
|
253
|
+
"text_bytes": size,
|
|
254
|
+
"error": proc.stderr.strip() if proc.returncode != 0 else None,
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _tmux_delete_buffer(buffer_name: str) -> dict[str, Any]:
|
|
259
|
+
proc = run_cmd(["tmux", "delete-buffer", "-b", buffer_name], timeout=10)
|
|
260
|
+
return {
|
|
261
|
+
"ok": proc.returncode == 0,
|
|
262
|
+
"stage": "delete-buffer",
|
|
263
|
+
"error": proc.stderr.strip() if proc.returncode != 0 else None,
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _tmux_load_buffer_stdin(buffer_name: str, text: str) -> subprocess.CompletedProcess[str]:
|
|
268
|
+
return subprocess.run(
|
|
269
|
+
["tmux", "load-buffer", "-b", buffer_name, "-"],
|
|
270
|
+
input=text,
|
|
271
|
+
text=True,
|
|
272
|
+
capture_output=True,
|
|
273
|
+
timeout=10,
|
|
274
|
+
check=False,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _prepare_tmux_pane_for_input(target: str) -> dict[str, Any]:
|
|
279
|
+
mode = run_cmd(["tmux", "display-message", "-p", "-t", target, "#{pane_in_mode}"], timeout=5)
|
|
280
|
+
if mode.returncode != 0:
|
|
281
|
+
return {
|
|
282
|
+
"ok": False,
|
|
283
|
+
"stage": "pane-mode-check",
|
|
284
|
+
"verification": "pane_mode_check_failed",
|
|
285
|
+
"error": mode.stderr.strip() or "tmux pane mode check failed",
|
|
286
|
+
}
|
|
287
|
+
if mode.stdout.strip() != "1":
|
|
288
|
+
return {"ok": True, "verification": "pane_input_ready"}
|
|
289
|
+
cancel = run_cmd(["tmux", "send-keys", "-t", target, "-X", "cancel"], timeout=10)
|
|
290
|
+
if cancel.returncode != 0:
|
|
291
|
+
return {
|
|
292
|
+
"ok": False,
|
|
293
|
+
"stage": "pane-mode-cancel",
|
|
294
|
+
"verification": "pane_mode_cancel_failed",
|
|
295
|
+
"error": cancel.stderr.strip() or "tmux copy-mode cancel failed",
|
|
296
|
+
}
|
|
297
|
+
deadline = time.monotonic() + 1.5
|
|
298
|
+
while True:
|
|
299
|
+
check = run_cmd(["tmux", "display-message", "-p", "-t", target, "#{pane_in_mode}"], timeout=5)
|
|
300
|
+
if check.returncode != 0:
|
|
301
|
+
return {
|
|
302
|
+
"ok": False,
|
|
303
|
+
"stage": "pane-mode-check",
|
|
304
|
+
"verification": "pane_mode_recheck_failed",
|
|
305
|
+
"error": check.stderr.strip() or "tmux pane mode recheck failed",
|
|
306
|
+
}
|
|
307
|
+
if check.stdout.strip() != "1":
|
|
308
|
+
return {"ok": True, "verification": "pane_input_ready_after_mode_cancel", "recovered_from_mode": True}
|
|
309
|
+
if time.monotonic() >= deadline:
|
|
310
|
+
return {
|
|
311
|
+
"ok": False,
|
|
312
|
+
"stage": "pane-mode-cancel",
|
|
313
|
+
"verification": "pane_mode_still_active_after_cancel",
|
|
314
|
+
"error": "tmux pane stayed in copy-mode after cancel",
|
|
315
|
+
}
|
|
316
|
+
time.sleep(0.1)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from team_agent.messaging.deps import (
|
|
4
|
+
DELIVERY_CAPTURE_LINES,
|
|
5
|
+
PASTED_CONTENT_PROMPT_RE,
|
|
6
|
+
TMUX_SUBMIT_MIN_SETTLE_TIMEOUT,
|
|
7
|
+
re,
|
|
8
|
+
run_cmd,
|
|
9
|
+
time,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
def _enable_codex_fast_mode(session_name: str, window_name: str) -> dict[str, Any]:
|
|
16
|
+
target = f"{session_name}:{window_name}"
|
|
17
|
+
proc = run_cmd(["tmux", "send-keys", "-t", target, "/fast", "Enter"], timeout=10)
|
|
18
|
+
if proc.returncode != 0:
|
|
19
|
+
return {"ok": False, "error": proc.stderr.strip() or "tmux send-keys failed"}
|
|
20
|
+
return {"ok": True, "target": target}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _wait_for_visible_token(target: str, token: str, timeout: float) -> tuple[bool, str]:
|
|
24
|
+
deadline = time.monotonic() + max(timeout, 0.0)
|
|
25
|
+
last = "not_checked"
|
|
26
|
+
while True:
|
|
27
|
+
capture = _capture_tmux_pane_text(target)
|
|
28
|
+
if capture["ok"]:
|
|
29
|
+
if token in capture["capture"] or f"[team-agent-token:{token}]" in capture["capture"]:
|
|
30
|
+
return True, "capture_contains_token"
|
|
31
|
+
last = "capture_missing_token"
|
|
32
|
+
else:
|
|
33
|
+
last = f"capture_failed: {capture.get('error')}"
|
|
34
|
+
if time.monotonic() >= deadline:
|
|
35
|
+
return False, last
|
|
36
|
+
time.sleep(0.1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _capture_tmux_pane_text(target: str) -> dict[str, Any]:
|
|
40
|
+
capture = run_cmd(["tmux", "capture-pane", "-p", "-S", f"-{DELIVERY_CAPTURE_LINES}", "-t", target], timeout=5)
|
|
41
|
+
if capture.returncode != 0:
|
|
42
|
+
return {"ok": False, "capture": "", "error": capture.stderr.strip() or "tmux capture-pane failed"}
|
|
43
|
+
return {"ok": True, "capture": capture.stdout}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _wait_for_message_ready(
|
|
47
|
+
target: str,
|
|
48
|
+
message_id: str,
|
|
49
|
+
timeout: float,
|
|
50
|
+
expected_text: str = "",
|
|
51
|
+
allow_pasted_prompt: bool = True,
|
|
52
|
+
baseline_capture: str = "",
|
|
53
|
+
) -> tuple[bool, str, str]:
|
|
54
|
+
deadline = time.monotonic() + max(timeout, 0.0)
|
|
55
|
+
last = "not_checked"
|
|
56
|
+
last_capture = ""
|
|
57
|
+
baseline_had_pasted_prompt = _capture_has_pasted_content_prompt(baseline_capture)
|
|
58
|
+
while True:
|
|
59
|
+
capture = _capture_tmux_pane_text(target)
|
|
60
|
+
if capture["ok"]:
|
|
61
|
+
capture_text = capture["capture"]
|
|
62
|
+
last_capture = capture_text
|
|
63
|
+
if message_id in capture_text or f"[team-agent-token:{message_id}]" in capture_text:
|
|
64
|
+
return True, "capture_contains_token", capture_text
|
|
65
|
+
if expected_text and _capture_contains_message_fragment(capture_text, expected_text):
|
|
66
|
+
return True, "capture_contains_message_fragment", capture_text
|
|
67
|
+
if allow_pasted_prompt and _capture_has_pasted_content_prompt(capture_text) and not baseline_had_pasted_prompt:
|
|
68
|
+
return True, "capture_contains_new_pasted_content_prompt", capture_text
|
|
69
|
+
last = "capture_missing_token"
|
|
70
|
+
else:
|
|
71
|
+
last = f"capture_failed: {capture.get('error')}"
|
|
72
|
+
if time.monotonic() >= deadline:
|
|
73
|
+
return False, last, last_capture
|
|
74
|
+
time.sleep(0.1)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _wait_for_worker_message_ready(target: str, message_id: str, timeout: float, expected_text: str = "") -> tuple[bool, str, str]:
|
|
78
|
+
return _wait_for_message_ready(target, message_id, timeout, expected_text=expected_text)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _capture_has_pasted_content_prompt(text: str) -> bool:
|
|
82
|
+
lines = [line.rstrip() for line in text.splitlines() if line.strip()]
|
|
83
|
+
if not lines:
|
|
84
|
+
return False
|
|
85
|
+
tail = [line.strip() for line in lines[-12:]]
|
|
86
|
+
tail_text = " ".join(tail)
|
|
87
|
+
if not PASTED_CONTENT_PROMPT_RE.search(tail_text):
|
|
88
|
+
return False
|
|
89
|
+
prompt_markers = ("›", "❯", ">")
|
|
90
|
+
if PASTED_CONTENT_PROMPT_RE.search(tail[-1]):
|
|
91
|
+
return True
|
|
92
|
+
if tail[-1].endswith(("chars]", "line]", "lines]")):
|
|
93
|
+
return True
|
|
94
|
+
if any(line.startswith(prompt_markers) for line in tail):
|
|
95
|
+
return True
|
|
96
|
+
if re.search(r"\b(codex|claude)\s*[>›❯]", tail_text, re.IGNORECASE):
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _capture_contains_message_fragment(capture_text: str, expected_text: str) -> bool:
|
|
102
|
+
haystack = _compact_visible_text(capture_text)
|
|
103
|
+
if not haystack:
|
|
104
|
+
return False
|
|
105
|
+
fragments = _message_fragment_candidates(expected_text)
|
|
106
|
+
if not fragments:
|
|
107
|
+
return False
|
|
108
|
+
return any(fragment in haystack for fragment in fragments)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _message_fragment_candidates(text: str) -> list[str]:
|
|
112
|
+
sanitized = re.sub(r"\[team-agent-token:[^\]]+\]", "", text)
|
|
113
|
+
fragments: list[str] = []
|
|
114
|
+
for line in _message_content_lines(sanitized):
|
|
115
|
+
compact = _compact_visible_text(line)
|
|
116
|
+
if not _is_strong_message_fragment(compact):
|
|
117
|
+
continue
|
|
118
|
+
if len(compact) <= 72:
|
|
119
|
+
fragments.append(compact)
|
|
120
|
+
continue
|
|
121
|
+
midpoint = len(compact) // 2
|
|
122
|
+
fragments.extend(
|
|
123
|
+
[
|
|
124
|
+
compact[:36],
|
|
125
|
+
compact[max(0, midpoint - 18) : midpoint + 18],
|
|
126
|
+
compact[-36:],
|
|
127
|
+
]
|
|
128
|
+
)
|
|
129
|
+
unique: list[str] = []
|
|
130
|
+
seen: set[str] = set()
|
|
131
|
+
for fragment in fragments:
|
|
132
|
+
if fragment in seen:
|
|
133
|
+
continue
|
|
134
|
+
seen.add(fragment)
|
|
135
|
+
unique.append(fragment)
|
|
136
|
+
return unique
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _message_content_lines(text: str) -> list[str]:
|
|
140
|
+
lines = text.splitlines()
|
|
141
|
+
if lines and lines[0].strip().startswith("Team Agent message from "):
|
|
142
|
+
lines = lines[1:]
|
|
143
|
+
return [line for line in lines if line.strip()]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _is_strong_message_fragment(compact: str) -> bool:
|
|
147
|
+
if not compact:
|
|
148
|
+
return False
|
|
149
|
+
generic_prefixes = (
|
|
150
|
+
"TeamAgentmessagefrom",
|
|
151
|
+
"TeamAgentpeermessagefrom",
|
|
152
|
+
"TeamAgentstoredthisresult",
|
|
153
|
+
"TeamAgenthascollectedthisresult",
|
|
154
|
+
"Nomanualpolling",
|
|
155
|
+
)
|
|
156
|
+
if compact.startswith(generic_prefixes):
|
|
157
|
+
return False
|
|
158
|
+
if re.fullmatch(r"[-::>›❯]+", compact):
|
|
159
|
+
return False
|
|
160
|
+
if re.search(r"(msg|res)_[0-9A-Fa-f]{8,}", compact):
|
|
161
|
+
return True
|
|
162
|
+
cjk_count = len(re.findall(r"[\u4e00-\u9fff]", compact))
|
|
163
|
+
if cjk_count >= 4 and len(compact) >= 6:
|
|
164
|
+
return True
|
|
165
|
+
return len(compact) >= 18
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _compact_visible_text(text: str) -> str:
|
|
169
|
+
return re.sub(r"\s+", "", text)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _submit_worker_prompt(
|
|
173
|
+
target: str,
|
|
174
|
+
before_capture: str,
|
|
175
|
+
submit_key: str = "Enter",
|
|
176
|
+
attempts: int = 3,
|
|
177
|
+
settle_timeout: float = TMUX_SUBMIT_MIN_SETTLE_TIMEOUT,
|
|
178
|
+
) -> dict[str, Any]:
|
|
179
|
+
verify_pasted_prompt = _capture_has_pasted_content_prompt(before_capture)
|
|
180
|
+
attempt_log: list[dict[str, Any]] = []
|
|
181
|
+
for attempt in range(1, max(attempts, 1) + 1):
|
|
182
|
+
proc = run_cmd(["tmux", "send-keys", "-t", target, submit_key], timeout=10)
|
|
183
|
+
if proc.returncode != 0:
|
|
184
|
+
return {
|
|
185
|
+
"ok": False,
|
|
186
|
+
"stage": "send-keys",
|
|
187
|
+
"verification": "send_keys_failed",
|
|
188
|
+
"error": proc.stderr.strip(),
|
|
189
|
+
"attempts": attempt_log,
|
|
190
|
+
}
|
|
191
|
+
if not verify_pasted_prompt:
|
|
192
|
+
return {
|
|
193
|
+
"ok": True,
|
|
194
|
+
"stage": "submitted",
|
|
195
|
+
"verification": "enter_sent_without_placeholder_check",
|
|
196
|
+
"attempts": attempt_log + [{"attempt": attempt, "submitted": True, "verification": "not_required"}],
|
|
197
|
+
}
|
|
198
|
+
cleared, verification = _wait_for_pasted_prompt_cleared(target, settle_timeout)
|
|
199
|
+
attempt_log.append({"attempt": attempt, "submitted": True, "verification": verification})
|
|
200
|
+
if cleared:
|
|
201
|
+
return {
|
|
202
|
+
"ok": True,
|
|
203
|
+
"stage": "submitted",
|
|
204
|
+
"verification": "pasted_content_prompt_absent_after_submit",
|
|
205
|
+
"attempts": attempt_log,
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
"ok": False,
|
|
209
|
+
"stage": "submit-verification",
|
|
210
|
+
"verification": "pasted_content_prompt_still_present_after_retries",
|
|
211
|
+
"error": "pasted content prompt still present after Enter retries",
|
|
212
|
+
"attempts": attempt_log,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _wait_for_pasted_prompt_cleared(target: str, timeout: float) -> tuple[bool, str]:
|
|
217
|
+
polls = max(1, int(max(timeout, 0.0) / 0.1) + 1)
|
|
218
|
+
last = "pasted_content_prompt_still_present"
|
|
219
|
+
for poll in range(polls):
|
|
220
|
+
capture = run_cmd(["tmux", "capture-pane", "-p", "-S", f"-{DELIVERY_CAPTURE_LINES}", "-t", target], timeout=5)
|
|
221
|
+
if capture.returncode != 0:
|
|
222
|
+
last = "capture_failed"
|
|
223
|
+
elif not _capture_has_pasted_content_prompt(capture.stdout):
|
|
224
|
+
return True, "pasted_content_prompt_absent"
|
|
225
|
+
else:
|
|
226
|
+
last = "pasted_content_prompt_still_present"
|
|
227
|
+
if poll < polls - 1:
|
|
228
|
+
time.sleep(0.1)
|
|
229
|
+
return False, last
|