@team-agent/installer 0.1.2 → 0.1.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/README.md +2 -4
- package/README.zh.md +2 -4
- package/package.json +10 -2
- package/pyproject.toml +1 -1
- package/scripts/run_regression_tests.py +4 -0
- package/src/team_agent/__init__.py +1 -1
- package/src/team_agent/mcp_server.py +1 -1
- package/src/team_agent/message_store.py +23 -0
- package/src/team_agent/runtime.py +155 -38
- package/tests/run_tests.py +5474 -0
- package/docs/README.md +0 -10
- package/docs/team-agent-foundation-and-boundaries.md +0 -292
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
**English** | [中文](README.zh.md)
|
|
1
|
+
**English** | [中文](https://github.com/Florious95/team-agent/blob/main/README.zh.md)
|
|
2
2
|
|
|
3
3
|
# Team Agent
|
|
4
4
|
|
|
@@ -83,8 +83,6 @@ Three design choices make this possible:
|
|
|
83
83
|
|
|
84
84
|
**3. Standards over inventions.** MCP for tool calls, Skill files for role definitions. Anything the broader ecosystem ships, this picks up automatically.
|
|
85
85
|
|
|
86
|
-
For the full design philosophy and boundaries, see [`docs/team-agent-foundation-and-boundaries.md`](./docs/team-agent-foundation-and-boundaries.md).
|
|
87
|
-
|
|
88
86
|
---
|
|
89
87
|
|
|
90
88
|
## Quick start
|
|
@@ -100,7 +98,7 @@ This sets up the MCP server, registers the Team Agent skill, and wires it into y
|
|
|
100
98
|
Source checkout install:
|
|
101
99
|
|
|
102
100
|
```bash
|
|
103
|
-
git clone
|
|
101
|
+
git clone https://github.com/Florious95/team-agent.git team-agent
|
|
104
102
|
cd team-agent
|
|
105
103
|
npm exec --yes --package . -- team-agent-installer install
|
|
106
104
|
```
|
package/README.zh.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[English](README.md) | **中文**
|
|
1
|
+
[English](https://github.com/Florious95/team-agent/blob/main/README.md) | **中文**
|
|
2
2
|
|
|
3
3
|
# Team Agent
|
|
4
4
|
|
|
@@ -82,8 +82,6 @@ Lead 自己想清楚:
|
|
|
82
82
|
|
|
83
83
|
**3. 用标准协议,不发明协议。** 用 MCP 做工具调用,用 Skill 文件做角色定义。生态发什么,本项目自动获得什么。
|
|
84
84
|
|
|
85
|
-
完整设计哲学和边界,见 [`docs/team-agent-foundation-and-boundaries.md`](./docs/team-agent-foundation-and-boundaries.md)。
|
|
86
|
-
|
|
87
85
|
---
|
|
88
86
|
|
|
89
87
|
## 快速开始
|
|
@@ -99,7 +97,7 @@ npx @team-agent/installer@latest install
|
|
|
99
97
|
源码安装:
|
|
100
98
|
|
|
101
99
|
```bash
|
|
102
|
-
git clone
|
|
100
|
+
git clone https://github.com/Florious95/team-agent.git team-agent
|
|
103
101
|
cd team-agent
|
|
104
102
|
npm exec --yes --package . -- team-agent-installer install
|
|
105
103
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
"tmux",
|
|
9
9
|
"multi-agent"
|
|
10
10
|
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/Florious95/team-agent.git"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/Florious95/team-agent#readme",
|
|
16
|
+
"bugs": {
|
|
17
|
+
"url": "https://github.com/Florious95/team-agent/issues"
|
|
18
|
+
},
|
|
11
19
|
"bin": {
|
|
12
20
|
"team-agent-installer": "npm/install.mjs"
|
|
13
21
|
},
|
|
@@ -21,11 +29,11 @@
|
|
|
21
29
|
"npm",
|
|
22
30
|
"scripts",
|
|
23
31
|
"src",
|
|
32
|
+
"tests",
|
|
24
33
|
"skills",
|
|
25
34
|
"templates",
|
|
26
35
|
"examples",
|
|
27
36
|
"schemas",
|
|
28
|
-
"docs/team-agent-foundation-and-boundaries.md",
|
|
29
37
|
"crates/team-agent-core/Cargo.toml",
|
|
30
38
|
"crates/team-agent-core/src",
|
|
31
39
|
"pyproject.toml",
|
package/pyproject.toml
CHANGED
|
@@ -10,6 +10,9 @@ from pathlib import Path
|
|
|
10
10
|
|
|
11
11
|
REGRESSION_TESTS = [
|
|
12
12
|
"tests.run_tests.RuntimeTests.test_send_default_timeout_reports_submitted_unverified",
|
|
13
|
+
"tests.run_tests.RuntimeTests.test_message_fragment_matching_ignores_generic_header",
|
|
14
|
+
"tests.run_tests.RuntimeTests.test_wait_for_message_ready_does_not_accept_old_header",
|
|
15
|
+
"tests.run_tests.RuntimeTests.test_wait_for_message_ready_accepts_only_new_pasted_prompt",
|
|
13
16
|
"tests.run_tests.RuntimeTests.test_worker_delivery_retries_paste_until_message_ready",
|
|
14
17
|
"tests.run_tests.RuntimeTests.test_worker_pasted_content_prompt_retries_enter_until_submitted",
|
|
15
18
|
"tests.run_tests.RuntimeTests.test_worker_pasted_content_prompt_reports_unverified_when_enter_does_not_submit",
|
|
@@ -26,6 +29,7 @@ REGRESSION_TESTS = [
|
|
|
26
29
|
"tests.run_tests.RuntimeTests.test_start_agent_falls_back_to_fresh_when_resume_window_exits",
|
|
27
30
|
"tests.run_tests.RuntimeTests.test_broadcast_sends_only_to_current_team_and_excludes_sender",
|
|
28
31
|
"tests.run_tests.RuntimeTests.test_status_and_collect_expose_uncollected_report_result",
|
|
32
|
+
"tests.run_tests.RuntimeTests.test_collect_accepts_message_scoped_report_result",
|
|
29
33
|
"tests.run_tests.RuntimeTests.test_report_result_queues_leader_notification_without_blocking_mcp",
|
|
30
34
|
"tests.run_tests.RuntimeTests.test_mcp_send_message_accepts_thin_args_and_returns_compact_result",
|
|
31
35
|
"tests.run_tests.RuntimeTests.test_mcp_send_message_accepts_broadcast_target",
|
|
@@ -505,7 +505,7 @@ def handle_mcp(tools: TeamOrchestratorTools, request: dict[str, Any]) -> dict[st
|
|
|
505
505
|
"result": {
|
|
506
506
|
"protocolVersion": request.get("params", {}).get("protocolVersion", "2024-11-05"),
|
|
507
507
|
"capabilities": {"tools": {}},
|
|
508
|
-
"serverInfo": {"name": "team_orchestrator", "version": "0.1.
|
|
508
|
+
"serverInfo": {"name": "team_orchestrator", "version": "0.1.4"},
|
|
509
509
|
},
|
|
510
510
|
}
|
|
511
511
|
if method == "tools/list":
|
|
@@ -475,6 +475,29 @@ class MessageStore:
|
|
|
475
475
|
)
|
|
476
476
|
return ids
|
|
477
477
|
|
|
478
|
+
def acknowledge_message(self, message_id: str, agent_id: str) -> list[str]:
|
|
479
|
+
now = utcnow()
|
|
480
|
+
with closing(self.connect()) as conn:
|
|
481
|
+
with conn:
|
|
482
|
+
row = conn.execute(
|
|
483
|
+
"""
|
|
484
|
+
select message_id from messages
|
|
485
|
+
where message_id = ? and recipient = ? and status in ('pending', 'accepted', 'target_resolved', 'injected', 'visible', 'submitted', 'delivered')
|
|
486
|
+
""",
|
|
487
|
+
(message_id, agent_id),
|
|
488
|
+
).fetchone()
|
|
489
|
+
if not row:
|
|
490
|
+
return []
|
|
491
|
+
conn.execute(
|
|
492
|
+
"""
|
|
493
|
+
update messages
|
|
494
|
+
set status = 'acknowledged', acknowledged_at = ?, updated_at = ?
|
|
495
|
+
where message_id = ? and recipient = ? and status in ('pending', 'accepted', 'target_resolved', 'injected', 'visible', 'submitted', 'delivered')
|
|
496
|
+
""",
|
|
497
|
+
(now, now, message_id, agent_id),
|
|
498
|
+
)
|
|
499
|
+
return [message_id]
|
|
500
|
+
|
|
478
501
|
def results(self, uncollected_only: bool = False) -> list[dict[str, Any]]:
|
|
479
502
|
query = "select * from results order by created_at"
|
|
480
503
|
if uncollected_only:
|
|
@@ -1290,13 +1290,15 @@ def collect(workspace: Path, result_file: Path | None = None) -> dict[str, Any]:
|
|
|
1290
1290
|
store.add_result(envelope)
|
|
1291
1291
|
|
|
1292
1292
|
rows = store.results(uncollected_only=True)
|
|
1293
|
-
valid_rows: list[tuple[dict[str, Any], dict[str, Any]]] = []
|
|
1293
|
+
valid_rows: list[tuple[dict[str, Any], dict[str, Any], dict[str, Any] | None]] = []
|
|
1294
1294
|
for row in rows:
|
|
1295
1295
|
envelope: Any = None
|
|
1296
1296
|
try:
|
|
1297
1297
|
envelope = json.loads(row["envelope"])
|
|
1298
1298
|
validate_result_envelope(envelope)
|
|
1299
|
-
|
|
1299
|
+
task = _find_task_or_none(state["tasks"], envelope["task_id"])
|
|
1300
|
+
if task is None and not _is_message_scoped_result(store, envelope):
|
|
1301
|
+
raise RuntimeError(f"unknown task id: {envelope['task_id']}")
|
|
1300
1302
|
except (json.JSONDecodeError, ValidationError, RuntimeError) as exc:
|
|
1301
1303
|
invalid_results.append(
|
|
1302
1304
|
_record_invalid_result(
|
|
@@ -1308,7 +1310,7 @@ def collect(workspace: Path, result_file: Path | None = None) -> dict[str, Any]:
|
|
|
1308
1310
|
)
|
|
1309
1311
|
store.mark_result_invalid(row["result_id"], str(exc))
|
|
1310
1312
|
else:
|
|
1311
|
-
valid_rows.append((row, envelope))
|
|
1313
|
+
valid_rows.append((row, envelope, task))
|
|
1312
1314
|
|
|
1313
1315
|
if invalid_results:
|
|
1314
1316
|
save_runtime_state(workspace, state)
|
|
@@ -1326,17 +1328,20 @@ def collect(workspace: Path, result_file: Path | None = None) -> dict[str, Any]:
|
|
|
1326
1328
|
collected: list[dict[str, Any]] = []
|
|
1327
1329
|
collected_results: list[dict[str, Any]] = []
|
|
1328
1330
|
next_state = copy.deepcopy(state)
|
|
1329
|
-
for row, envelope in valid_rows:
|
|
1330
|
-
task
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1331
|
+
for row, envelope, task in valid_rows:
|
|
1332
|
+
if task is not None:
|
|
1333
|
+
next_task = _find_task(next_state["tasks"], envelope["task_id"])
|
|
1334
|
+
task_status = _result_status_to_task_status(next_task, envelope["status"])
|
|
1335
|
+
update_task_status(
|
|
1336
|
+
next_state["tasks"],
|
|
1337
|
+
envelope["task_id"],
|
|
1338
|
+
task_status,
|
|
1339
|
+
envelope.get("summary"),
|
|
1340
|
+
envelope.get("artifacts", []),
|
|
1341
|
+
)
|
|
1342
|
+
next_task["accepted_result_id"] = row["result_id"]
|
|
1343
|
+
else:
|
|
1344
|
+
task_status = "message_scoped"
|
|
1340
1345
|
collected.append(envelope)
|
|
1341
1346
|
collected_results.append(
|
|
1342
1347
|
{
|
|
@@ -1346,6 +1351,7 @@ def collect(workspace: Path, result_file: Path | None = None) -> dict[str, Any]:
|
|
|
1346
1351
|
"status": envelope["status"],
|
|
1347
1352
|
"summary": envelope.get("summary"),
|
|
1348
1353
|
"tests": envelope.get("tests", []),
|
|
1354
|
+
"scope": "task" if task is not None else "message",
|
|
1349
1355
|
}
|
|
1350
1356
|
)
|
|
1351
1357
|
event_log.write(
|
|
@@ -1354,12 +1360,13 @@ def collect(workspace: Path, result_file: Path | None = None) -> dict[str, Any]:
|
|
|
1354
1360
|
task_id=envelope["task_id"],
|
|
1355
1361
|
status=envelope["status"],
|
|
1356
1362
|
task_status=task_status,
|
|
1357
|
-
retry_count=task.get("retry_count"),
|
|
1358
|
-
retry_limit=task.get("retry_limit"),
|
|
1363
|
+
retry_count=task.get("retry_count") if task else None,
|
|
1364
|
+
retry_limit=task.get("retry_limit") if task else None,
|
|
1365
|
+
scope="task" if task is not None else "message",
|
|
1359
1366
|
)
|
|
1360
1367
|
state_path = write_team_state(workspace, spec, next_state, [{"envelope": env} for env in collected])
|
|
1361
1368
|
save_runtime_state(workspace, next_state)
|
|
1362
|
-
for row, _ in valid_rows:
|
|
1369
|
+
for row, _, _ in valid_rows:
|
|
1363
1370
|
store.mark_result_collected(row["result_id"])
|
|
1364
1371
|
return {
|
|
1365
1372
|
"ok": not invalid_results,
|
|
@@ -1377,6 +1384,8 @@ def report_result(workspace: Path, envelope: dict[str, Any]) -> dict[str, Any]:
|
|
|
1377
1384
|
store = MessageStore(workspace)
|
|
1378
1385
|
result_id = store.add_result(envelope)
|
|
1379
1386
|
acknowledged = store.acknowledge_task_messages(envelope["task_id"], envelope["agent_id"])
|
|
1387
|
+
if not acknowledged:
|
|
1388
|
+
acknowledged = store.acknowledge_message(envelope["task_id"], envelope["agent_id"])
|
|
1380
1389
|
event_log = EventLog(workspace)
|
|
1381
1390
|
notification = _notify_leader_of_report_result(workspace, envelope, result_id, event_log)
|
|
1382
1391
|
leader_notified = bool(notification.get("ok")) and notification.get("status") in {"submitted", "visible", "delivered", "acknowledged"}
|
|
@@ -4018,8 +4027,24 @@ def _tmux_inject_text(target: str, text: str, submit_key: str, buffer_name: str,
|
|
|
4018
4027
|
"attempts": attempt_log,
|
|
4019
4028
|
"verification": prepared["verification"],
|
|
4020
4029
|
}
|
|
4030
|
+
baseline = _capture_tmux_pane_text(target)
|
|
4031
|
+
if not baseline["ok"]:
|
|
4032
|
+
return {
|
|
4033
|
+
"ok": False,
|
|
4034
|
+
"stage": "pre-paste-capture",
|
|
4035
|
+
"error": baseline.get("error"),
|
|
4036
|
+
"attempts": attempt_log,
|
|
4037
|
+
"verification": "pre_paste_capture_failed",
|
|
4038
|
+
}
|
|
4039
|
+
baseline_capture = baseline["capture"]
|
|
4021
4040
|
if token:
|
|
4022
|
-
pre_visible, pre_verification, pre_capture = _wait_for_message_ready(
|
|
4041
|
+
pre_visible, pre_verification, pre_capture = _wait_for_message_ready(
|
|
4042
|
+
target,
|
|
4043
|
+
token,
|
|
4044
|
+
0.0,
|
|
4045
|
+
expected_text=text,
|
|
4046
|
+
allow_pasted_prompt=False,
|
|
4047
|
+
)
|
|
4023
4048
|
if pre_visible:
|
|
4024
4049
|
attempt_entry = {
|
|
4025
4050
|
"attempt": attempt,
|
|
@@ -4060,6 +4085,23 @@ def _tmux_inject_text(target: str, text: str, submit_key: str, buffer_name: str,
|
|
|
4060
4085
|
"attempts": attempt_log,
|
|
4061
4086
|
"submit_attempts": submit.get("attempts"),
|
|
4062
4087
|
}
|
|
4088
|
+
if _capture_has_pasted_content_prompt(baseline_capture):
|
|
4089
|
+
attempt_log.append(
|
|
4090
|
+
{
|
|
4091
|
+
"attempt": attempt,
|
|
4092
|
+
"visible": False,
|
|
4093
|
+
"verification": "preexisting_unverified_pasted_content_prompt",
|
|
4094
|
+
"text_bytes": text_bytes,
|
|
4095
|
+
"ready_timeout_sec": 0.0,
|
|
4096
|
+
}
|
|
4097
|
+
)
|
|
4098
|
+
return {
|
|
4099
|
+
"ok": False,
|
|
4100
|
+
"stage": "preexisting-input",
|
|
4101
|
+
"error": "target pane already has an unverified pasted-content prompt; refusing to paste again to avoid duplicate messages",
|
|
4102
|
+
"attempts": attempt_log,
|
|
4103
|
+
"verification": "preexisting_unverified_pasted_content_prompt",
|
|
4104
|
+
}
|
|
4063
4105
|
buffered = _tmux_set_buffer_text(buffer_name, text)
|
|
4064
4106
|
if not buffered["ok"]:
|
|
4065
4107
|
return {"ok": False, "stage": buffered["stage"], "error": buffered.get("error"), "attempts": attempt_log}
|
|
@@ -4068,7 +4110,13 @@ def _tmux_inject_text(target: str, text: str, submit_key: str, buffer_name: str,
|
|
|
4068
4110
|
return {"ok": False, "stage": "paste-buffer", "error": proc.stderr.strip(), "attempts": attempt_log}
|
|
4069
4111
|
time.sleep(0.25)
|
|
4070
4112
|
if token:
|
|
4071
|
-
visible, verification, capture_text = _wait_for_message_ready(
|
|
4113
|
+
visible, verification, capture_text = _wait_for_message_ready(
|
|
4114
|
+
target,
|
|
4115
|
+
token,
|
|
4116
|
+
ready_timeout,
|
|
4117
|
+
expected_text=text,
|
|
4118
|
+
baseline_capture=baseline_capture,
|
|
4119
|
+
)
|
|
4072
4120
|
else:
|
|
4073
4121
|
visible, verification, capture_text = True, "no_token", ""
|
|
4074
4122
|
last_verification = verification
|
|
@@ -4237,35 +4285,51 @@ def _wait_for_visible_token(target: str, token: str, timeout: float) -> tuple[bo
|
|
|
4237
4285
|
deadline = time.monotonic() + max(timeout, 0.0)
|
|
4238
4286
|
last = "not_checked"
|
|
4239
4287
|
while True:
|
|
4240
|
-
capture =
|
|
4241
|
-
if capture
|
|
4242
|
-
if token in capture
|
|
4288
|
+
capture = _capture_tmux_pane_text(target)
|
|
4289
|
+
if capture["ok"]:
|
|
4290
|
+
if token in capture["capture"] or f"[team-agent-token:{token}]" in capture["capture"]:
|
|
4243
4291
|
return True, "capture_contains_token"
|
|
4244
4292
|
last = "capture_missing_token"
|
|
4245
4293
|
else:
|
|
4246
|
-
last = f"capture_failed: {capture.
|
|
4294
|
+
last = f"capture_failed: {capture.get('error')}"
|
|
4247
4295
|
if time.monotonic() >= deadline:
|
|
4248
4296
|
return False, last
|
|
4249
4297
|
time.sleep(0.1)
|
|
4250
4298
|
|
|
4251
4299
|
|
|
4252
|
-
def
|
|
4300
|
+
def _capture_tmux_pane_text(target: str) -> dict[str, Any]:
|
|
4301
|
+
capture = run_cmd(["tmux", "capture-pane", "-p", "-S", f"-{DELIVERY_CAPTURE_LINES}", "-t", target], timeout=5)
|
|
4302
|
+
if capture.returncode != 0:
|
|
4303
|
+
return {"ok": False, "capture": "", "error": capture.stderr.strip() or "tmux capture-pane failed"}
|
|
4304
|
+
return {"ok": True, "capture": capture.stdout}
|
|
4305
|
+
|
|
4306
|
+
|
|
4307
|
+
def _wait_for_message_ready(
|
|
4308
|
+
target: str,
|
|
4309
|
+
message_id: str,
|
|
4310
|
+
timeout: float,
|
|
4311
|
+
expected_text: str = "",
|
|
4312
|
+
allow_pasted_prompt: bool = True,
|
|
4313
|
+
baseline_capture: str = "",
|
|
4314
|
+
) -> tuple[bool, str, str]:
|
|
4253
4315
|
deadline = time.monotonic() + max(timeout, 0.0)
|
|
4254
4316
|
last = "not_checked"
|
|
4255
4317
|
last_capture = ""
|
|
4318
|
+
baseline_had_pasted_prompt = _capture_has_pasted_content_prompt(baseline_capture)
|
|
4256
4319
|
while True:
|
|
4257
|
-
capture =
|
|
4258
|
-
if capture
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4320
|
+
capture = _capture_tmux_pane_text(target)
|
|
4321
|
+
if capture["ok"]:
|
|
4322
|
+
capture_text = capture["capture"]
|
|
4323
|
+
last_capture = capture_text
|
|
4324
|
+
if message_id in capture_text or f"[team-agent-token:{message_id}]" in capture_text:
|
|
4325
|
+
return True, "capture_contains_token", capture_text
|
|
4326
|
+
if expected_text and _capture_contains_message_fragment(capture_text, expected_text):
|
|
4327
|
+
return True, "capture_contains_message_fragment", capture_text
|
|
4328
|
+
if allow_pasted_prompt and _capture_has_pasted_content_prompt(capture_text) and not baseline_had_pasted_prompt:
|
|
4329
|
+
return True, "capture_contains_new_pasted_content_prompt", capture_text
|
|
4266
4330
|
last = "capture_missing_token"
|
|
4267
4331
|
else:
|
|
4268
|
-
last = f"capture_failed: {capture.
|
|
4332
|
+
last = f"capture_failed: {capture.get('error')}"
|
|
4269
4333
|
if time.monotonic() >= deadline:
|
|
4270
4334
|
return False, last, last_capture
|
|
4271
4335
|
time.sleep(0.1)
|
|
@@ -4299,15 +4363,18 @@ def _capture_contains_message_fragment(capture_text: str, expected_text: str) ->
|
|
|
4299
4363
|
haystack = _compact_visible_text(capture_text)
|
|
4300
4364
|
if not haystack:
|
|
4301
4365
|
return False
|
|
4302
|
-
|
|
4366
|
+
fragments = _message_fragment_candidates(expected_text)
|
|
4367
|
+
if not fragments:
|
|
4368
|
+
return False
|
|
4369
|
+
return any(fragment in haystack for fragment in fragments)
|
|
4303
4370
|
|
|
4304
4371
|
|
|
4305
4372
|
def _message_fragment_candidates(text: str) -> list[str]:
|
|
4306
4373
|
sanitized = re.sub(r"\[team-agent-token:[^\]]+\]", "", text)
|
|
4307
4374
|
fragments: list[str] = []
|
|
4308
|
-
for line in sanitized
|
|
4375
|
+
for line in _message_content_lines(sanitized):
|
|
4309
4376
|
compact = _compact_visible_text(line)
|
|
4310
|
-
if
|
|
4377
|
+
if not _is_strong_message_fragment(compact):
|
|
4311
4378
|
continue
|
|
4312
4379
|
if len(compact) <= 72:
|
|
4313
4380
|
fragments.append(compact)
|
|
@@ -4330,6 +4397,35 @@ def _message_fragment_candidates(text: str) -> list[str]:
|
|
|
4330
4397
|
return unique
|
|
4331
4398
|
|
|
4332
4399
|
|
|
4400
|
+
def _message_content_lines(text: str) -> list[str]:
|
|
4401
|
+
lines = text.splitlines()
|
|
4402
|
+
if lines and lines[0].strip().startswith("Team Agent message from "):
|
|
4403
|
+
lines = lines[1:]
|
|
4404
|
+
return [line for line in lines if line.strip()]
|
|
4405
|
+
|
|
4406
|
+
|
|
4407
|
+
def _is_strong_message_fragment(compact: str) -> bool:
|
|
4408
|
+
if not compact:
|
|
4409
|
+
return False
|
|
4410
|
+
generic_prefixes = (
|
|
4411
|
+
"TeamAgentmessagefrom",
|
|
4412
|
+
"TeamAgentpeermessagefrom",
|
|
4413
|
+
"TeamAgentstoredthisresult",
|
|
4414
|
+
"TeamAgenthascollectedthisresult",
|
|
4415
|
+
"Nomanualpolling",
|
|
4416
|
+
)
|
|
4417
|
+
if compact.startswith(generic_prefixes):
|
|
4418
|
+
return False
|
|
4419
|
+
if re.fullmatch(r"[-::>›❯]+", compact):
|
|
4420
|
+
return False
|
|
4421
|
+
if re.search(r"(msg|res)_[0-9A-Fa-f]{8,}", compact):
|
|
4422
|
+
return True
|
|
4423
|
+
cjk_count = len(re.findall(r"[\u4e00-\u9fff]", compact))
|
|
4424
|
+
if cjk_count >= 4 and len(compact) >= 6:
|
|
4425
|
+
return True
|
|
4426
|
+
return len(compact) >= 18
|
|
4427
|
+
|
|
4428
|
+
|
|
4333
4429
|
def _compact_visible_text(text: str) -> str:
|
|
4334
4430
|
return re.sub(r"\s+", "", text)
|
|
4335
4431
|
|
|
@@ -4668,7 +4764,12 @@ def _deliver_pending_message(
|
|
|
4668
4764
|
if ready:
|
|
4669
4765
|
status = (
|
|
4670
4766
|
"submitted"
|
|
4671
|
-
if verification
|
|
4767
|
+
if verification
|
|
4768
|
+
in {
|
|
4769
|
+
"capture_contains_pasted_content_prompt",
|
|
4770
|
+
"capture_contains_new_pasted_content_prompt",
|
|
4771
|
+
"capture_contains_message_fragment",
|
|
4772
|
+
}
|
|
4672
4773
|
else "visible"
|
|
4673
4774
|
)
|
|
4674
4775
|
store.mark(message_id, status)
|
|
@@ -5200,6 +5301,22 @@ def _find_task(tasks: list[dict[str, Any]], task_id: str) -> dict[str, Any]:
|
|
|
5200
5301
|
raise RuntimeError(f"unknown task id: {task_id}")
|
|
5201
5302
|
|
|
5202
5303
|
|
|
5304
|
+
def _find_task_or_none(tasks: list[dict[str, Any]], task_id: str) -> dict[str, Any] | None:
|
|
5305
|
+
for task in tasks:
|
|
5306
|
+
if task.get("id") == task_id:
|
|
5307
|
+
return task
|
|
5308
|
+
return None
|
|
5309
|
+
|
|
5310
|
+
|
|
5311
|
+
def _is_message_scoped_result(store: MessageStore, envelope: dict[str, Any]) -> bool:
|
|
5312
|
+
task_id = str(envelope.get("task_id") or "")
|
|
5313
|
+
agent_id = str(envelope.get("agent_id") or "")
|
|
5314
|
+
if not task_id.startswith("msg_"):
|
|
5315
|
+
return False
|
|
5316
|
+
message = _message_by_id(store, task_id)
|
|
5317
|
+
return bool(message and message.get("recipient") == agent_id)
|
|
5318
|
+
|
|
5319
|
+
|
|
5203
5320
|
def _find_agent(spec: dict[str, Any], agent_id: str | None) -> dict[str, Any] | None:
|
|
5204
5321
|
if not agent_id:
|
|
5205
5322
|
return None
|