agents-relay 1.0.5 → 1.0.7

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.
@@ -1,84 +0,0 @@
1
- """Create a ChatGPT thread through an injected browser-harness port."""
2
- from __future__ import annotations
3
-
4
- from datetime import datetime, timezone
5
- from typing import Any, Callable, Protocol
6
-
7
- try:
8
- from .contract import ContractError, project_matches, validate_request
9
- except ImportError: # Loaded directly by the lightweight skill test harness.
10
- from contract import ContractError, project_matches, validate_request
11
-
12
-
13
- class BrowserPort(Protocol):
14
- def select_project(self, project: dict[str, str]) -> dict[str, Any]: ...
15
- def set_thinking_level(self, level: str) -> dict[str, Any]: ...
16
- def send_prompt(self, prompt: str) -> dict[str, Any]: ...
17
-
18
-
19
- def _now() -> str:
20
- return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
21
-
22
-
23
- def _effective_level(observation: Any) -> str:
24
- if not isinstance(observation, dict):
25
- return "unknown"
26
- value = observation.get("effective_thinking_level", observation.get("level"))
27
- return value if value in {"default", "low", "medium", "high"} else "unknown"
28
-
29
-
30
- def create_thread(
31
- port: BrowserPort,
32
- request: Any,
33
- *,
34
- persist: Callable[[dict[str, Any]], None] | None = None,
35
- now: Callable[[], str] = _now,
36
- ) -> dict[str, Any]:
37
- """Select the requested Project, send the first prompt, and return state.
38
-
39
- The port returns observations from the browser. It is deliberately not
40
- responsible for durable state or contract validation.
41
- """
42
- request = validate_request(request)
43
- if request["operation"] != "create":
44
- raise ContractError("create_thread requires operation=create")
45
-
46
- selected = port.select_project(request["project"])
47
- if not project_matches(request["project"], selected):
48
- raise ContractError("browser observed a different Project after selection")
49
-
50
- thinking_observation: dict[str, Any] = {"requested": "default", "changed": False}
51
- effective = "unknown"
52
- if request["thinking_level"] != "default":
53
- raw = port.set_thinking_level(request["thinking_level"])
54
- thinking_observation = dict(raw) if isinstance(raw, dict) else {"observed": raw}
55
- thinking_observation["requested"] = request["thinking_level"]
56
- thinking_observation["changed"] = True
57
- effective = _effective_level(raw)
58
-
59
- sent = port.send_prompt(request["prompt"])
60
- if not isinstance(sent, dict) or not isinstance(sent.get("thread_id"), str) or not sent["thread_id"].strip():
61
- raise ContractError("browser did not return an observed thread_id")
62
-
63
- timestamp = now()
64
- state: dict[str, Any] = {
65
- "schema_version": 1,
66
- "thread_id": sent["thread_id"].strip(),
67
- "project": dict(selected),
68
- "status": "running",
69
- "requested_thinking_level": request["thinking_level"],
70
- "effective_thinking_level": effective,
71
- "created_at": timestamp,
72
- "updated_at": timestamp,
73
- "last_error": None,
74
- "evidence": {
75
- "project": dict(selected),
76
- "thinking": thinking_observation,
77
- "thread": {key: value for key, value in sent.items() if key != "text"},
78
- },
79
- }
80
- if "conversation_url" in sent:
81
- state["conversation_url"] = sent["conversation_url"]
82
- if persist is not None:
83
- persist(state)
84
- return state
@@ -1,36 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Run one existing-thread operation through the authenticated browser-harness."""
3
- from __future__ import annotations
4
-
5
- import argparse
6
- import json
7
- import os
8
- import subprocess
9
- import tempfile
10
-
11
-
12
- BH_SCRIPT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_operate_bh.py")
13
-
14
-
15
- def main() -> int:
16
- parser = argparse.ArgumentParser()
17
- parser.add_argument("operation", choices=("resume", "continue", "status", "result", "delete"))
18
- parser.add_argument("--thread-id", required=True)
19
- parser.add_argument("--project", required=True)
20
- parser.add_argument("--prompt")
21
- args = parser.parse_args()
22
- if args.operation == "continue" and not args.prompt:
23
- parser.error("continue requires --prompt")
24
- config = {key: value for key, value in vars(args).items() if value is not None}
25
- with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as handle:
26
- json.dump(config, handle, ensure_ascii=False)
27
- config_path = handle.name
28
- try:
29
- code = open(BH_SCRIPT, encoding="utf-8").read().replace("__CFG_PATH__", config_path)
30
- return subprocess.run(["browser-harness"], input=code, text=True, timeout=180).returncode
31
- finally:
32
- os.unlink(config_path)
33
-
34
-
35
- if __name__ == "__main__":
36
- raise SystemExit(main())
@@ -1,179 +0,0 @@
1
- """Existing-thread operations for an injected browser-harness port."""
2
- from __future__ import annotations
3
-
4
- from datetime import datetime, timezone
5
- from typing import Any, Callable, Protocol
6
-
7
- try:
8
- from .contract import ContractError, transition, validate_cleanup_observation, validate_project, validate_request, validate_result, validate_state
9
- except ImportError:
10
- from contract import ContractError, transition, validate_cleanup_observation, validate_project, validate_request, validate_result, validate_state
11
-
12
-
13
- class BrowserPort(Protocol):
14
- def open_thread(self, thread_id: str) -> dict[str, Any]: ...
15
- def send_prompt(self, prompt: str) -> dict[str, Any]: ...
16
- def read_status(self) -> dict[str, Any]: ...
17
- def read_result(self) -> dict[str, Any]: ...
18
- def delete_thread(self) -> dict[str, Any]: ...
19
-
20
-
21
- def _now() -> str:
22
- return datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
23
-
24
-
25
- def _project(observation: Any) -> dict[str, Any]:
26
- if not isinstance(observation, dict):
27
- raise ContractError("browser did not return an observed thread")
28
- project = observation.get("project", observation.get("selected_project"))
29
- if project is None:
30
- raise ContractError("browser did not expose the thread Project")
31
- return project
32
-
33
-
34
- def _status(observation: Any, default: str = "awaiting_result") -> str:
35
- value = observation.get("status") if isinstance(observation, dict) else None
36
- return value if value in {"created", "running", "awaiting_result", "completed", "failed", "blocked"} else default
37
-
38
-
39
- def _same_project(left: Any, right: Any) -> bool:
40
- """Compare names always; compare IDs only when both UI boundaries expose them."""
41
- expected = validate_project(left)
42
- observed = validate_project(right)
43
- return expected["name"] == observed["name"] and (
44
- "id" not in expected or "id" not in observed or expected["id"] == observed["id"]
45
- )
46
-
47
-
48
- def _save(state: dict[str, Any], persist: Callable[[dict[str, Any]], None] | None, now: Callable[[], str]) -> dict[str, Any]:
49
- state["updated_at"] = now()
50
- if persist is not None:
51
- persist(state)
52
- return state
53
-
54
-
55
- def _opened(port: BrowserPort, state: dict[str, Any], *, expected_project: dict[str, Any]) -> dict[str, Any]:
56
- opened = port.open_thread(state["thread_id"])
57
- if not isinstance(opened, dict):
58
- raise ContractError("browser did not return an observed thread")
59
- observed_id = opened.get("thread_id", state["thread_id"])
60
- if observed_id != state["thread_id"]:
61
- raise ContractError("browser opened a different thread")
62
- observed_project = _project(opened)
63
- if not _same_project(expected_project, observed_project):
64
- raise ContractError("observed Project does not match durable Project binding")
65
- return opened
66
-
67
-
68
- def resume_thread(
69
- port: BrowserPort,
70
- request: Any,
71
- *,
72
- state: Any | None = None,
73
- persist: Callable[[dict[str, Any]], None] | None = None,
74
- now: Callable[[], str] = _now,
75
- ) -> dict[str, Any]:
76
- """Open and verify an existing thread without sending a new prompt."""
77
- request = validate_request(request)
78
- if request["operation"] != "resume":
79
- raise ContractError("resume_thread requires operation=resume")
80
- current = validate_state(state) if state is not None else {
81
- "schema_version": 1,
82
- "thread_id": request["thread_id"],
83
- "project": request["project"],
84
- "status": "created",
85
- "requested_thinking_level": request.get("thinking_level", "default"),
86
- "effective_thinking_level": "unknown",
87
- }
88
- if current["thread_id"] != request["thread_id"]:
89
- raise ContractError("resume request does not match durable thread identity")
90
- if current["status"] == "deleted":
91
- raise ContractError("deleted thread is terminal")
92
- if not _same_project(current["project"], request["project"]):
93
- raise ContractError("resume Project does not match durable Project binding")
94
- opened = _opened(port, current, expected_project=current["project"])
95
- observed_status = _status(opened)
96
- if current["status"] == "created":
97
- current = transition(current, "running")
98
- if observed_status != current["status"]:
99
- current = transition(current, observed_status)
100
- current["project"] = dict(_project(opened))
101
- current.setdefault("evidence", {})["open"] = opened
102
- return _save(current, persist, now)
103
-
104
-
105
- def continue_thread(
106
- port: BrowserPort,
107
- state: Any,
108
- prompt: str,
109
- *,
110
- persist: Callable[[dict[str, Any]], None] | None = None,
111
- now: Callable[[], str] = _now,
112
- ) -> dict[str, Any]:
113
- """Open a durable thread, send one follow-up prompt, and persist running state."""
114
- current = validate_state(state)
115
- if not isinstance(prompt, str) or not prompt.strip():
116
- raise ContractError("prompt must be a non-empty string")
117
- _opened(port, current, expected_project=current["project"])
118
- if current["status"] != "running":
119
- current = transition(current, "running")
120
- sent = port.send_prompt(prompt.strip())
121
- if not isinstance(sent, dict):
122
- raise ContractError("browser did not acknowledge the follow-up prompt")
123
- current.setdefault("evidence", {})["prompt"] = sent
124
- return _save(current, persist, now)
125
-
126
-
127
- def inspect_thread(
128
- port: BrowserPort,
129
- state: Any,
130
- *,
131
- persist: Callable[[dict[str, Any]], None] | None = None,
132
- now: Callable[[], str] = _now,
133
- ) -> dict[str, Any]:
134
- """Read the observable UI status/progress and persist its state transition."""
135
- current = validate_state(state)
136
- if current["status"] == "deleted":
137
- raise ContractError("deleted thread is terminal")
138
- observation = port.read_status()
139
- observed_status = _status(observation, default="awaiting_result")
140
- if observed_status != current["status"]:
141
- current = transition(current, observed_status)
142
- current.setdefault("evidence", {})["status"] = observation
143
- return _save(current, persist, now)
144
-
145
-
146
- def result_thread(
147
- port: BrowserPort,
148
- state: Any,
149
- *,
150
- persist: Callable[[dict[str, Any]], None] | None = None,
151
- now: Callable[[], str] = _now,
152
- ) -> dict[str, Any]:
153
- """Return only an observed, normalized assistant result and persist completion."""
154
- current = validate_state(state)
155
- if current["status"] == "deleted":
156
- raise ContractError("deleted thread is terminal")
157
- result = validate_result(port.read_result(), thread_id=current["thread_id"])
158
- if current["status"] != "completed":
159
- current = transition(current, "completed")
160
- current.setdefault("evidence", {})["result"] = result
161
- _save(current, persist, now)
162
- return result
163
-
164
-
165
- def delete_thread(
166
- port: BrowserPort,
167
- state: Any,
168
- *,
169
- persist: Callable[[dict[str, Any]], None] | None = None,
170
- now: Callable[[], str] = _now,
171
- ) -> dict[str, Any]:
172
- """Delete one durable thread and persist its terminal tombstone."""
173
- current = validate_state(state)
174
- if current["status"] == "deleted":
175
- return current
176
- observation = validate_cleanup_observation(port.delete_thread(), thread_id=current["thread_id"])
177
- current = transition(current, "deleted")
178
- current.setdefault("evidence", {})["cleanup"] = observation
179
- return _save(current, persist, now)
@@ -1,21 +0,0 @@
1
- {
2
- "job_id": "neo-chatgpt-browser-worker-20260919",
3
- "task_id": "chatgpt-thread-1",
4
- "project": {"id": "project-1", "name": "neo"},
5
- "create": {
6
- "operation": "create",
7
- "project": {"name": "neo"},
8
- "prompt": "Run the assigned task.",
9
- "thinking_level": "high"
10
- },
11
- "thread": {
12
- "thread_id": "thread-1",
13
- "conversation_url": "https://chatgpt.com/c/thread-1"
14
- },
15
- "result": {
16
- "thread_id": "thread-1",
17
- "status": "completed",
18
- "text": "The assigned task is complete.",
19
- "message_id": "message-1"
20
- }
21
- }
@@ -1,27 +0,0 @@
1
- from pathlib import Path
2
-
3
-
4
- AGENT = Path(__file__).parents[1] / "agents" / "browser-worker.agent.md"
5
-
6
-
7
- def test_browser_worker_agent_defines_typed_lifecycle_and_verified_result():
8
- text = AGENT.read_text()
9
-
10
- for operation in ("create", "resume", "continue", "status", "result", "delete"):
11
- assert f"`{operation}`" in text
12
- for field in ("thread_id", "project", "message_id", "verified", "observed_at"):
13
- assert f'"{field}"' in text
14
- assert "browser-harness" in text
15
- assert "authentication_required" in text
16
- assert "Do not report success" in text
17
-
18
-
19
- def test_callers_are_directed_to_launch_agent_not_helper_scripts():
20
- skill = (AGENT.parents[1] / "SKILL.md").read_text()
21
- orchestration = (AGENT.parents[1] / "references" / "orchestration.md").read_text()
22
-
23
- assert "Callers launch that" in skill
24
- assert "agent with a typed lifecycle intent" in skill
25
- assert "must not directly call" in skill
26
- assert "launches the `browser-worker` agent" in orchestration
27
- assert "must not directly invoke" in orchestration
@@ -1,315 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import importlib.util
4
- import json
5
- import sys
6
- from pathlib import Path
7
-
8
- import pytest
9
-
10
-
11
- HERE = Path(__file__).resolve().parents[1]
12
- spec = importlib.util.spec_from_file_location("chatgpt_browser_worker_contract", HERE / "scripts/contract.py")
13
- assert spec and spec.loader
14
- contract = importlib.util.module_from_spec(spec)
15
- spec.loader.exec_module(contract)
16
- sys.modules["contract"] = contract
17
-
18
- create_spec = importlib.util.spec_from_file_location("chatgpt_browser_worker_create", HERE / "scripts/create.py")
19
- assert create_spec and create_spec.loader
20
- create = importlib.util.module_from_spec(create_spec)
21
- create_spec.loader.exec_module(create)
22
-
23
- operations_spec = importlib.util.spec_from_file_location("chatgpt_browser_worker_operations", HERE / "scripts/operations.py")
24
- assert operations_spec and operations_spec.loader
25
- operations = importlib.util.module_from_spec(operations_spec)
26
- operations_spec.loader.exec_module(operations)
27
-
28
-
29
- def state(status="created", project=None):
30
- return {
31
- "schema_version": 1,
32
- "thread_id": "thread-1",
33
- "project": project or {"id": "project-1", "name": "neo"},
34
- "status": status,
35
- "requested_thinking_level": "high",
36
- "effective_thinking_level": "unknown",
37
- }
38
-
39
-
40
- def test_create_and_resume_require_verified_project_and_preserve_level_request():
41
- created = contract.validate_request(
42
- {"operation": "create", "project": {"name": "neo"}, "prompt": "hello", "thinking_level": "high"}
43
- )
44
- resumed = contract.validate_request(
45
- {"operation": "resume", "thread_id": "thread-1", "project": {"id": "project-1", "name": "neo"}}
46
- )
47
- assert created["project"] == {"name": "neo"}
48
- assert created["thinking_level"] == "high"
49
- assert resumed["thread_id"] == "thread-1"
50
- with pytest.raises(contract.ContractError):
51
- contract.validate_request({"operation": "create", "prompt": "hello", "thinking_level": "high"})
52
-
53
-
54
- def test_project_mismatch_is_rejected_and_unknown_effective_level_is_valid():
55
- current = contract.validate_state(state())
56
- assert current["effective_thinking_level"] == "unknown"
57
- with pytest.raises(contract.ContractError, match="Project"):
58
- contract.transition(current, "running", observed_project={"id": "other", "name": "other"})
59
-
60
-
61
- def test_project_id_is_optional_at_a_browser_boundary():
62
- assert contract.project_matches({"id": "project-1", "name": "neo"}, {"name": "neo"})
63
- assert not contract.project_matches({"id": "project-1", "name": "neo"}, {"id": "other", "name": "neo"})
64
-
65
-
66
- def test_completion_requires_explicit_transition_and_delete_is_terminal():
67
- running = contract.transition(state(), "running")
68
- waiting = contract.transition(running, "awaiting_result")
69
- completed = contract.transition(waiting, "completed")
70
- assert completed["status"] == "completed"
71
- deleted = contract.transition(completed, "deleted")
72
- assert deleted["status"] == "deleted"
73
- with pytest.raises(contract.ContractError, match="terminal"):
74
- contract.transition(deleted, "running")
75
-
76
-
77
- def test_result_requires_observed_assistant_message_and_matching_identity():
78
- result = contract.validate_result(
79
- {
80
- "thread_id": "thread-1",
81
- "status": "completed",
82
- "text": "normalized answer",
83
- "message_id": "message-1",
84
- },
85
- thread_id="thread-1",
86
- )
87
- assert result["message_id"] == "message-1"
88
- with pytest.raises(contract.ContractError):
89
- contract.validate_result(
90
- {"thread_id": "thread-1", "status": "completed", "text": "answer"},
91
- thread_id="thread-1",
92
- )
93
- with pytest.raises(contract.ContractError, match="different thread"):
94
- contract.validate_result(
95
- {
96
- "thread_id": "other",
97
- "status": "completed",
98
- "text": "answer",
99
- "message_id": "message-1",
100
- },
101
- thread_id="thread-1",
102
- )
103
-
104
-
105
- def test_contract_module_has_no_browser_runtime_dependency():
106
- source = (HERE / "scripts/contract.py").read_text()
107
- assert "import browser_harness" not in source
108
- assert "import macbridge" not in source.lower()
109
-
110
-
111
- class FakePort:
112
- def __init__(self, project=None, sent=None):
113
- self.project = project or {"id": "project-1", "name": "neo"}
114
- self.sent = sent or {"thread_id": "thread-42", "conversation_url": "https://chatgpt.com/c/thread-42", "prompt_sent": True}
115
- self.calls = []
116
-
117
- def select_project(self, project):
118
- self.calls.append(("select_project", project))
119
- return self.project
120
-
121
- def set_thinking_level(self, level):
122
- self.calls.append(("set_thinking_level", level))
123
- return {"effective_thinking_level": level, "label": level.title()}
124
-
125
- def send_prompt(self, prompt):
126
- self.calls.append(("send_prompt", prompt))
127
- return self.sent
128
-
129
-
130
- class ExistingThreadPort:
131
- def __init__(self, project=None, status="awaiting_result", result=None):
132
- self.project = project or {"id": "project-1", "name": "neo"}
133
- self.status = status
134
- self.result = result or {
135
- "thread_id": "thread-1",
136
- "status": "completed",
137
- "text": "done",
138
- "message_id": "message-1",
139
- }
140
- self.calls = []
141
-
142
- def open_thread(self, thread_id):
143
- self.calls.append(("open_thread", thread_id))
144
- return {"thread_id": thread_id, "project": self.project, "status": self.status}
145
-
146
- def send_prompt(self, prompt):
147
- self.calls.append(("send_prompt", prompt))
148
- return {"prompt_sent": True}
149
-
150
- def read_status(self):
151
- self.calls.append(("read_status",))
152
- return {"status": self.status, "progress": "visible"}
153
-
154
- def read_result(self):
155
- self.calls.append(("read_result",))
156
- return self.result
157
-
158
- def delete_thread(self):
159
- self.calls.append(("delete_thread",))
160
- return {"thread_id": "thread-1", "outcome": "deleted", "verified": True}
161
-
162
-
163
- def test_create_returns_durable_identity_project_binding_and_evidence():
164
- port = FakePort()
165
- persisted = []
166
- result = create.create_thread(
167
- port,
168
- {"operation": "create", "project": {"name": "neo"}, "prompt": "hello", "thinking_level": "high"},
169
- persist=persisted.append,
170
- now=lambda: "2026-09-19T12:00:00Z",
171
- )
172
- assert result["thread_id"] == "thread-42"
173
- assert result["project"] == port.project
174
- assert result["effective_thinking_level"] == "high"
175
- assert result["evidence"]["thread"]["prompt_sent"] is True
176
- assert persisted == [result]
177
- assert [call[0] for call in port.calls] == ["select_project", "set_thinking_level", "send_prompt"]
178
-
179
-
180
- def test_create_default_leaves_platform_thinking_unchanged():
181
- port = FakePort()
182
- result = create.create_thread(
183
- port,
184
- {"operation": "create", "project": {"name": "neo"}, "prompt": "hello", "thinking_level": "default"},
185
- )
186
- assert result["requested_thinking_level"] == "default"
187
- assert result["effective_thinking_level"] == "unknown"
188
- assert [call[0] for call in port.calls] == ["select_project", "send_prompt"]
189
-
190
-
191
- def test_create_rejects_unverified_project_and_missing_thread_identity():
192
- with pytest.raises(contract.ContractError, match="different Project"):
193
- create.create_thread(
194
- FakePort(project={"name": "other"}),
195
- {"operation": "create", "project": {"name": "neo"}, "prompt": "hello", "thinking_level": "default"},
196
- )
197
- with pytest.raises(contract.ContractError, match="thread_id"):
198
- create.create_thread(
199
- FakePort(sent={"prompt_sent": True}),
200
- {"operation": "create", "project": {"name": "neo"}, "prompt": "hello", "thinking_level": "default"},
201
- )
202
-
203
-
204
- def test_resume_opens_durable_thread_and_requires_observed_project():
205
- port = ExistingThreadPort()
206
- persisted = []
207
- result = operations.resume_thread(
208
- port,
209
- {"operation": "resume", "thread_id": "thread-1", "project": {"name": "neo"}},
210
- state=state("running"),
211
- persist=persisted.append,
212
- now=lambda: "2026-09-19T12:03:00Z",
213
- )
214
- assert result["status"] == "awaiting_result"
215
- assert result["updated_at"] == "2026-09-19T12:03:00Z"
216
- assert persisted == [result]
217
- with pytest.raises(contract.ContractError, match="Project"):
218
- operations.resume_thread(
219
- ExistingThreadPort(project={"id": "other", "name": "other"}),
220
- {"operation": "resume", "thread_id": "thread-1", "project": {"name": "neo"}},
221
- state=state("running"),
222
- )
223
-
224
-
225
- def test_continue_status_and_result_persist_observed_transitions():
226
- port = ExistingThreadPort(status="running")
227
- persisted = []
228
- running = operations.continue_thread(port, state("completed"), "continue", persist=persisted.append)
229
- assert running["status"] == "running"
230
- observed = operations.inspect_thread(port, running, persist=persisted.append)
231
- assert observed["status"] == "running"
232
- port.status = "completed"
233
- result = operations.result_thread(port, observed, persist=persisted.append)
234
- assert result["text"] == "done"
235
- assert [call[0] for call in port.calls] == ["open_thread", "send_prompt", "read_status", "read_result"]
236
- assert [item["status"] for item in persisted] == ["running", "running", "completed"]
237
-
238
-
239
- def test_result_without_observed_assistant_message_does_not_complete_thread():
240
- port = ExistingThreadPort(result={"thread_id": "thread-1", "status": "awaiting_result"})
241
- with pytest.raises(contract.ContractError, match="completed"):
242
- operations.result_thread(port, state("running"))
243
-
244
-
245
- def test_delete_persists_verified_tombstone_and_is_idempotent():
246
- port = ExistingThreadPort()
247
- persisted = []
248
- deleted = operations.delete_thread(
249
- port, state("completed"), persist=persisted.append,
250
- now=lambda: "2026-09-19T12:04:00Z",
251
- )
252
- assert deleted["status"] == "deleted"
253
- assert deleted["evidence"]["cleanup"]["verified"] is True
254
- assert deleted["updated_at"] == "2026-09-19T12:04:00Z"
255
- assert [call[0] for call in port.calls] == ["delete_thread"]
256
-
257
- again = operations.delete_thread(port, deleted, persist=persisted.append)
258
- assert again["status"] == "deleted"
259
- assert [call[0] for call in port.calls] == ["delete_thread"]
260
-
261
-
262
- def test_delete_accepts_verified_not_found_for_retry_safety():
263
- class MissingPort(ExistingThreadPort):
264
- def delete_thread(self):
265
- return {"thread_id": "thread-1", "outcome": "not_found", "verified": True}
266
-
267
- deleted = operations.delete_thread(MissingPort(), state("failed"))
268
- assert deleted["status"] == "deleted"
269
-
270
-
271
- def test_cleanup_rejects_unverified_or_unrelated_observations():
272
- with pytest.raises(contract.ContractError, match="verified"):
273
- contract.validate_cleanup_observation(
274
- {"thread_id": "thread-1", "outcome": "deleted", "verified": False},
275
- thread_id="thread-1",
276
- )
277
- with pytest.raises(contract.ContractError, match="different thread"):
278
- contract.validate_cleanup_observation(
279
- {"thread_id": "other", "outcome": "deleted", "verified": True},
280
- thread_id="thread-1",
281
- )
282
-
283
-
284
- def test_agents_relay_fixture_drives_one_serial_browser_thread_lifecycle():
285
- fixture = json.loads((HERE / "tests" / "fixtures" / "relay_lifecycle.json").read_text())
286
- port = ExistingThreadPort(status="completed", result=fixture["result"])
287
- port.project = fixture["project"]
288
- persisted = []
289
-
290
- created = create.create_thread(
291
- FakePort(project=fixture["project"], sent=fixture["thread"]),
292
- fixture["create"],
293
- persist=persisted.append,
294
- now=lambda: "2026-09-19T12:00:00Z",
295
- )
296
- resumed = operations.resume_thread(
297
- port,
298
- {"operation": "resume", "thread_id": created["thread_id"], "project": fixture["project"]},
299
- state=created,
300
- persist=persisted.append,
301
- now=lambda: "2026-09-19T12:01:00Z",
302
- )
303
- result = operations.result_thread(
304
- port, resumed, persist=persisted.append, now=lambda: "2026-09-19T12:02:00Z"
305
- )
306
- deleted = operations.delete_thread(
307
- port, resumed, persist=persisted.append, now=lambda: "2026-09-19T12:03:00Z"
308
- )
309
-
310
- assert fixture["job_id"] and fixture["task_id"]
311
- assert created["thread_id"] == fixture["thread"]["thread_id"]
312
- assert result["text"] == fixture["result"]["text"]
313
- assert deleted["status"] == "deleted"
314
- assert [call[0] for call in port.calls] == ["open_thread", "read_result", "delete_thread"]
315
- assert [item["status"] for item in persisted] == ["running", "completed", "completed", "deleted"]