agents-relay 2.0.33 → 2.0.34
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/dist/adapters.js +119 -50
- package/dist/relayd.js +14 -5
- package/dist/store.js +1 -1
- package/package.json +1 -1
package/dist/adapters.js
CHANGED
|
@@ -120,6 +120,8 @@ export class CodexAdapter {
|
|
|
120
120
|
return execution;
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
|
+
export const CHATGPT_BROWSER_WORKSPACE = 'agents-replay';
|
|
124
|
+
export const CHATGPT_BROWSER_WORKSPACE_POOL_SIZE = 6;
|
|
123
125
|
export function chatGptTabClosePolicy(value, fallback = 'after-start') {
|
|
124
126
|
const policy = value || fallback;
|
|
125
127
|
if (!['after-start', 'never', 'after-terminal'].includes(policy))
|
|
@@ -163,62 +165,129 @@ function parseDriverJson(output) {
|
|
|
163
165
|
}
|
|
164
166
|
throw new Error('browser-worker driver returned no submitted receipt');
|
|
165
167
|
}
|
|
166
|
-
function
|
|
167
|
-
return (
|
|
168
|
-
const
|
|
169
|
-
const args = [join(root, 'scripts', 'temporary_bh.py'), '--prompt', request.prompt, '--release-file', releaseFile, '--close-policy', request.tabClosePolicy ?? 'after-start'];
|
|
170
|
-
for (const file of request.files ?? [])
|
|
171
|
-
args.push('--file', file);
|
|
172
|
-
const child = spawn('python3', args, { shell: false, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
168
|
+
function browserWorkspaceCommand(command, args, stdin) {
|
|
169
|
+
return new Promise((resolve, reject) => {
|
|
170
|
+
const child = spawn(command, [...args], { shell: false, stdio: [stdin === undefined ? 'ignore' : 'pipe', 'pipe', 'pipe'] });
|
|
173
171
|
let stdout = '';
|
|
174
172
|
let stderr = '';
|
|
175
|
-
|
|
173
|
+
child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
174
|
+
child.stderr?.on('data', (chunk) => { stderr = appendTail(stderr, chunk.toString()); });
|
|
175
|
+
child.on('error', reject);
|
|
176
|
+
child.on('close', code => {
|
|
177
|
+
if (code === 0)
|
|
178
|
+
resolve(stdout.trim());
|
|
179
|
+
else
|
|
180
|
+
reject(new Error(stderr.trim() || stdout.trim() || `browser-workspace exited ${code}`));
|
|
181
|
+
});
|
|
182
|
+
if (stdin !== undefined) {
|
|
183
|
+
child.stdin?.end(stdin);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function parseWorkspaceJson(output) {
|
|
188
|
+
const lines = output.trim().split(/\r?\n/).filter(Boolean);
|
|
189
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
190
|
+
try {
|
|
191
|
+
return JSON.parse(lines[index]);
|
|
192
|
+
}
|
|
193
|
+
catch { /* diagnostics may precede JSON */ }
|
|
194
|
+
}
|
|
195
|
+
throw new Error('browser-workspace returned no JSON result');
|
|
196
|
+
}
|
|
197
|
+
export async function ensureChatGptBrowserWorkspace(command = process.env.AGENTS_RELAY_BROWSER_WORKSPACE_COMMAND ?? 'browser-workspace') {
|
|
198
|
+
await browserWorkspaceCommand(command, ['create', CHATGPT_BROWSER_WORKSPACE, String(CHATGPT_BROWSER_WORKSPACE_POOL_SIZE)]);
|
|
199
|
+
}
|
|
200
|
+
function sessionBrowserWorkerRunner(root) {
|
|
201
|
+
const command = process.env.AGENTS_RELAY_BROWSER_WORKSPACE_COMMAND ?? 'browser-workspace';
|
|
202
|
+
return (_definition, request, _route, signal) => {
|
|
203
|
+
const policy = request.tabClosePolicy ?? 'after-start';
|
|
204
|
+
let activeSessionId;
|
|
205
|
+
let released = false;
|
|
206
|
+
let releaseResolve;
|
|
207
|
+
const releaseBarrier = new Promise(resolve => { releaseResolve = resolve; });
|
|
208
|
+
const release = () => {
|
|
209
|
+
if (released)
|
|
210
|
+
return;
|
|
211
|
+
released = true;
|
|
212
|
+
releaseResolve();
|
|
213
|
+
};
|
|
176
214
|
let submittedResolve;
|
|
177
215
|
let submittedReject;
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if (!submitted) {
|
|
182
|
-
try {
|
|
183
|
-
const response = parseDriverJson(stdout);
|
|
184
|
-
submitted = true;
|
|
185
|
-
submittedResolve?.(JSON.stringify(response));
|
|
186
|
-
}
|
|
187
|
-
catch { /* receipt may be split across chunks */ }
|
|
188
|
-
}
|
|
216
|
+
const submitted = new Promise((resolve, reject) => {
|
|
217
|
+
submittedResolve = resolve;
|
|
218
|
+
submittedReject = reject;
|
|
189
219
|
});
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
220
|
+
const promise = (async () => {
|
|
221
|
+
const configFile = join(process.env.TMPDIR ?? '/tmp', `agents-relay-chatgpt-${randomUUID()}.json`);
|
|
222
|
+
try {
|
|
223
|
+
const [readiness, submitSource, temporarySource] = await Promise.all([
|
|
224
|
+
readFile(join(root, 'scripts', '_submit_readiness.py'), 'utf8'),
|
|
225
|
+
readFile(join(root, 'scripts', '_submit_bh.py'), 'utf8'),
|
|
226
|
+
readFile(join(root, 'scripts', '_temporary_bh.py'), 'utf8'),
|
|
227
|
+
]);
|
|
228
|
+
await writeFile(configFile, JSON.stringify({
|
|
229
|
+
prompt: request.prompt,
|
|
230
|
+
file: request.files ?? [],
|
|
231
|
+
close_policy: policy,
|
|
232
|
+
release_file: '',
|
|
233
|
+
}));
|
|
234
|
+
const startOutput = await browserWorkspaceCommand(command, ['session', 'start', '--workspace', CHATGPT_BROWSER_WORKSPACE]);
|
|
235
|
+
const started = parseWorkspaceJson(startOutput);
|
|
236
|
+
if (typeof started.session_id !== 'string') {
|
|
237
|
+
throw new Error('browser-workspace session start returned no session_id');
|
|
201
238
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
239
|
+
activeSessionId = started.session_id;
|
|
240
|
+
if (signal.aborted)
|
|
241
|
+
throw new Error('browser-worker execution aborted');
|
|
242
|
+
const escapedConfig = configFile.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
|
|
243
|
+
const source = [
|
|
244
|
+
readiness,
|
|
245
|
+
submitSource.replace(/^from browser_harness import \*\s*$/m, ''),
|
|
246
|
+
temporarySource.replace(/^_wait_for_release\(\)\s*$/m, ''),
|
|
247
|
+
].join('\n\n').replaceAll('__CFG_PATH__', escapedConfig);
|
|
248
|
+
const execOutput = await browserWorkspaceCommand(command, ['session', 'exec', activeSessionId], source);
|
|
249
|
+
const wrapper = parseWorkspaceJson(execOutput);
|
|
250
|
+
if (wrapper.ok !== true) {
|
|
251
|
+
throw new Error(typeof wrapper.error === 'string' ? wrapper.error : 'browser-workspace session exec failed');
|
|
209
252
|
}
|
|
210
|
-
|
|
211
|
-
|
|
253
|
+
const receipt = parseDriverJson(typeof wrapper.stdout === 'string' ? wrapper.stdout : '');
|
|
254
|
+
const result = JSON.stringify(receipt);
|
|
255
|
+
submittedResolve(result);
|
|
256
|
+
if (policy === 'never') {
|
|
257
|
+
await unlink(configFile).catch(() => { });
|
|
258
|
+
activeSessionId = undefined;
|
|
259
|
+
return result;
|
|
212
260
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
261
|
+
await releaseBarrier;
|
|
262
|
+
if (activeSessionId) {
|
|
263
|
+
await browserWorkspaceCommand(command, ['session', 'stop', activeSessionId]);
|
|
264
|
+
activeSessionId = undefined;
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
catch (failure) {
|
|
269
|
+
if (activeSessionId) {
|
|
270
|
+
await browserWorkspaceCommand(command, ['session', 'stop', activeSessionId]).catch(() => { });
|
|
271
|
+
activeSessionId = undefined;
|
|
272
|
+
}
|
|
273
|
+
await unlink(configFile).catch(() => { });
|
|
274
|
+
const error = failure instanceof Error ? failure : new Error(String(failure));
|
|
275
|
+
submittedReject(error);
|
|
276
|
+
throw error;
|
|
277
|
+
}
|
|
278
|
+
})();
|
|
279
|
+
const cancel = () => {
|
|
280
|
+
release();
|
|
281
|
+
if (activeSessionId) {
|
|
282
|
+
void browserWorkspaceCommand(command, ['session', 'stop', activeSessionId]);
|
|
283
|
+
activeSessionId = undefined;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
if (signal.aborted)
|
|
287
|
+
cancel();
|
|
288
|
+
else
|
|
289
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
290
|
+
return { promise, submitted, release, cancel };
|
|
222
291
|
};
|
|
223
292
|
}
|
|
224
293
|
function parseBrowserWorkerResponse(output) {
|
|
@@ -243,7 +312,7 @@ function parseBrowserWorkerResponse(output) {
|
|
|
243
312
|
export class ChatGptAdapter {
|
|
244
313
|
name = 'chatgpt';
|
|
245
314
|
id = 'chatgpt/browser-worker';
|
|
246
|
-
capabilities = ['model', 'chatgpt', 'browser-
|
|
315
|
+
capabilities = ['model', 'chatgpt', 'browser-workspace'];
|
|
247
316
|
agentPath;
|
|
248
317
|
runner;
|
|
249
318
|
tabClosePolicy;
|
|
@@ -276,7 +345,7 @@ export class ChatGptAdapter {
|
|
|
276
345
|
if (controller.signal.aborted)
|
|
277
346
|
throw new Error('browser-worker execution aborted');
|
|
278
347
|
const policy = chatGptTabClosePolicy(task.chatgptTabClosePolicy, this.tabClosePolicy);
|
|
279
|
-
const run = (this.runner ??
|
|
348
|
+
const run = (this.runner ?? sessionBrowserWorkerRunner(runtime.root))(runtime.definition, { prompt: task.input, ...(task.attachments?.length ? { files: task.attachments } : {}), tabClosePolicy: policy }, route, controller.signal);
|
|
280
349
|
try {
|
|
281
350
|
const output = await (run.submitted ?? run.promise);
|
|
282
351
|
const response = parseBrowserWorkerResponse(output);
|
package/dist/relayd.js
CHANGED
|
@@ -3,7 +3,7 @@ import { once } from 'node:events';
|
|
|
3
3
|
import { readFile } from 'node:fs/promises';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { ChatGptAdapter, CodexAdapter } from './adapters.js';
|
|
6
|
+
import { ChatGptAdapter, CodexAdapter, ensureChatGptBrowserWorkspace } from './adapters.js';
|
|
7
7
|
import { CodexThreadContinuation, CommandContinuation, WebhookContinuation } from './continuation.js';
|
|
8
8
|
import { serveDashboard } from './dashboard.js';
|
|
9
9
|
import { RepositoryWorkerPool, aggregateManagedGitHubJobs } from './pool.js';
|
|
@@ -35,6 +35,15 @@ export async function overlayOperationalOverviews(overviews, storeFor) {
|
|
|
35
35
|
return { ...item, job };
|
|
36
36
|
}));
|
|
37
37
|
}
|
|
38
|
+
function managedOverviewKey(item) {
|
|
39
|
+
return item.job.repository + '#' + item.job.prNumber + ':' + item.job.id;
|
|
40
|
+
}
|
|
41
|
+
export function mergeOperationalInventory(current, discovered) {
|
|
42
|
+
const merged = new Map(current.map(item => [managedOverviewKey(item), item]));
|
|
43
|
+
for (const item of discovered)
|
|
44
|
+
merged.set(managedOverviewKey(item), item);
|
|
45
|
+
return [...merged.values()];
|
|
46
|
+
}
|
|
38
47
|
const PR_REFRESH_COALESCE_MS = 1000;
|
|
39
48
|
export const SCHEDULE_HEARTBEAT_MS = 600_000;
|
|
40
49
|
export function dueScheduledOverviews(overviews, now = new Date()) {
|
|
@@ -120,6 +129,7 @@ export async function runDaemon(argv) {
|
|
|
120
129
|
return;
|
|
121
130
|
}
|
|
122
131
|
configureExecutionRouterRulesPath(argv);
|
|
132
|
+
await ensureChatGptBrowserWorkspace();
|
|
123
133
|
const normalized = normalizeDaemonArguments(argv);
|
|
124
134
|
if (normalized.pullRequest && normalized.jobId) {
|
|
125
135
|
await startService([...argv]);
|
|
@@ -183,9 +193,8 @@ export async function runDaemon(argv) {
|
|
|
183
193
|
try {
|
|
184
194
|
githubReadPaths.workspaceScans += 1;
|
|
185
195
|
const refreshed = await withGitHubRequestContext(client, 'relayd.workspace-refresh', () => aggregateManagedGitHubJobs(client, scanRepositories, trusted));
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
cachedOverviews = await overlayOperationalOverviews(merged, (repository, prNumber) => runtimeStore(repository, prNumber));
|
|
196
|
+
const merged = mergeOperationalInventory(cachedOverviews, refreshed);
|
|
197
|
+
cachedOverviews = dashboardManagedJobs(await overlayOperationalOverviews(merged, (repository, prNumber) => runtimeStore(repository, prNumber)));
|
|
189
198
|
return cachedOverviews;
|
|
190
199
|
}
|
|
191
200
|
catch (error) {
|
|
@@ -382,7 +391,7 @@ export async function runDaemon(argv) {
|
|
|
382
391
|
const runtime = summarizeRuntime(cachedOverviews.map(item => item.job));
|
|
383
392
|
const workers = workerCandidates([
|
|
384
393
|
{ name: 'codex', id: 'codex', capabilities: ['model', 'codex'] },
|
|
385
|
-
{ name: 'chatgpt', id: 'chatgpt/browser-worker', capabilities: ['model', 'chatgpt', 'browser-
|
|
394
|
+
{ name: 'chatgpt', id: 'chatgpt/browser-worker', capabilities: ['model', 'chatgpt', 'browser-workspace'] },
|
|
386
395
|
], [], usage);
|
|
387
396
|
const result = await routeExecution(workers, task.workerRequirements ?? { capabilities: task.capabilities ?? [] }, {
|
|
388
397
|
decisionEngine: new LayaCliDecisionEngine(),
|
package/dist/store.js
CHANGED
|
@@ -70,7 +70,7 @@ export async function loadManagedGitHubJobGraphql(client, repository, prNumber,
|
|
|
70
70
|
return (await loadManagedGitHubPullRequestGraphql(client, repository, prNumber, trustedAuthors)).find(item => item.job.id === jobId) ?? null;
|
|
71
71
|
}
|
|
72
72
|
const COMPLETED_JOB_HISTORY_LIMIT = 10;
|
|
73
|
-
function isCompletedManagedJob(item) { return ['COMPLETED', 'CANCELLED'].includes(item.job.state)
|
|
73
|
+
function isCompletedManagedJob(item) { return ['COMPLETED', 'CANCELLED'].includes(item.job.state); }
|
|
74
74
|
function completedAt(item) {
|
|
75
75
|
return [item.job.updatedAt, item.mergedAt, item.closedAt].filter((value) => typeof value === 'string' && value.length > 0).sort().at(-1) ?? '';
|
|
76
76
|
}
|