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.
- package/README.md +14 -13
- package/dist/adapters.js +133 -234
- package/dist/cli.js +18 -7
- package/dist/planner.js +9 -4
- package/dist/reconciler.js +128 -98
- package/dist/relayd.js +2 -2
- package/dist/store.js +1 -1
- package/package.json +1 -1
- package/skills/agents-relay/SKILL.md +57 -45
- package/skills/agents-relay/agents/planner.agent.md +12 -9
- package/skills/chatgpt-browser-worker/SKILL.md +62 -0
- package/skills/chatgpt-browser-worker/agents/browser-worker.agent.md +42 -0
- package/skills/chatgpt-browser-worker/scripts/_temporary_bh.py +171 -0
- package/skills/chatgpt-browser-worker/scripts/temporary_bh.py +31 -0
|
@@ -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())
|