agents-relay 1.0.4 → 1.0.6

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.
@@ -0,0 +1,62 @@
1
+ ---
2
+ name: chatgpt-browser-worker
3
+ description: Submit an isolated one-shot ChatGPT browser worker task through browser-harness with a required durable output destination.
4
+ ---
5
+
6
+ # ChatGPT browser worker
7
+
8
+ Use this skill to hand one self-contained task to ChatGPT through the authenticated
9
+ browser session. The caller does not manage ChatGPT threads, model selection,
10
+ thinking level, polling, or result retrieval.
11
+
12
+ ## Output contract
13
+
14
+ Every task handed to this worker MUST declare exactly one durable output mode
15
+ before the worker is launched.
16
+
17
+ ### 1. Task / PR output
18
+
19
+ Use this for repository or managed-task work.
20
+
21
+ The task prompt must identify the exact managed task/PR that owns the result.
22
+ The worker performs the requested work directly against that task/PR and records
23
+ its final outcome there.
24
+
25
+ Output:
26
+ task/PR
27
+ <exact managed task/PR identity and required final action>
28
+
29
+ ### 2. File output
30
+
31
+ Use this for research, analysis, reports, or other artifact-producing work.
32
+
33
+ The task prompt must name the exact file path. That file is the authoritative
34
+ result.
35
+
36
+ Output:
37
+ file
38
+ <exact file path>
39
+
40
+ A Browser ChatGPT worker task without one of these output declarations is
41
+ invalid and must not be launched. The orchestrator chooses and writes the output
42
+ contract when it creates the worker task; the worker must not invent or change
43
+ the destination.
44
+
45
+ Progress and terminal success/failure are always reported through the normal
46
+ Neo event contract. Events are execution observability, not a third output mode.
47
+
48
+ ## Runtime behavior
49
+
50
+ The worker is one-shot. It opens an isolated worker-owned ChatGPT tab, submits
51
+ the complete task, verifies that ChatGPT accepted the submission, closes its
52
+ owned tab, and returns. ChatGPT continues the task independently and publishes
53
+ the result through the declared output contract.
54
+
55
+ The worker must never interact through a pre-existing user ChatGPT tab.
56
+ Browser details, transient conversation identity, submission verification,
57
+ model/default-thinking behavior, and tab cleanup are implementation concerns of
58
+ the worker and are not part of the caller-facing contract.
59
+
60
+ The runtime entry point is the browser-worker agent. Callers launch that agent
61
+ with the complete task prompt and declared output; they do not call helper
62
+ scripts directly.
@@ -0,0 +1,42 @@
1
+ ---
2
+ name: browser-worker
3
+ description: Submit one isolated ChatGPT browser task and return after the task is accepted.
4
+ type: worker
5
+ ---
6
+
7
+ # Browser Worker Agent
8
+
9
+ You are the runtime owner for the `chatgpt-browser-worker` skill. Load and follow
10
+ that skill and the `browser-harness` skill before execution.
11
+
12
+ The outer orchestrator gives you one complete task. That task MUST already
13
+ contain exactly one output declaration defined by the skill:
14
+
15
+ - `task/PR` with the exact managed task/PR identity and required final action; or
16
+ - `file` with the exact authoritative output path.
17
+
18
+ If the output declaration is missing or ambiguous, do not invent one.
19
+
20
+ ## Runtime rules
21
+
22
+ - Use `browser-harness` for every ChatGPT browser interaction.
23
+ - Execute only a one-shot submission. Do not expose or operate a
24
+ create/resume/status/result/continue lifecycle for callers.
25
+ - Always create a fresh worker-owned tab. Never type, upload, click New chat,
26
+ select a Project, or submit through a pre-existing user ChatGPT tab.
27
+ - Use Temporary Chat and the account's existing default model/thinking
28
+ settings. Do not change model or thinking settings.
29
+ - Upload requested files, submit the complete task prompt, and verify that the
30
+ submission became a new user turn.
31
+ - Once submission is verified, close the worker-owned tab and return. Do not
32
+ wait for the assistant response and do not reopen or poll the conversation.
33
+ - Treat any observed conversation/thread identity only as diagnostic evidence,
34
+ never as a resumable handle.
35
+ - Progress and terminal success/failure for the actual delegated task are
36
+ reported through the normal Neo event contract by the executing task. The
37
+ final durable result goes to the declared task/PR or file output.
38
+ - Stop on authentication, MFA, consent, or ambiguous browser state rather than
39
+ bypassing it.
40
+
41
+ Helper scripts under `scripts/` are implementation details. Callers must not
42
+ invoke them directly.
@@ -0,0 +1,171 @@
1
+ import atexit
2
+ import json
3
+ import os
4
+ import re
5
+ import time
6
+
7
+ from browser_harness import *
8
+
9
+
10
+ CFG = json.load(open("__CFG_PATH__", encoding="utf-8"))
11
+ _OWNED_TABS = []
12
+
13
+
14
+ def _new_owned_tab(url):
15
+ target_id = cdp("Target.createTarget", url="about:blank", background=True)["targetId"]
16
+ switch_tab(target_id)
17
+ _OWNED_TABS.append(target_id)
18
+ if url != "about:blank":
19
+ goto_url(url)
20
+ return target_id
21
+
22
+
23
+ def _close_owned_tabs():
24
+ while _OWNED_TABS:
25
+ try:
26
+ close_tab(_OWNED_TABS.pop())
27
+ except Exception:
28
+ pass
29
+
30
+
31
+ atexit.register(_close_owned_tabs)
32
+
33
+
34
+ def _composer():
35
+ selector = js("""(() => {
36
+ const preferred = document.querySelector('#prompt-textarea');
37
+ if (preferred) {
38
+ const r=preferred.getBoundingClientRect();
39
+ if (r.width>0 && r.height>0) return '#prompt-textarea';
40
+ }
41
+ const fallback = [...document.querySelectorAll('textarea,[contenteditable="true"]')]
42
+ .find(e => { const r=e.getBoundingClientRect(); return r.width>0 && r.height>0 && !e.disabled; });
43
+ if (!fallback) return null;
44
+ return fallback.tagName === 'TEXTAREA' ? 'textarea' : '[contenteditable="true"]';
45
+ })()""")
46
+ if not selector:
47
+ raise RuntimeError("Temporary Chat composer was not observed")
48
+ return selector
49
+
50
+
51
+ def _composer_text(selector):
52
+ return js(f"""(() => {{
53
+ const e=document.querySelector({json.dumps(selector)});
54
+ return e ? ((e.innerText ?? e.value) || '') : '';
55
+ }})()""") or ""
56
+
57
+
58
+ def _attachment_names():
59
+ return js(r"""(() => [...document.querySelectorAll('button[aria-label^="Remove file"]')]
60
+ .map(b => (b.getAttribute('aria-label') || '').replace(/^Remove file\s+\d+:\s*/, ''))
61
+ .filter(Boolean))()""") or []
62
+
63
+
64
+ def _upload_files(paths):
65
+ if not paths:
66
+ return []
67
+ selector = js("""(() => {
68
+ for (const s of ['#upload-files','#upload-media','input[name="upload-media"]','input[type="file"]'])
69
+ if (document.querySelector(s)) return s;
70
+ return null;
71
+ })()""")
72
+ if not selector:
73
+ raise RuntimeError("ChatGPT file input was not observed")
74
+ for path in paths:
75
+ if not os.path.isfile(path):
76
+ raise RuntimeError(f"attachment does not exist: {path}")
77
+ upload_file(selector, path)
78
+ expected = os.path.basename(path)
79
+ deadline = time.time() + 20
80
+ while time.time() < deadline and expected not in _attachment_names():
81
+ time.sleep(.25)
82
+ if expected not in _attachment_names():
83
+ raise RuntimeError(f"attachment was not observed ready: {expected}")
84
+ return [os.path.basename(path) for path in paths]
85
+
86
+
87
+ def _user_turns():
88
+ return js("""(() => [...document.querySelectorAll('[data-message-author-role="user"]')]
89
+ .map(e => ({text:e.innerText.trim(), id:e.getAttribute('data-message-id') || ''}))
90
+ .filter(e => e.text))()""") or []
91
+
92
+
93
+ def _click_send():
94
+ ok = js("""(() => {
95
+ const b=document.querySelector('button[data-testid="send-button"],button[aria-label="Send prompt"]');
96
+ if (!b || b.disabled || b.getAttribute('aria-disabled') === 'true') return false;
97
+ b.click(); return true;
98
+ })()""")
99
+ if not ok:
100
+ raise RuntimeError("Temporary Chat Send prompt button was not observed ready")
101
+
102
+
103
+ def _wait_user_turn(before_count, prompt, timeout=20):
104
+ deadline = time.time() + timeout
105
+ while time.time() < deadline:
106
+ turns = _user_turns()
107
+ if len(turns) > before_count and prompt.strip() in turns[-1]["text"]:
108
+ return turns[-1]
109
+ time.sleep(.25)
110
+ raise RuntimeError("Temporary Chat prompt did not become an observed user turn")
111
+
112
+
113
+ def _diagnostic_thread_id():
114
+ match = re.search(r"/c/([^/?#]+)", page_info().get("url", ""))
115
+ return match.group(1) if match else None
116
+
117
+
118
+ _new_owned_tab("https://chatgpt.com/")
119
+ wait_for_load()
120
+
121
+ enabled = js("""(() => {
122
+ const matches=[...document.querySelectorAll('button')].filter(
123
+ b => (b.getAttribute('aria-label') || '').trim() === 'Temporary chat'
124
+ );
125
+ if (matches.length !== 1) return false;
126
+ matches[0].click();
127
+ return true;
128
+ })()""")
129
+ if not enabled:
130
+ raise RuntimeError("Temporary Chat toggle was not uniquely observed")
131
+
132
+ deadline = time.time() + 20
133
+ while time.time() < deadline:
134
+ if "temporary-chat=true" in page_info().get("url", ""):
135
+ try:
136
+ _composer()
137
+ break
138
+ except RuntimeError:
139
+ pass
140
+ time.sleep(.25)
141
+ else:
142
+ raise RuntimeError("Temporary Chat mode did not become ready")
143
+
144
+ attachments = _upload_files(CFG.get("file", []))
145
+ selector = _composer()
146
+ before_count = len(_user_turns())
147
+ fill_input(selector, CFG["prompt"], clear_first=True)
148
+ if CFG["prompt"].strip() not in _composer_text(selector):
149
+ info = js(f"""(() => {{
150
+ const e=document.querySelector({json.dumps(selector)});
151
+ const r=e.getBoundingClientRect();
152
+ return {{x:r.x+r.width/2,y:r.y+r.height/2}};
153
+ }})()""")
154
+ click_at_xy(info["x"], info["y"])
155
+ press_key("CTRL+A")
156
+ type_text(CFG["prompt"])
157
+ if CFG["prompt"].strip() not in _composer_text(selector):
158
+ raise RuntimeError("Temporary Chat prompt was not observed in the composer")
159
+
160
+ _click_send()
161
+ user_turn = _wait_user_turn(before_count, CFG["prompt"])
162
+
163
+ print(json.dumps({
164
+ "operation": "submit",
165
+ "status": "submitted",
166
+ "temporary": True,
167
+ "attachments": attachments,
168
+ "diagnostic_thread_id": _diagnostic_thread_id(),
169
+ "user_message_id": user_turn.get("id") or None,
170
+ "verified": True,
171
+ }, ensure_ascii=False))
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env python3
2
+ """Submit one isolated Temporary Chat task through 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__)), "_temporary_bh.py")
13
+
14
+
15
+ def main() -> int:
16
+ parser = argparse.ArgumentParser()
17
+ parser.add_argument("--prompt", required=True)
18
+ parser.add_argument("--file", action="append", default=[])
19
+ args = parser.parse_args()
20
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as handle:
21
+ json.dump(vars(args), handle, ensure_ascii=False)
22
+ config_path = handle.name
23
+ try:
24
+ code = open(BH_SCRIPT, encoding="utf-8").read().replace("__CFG_PATH__", config_path)
25
+ return subprocess.run(["browser-harness"], input=code, text=True, timeout=180).returncode
26
+ finally:
27
+ os.unlink(config_path)
28
+
29
+
30
+ if __name__ == "__main__":
31
+ raise SystemExit(main())