@mearl/client 2.20.6 → 2.21.0
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 +122 -2
- package/dist/cli.js +10 -5
- package/dist/commands.d.ts +14 -5
- package/dist/commands.js +21 -2
- package/dist/generated-browser-action-protocol.d.ts +11 -1
- package/dist/generated-browser-action-protocol.js +3 -0
- package/dist/generated-plugin-diagnostics.d.ts +110 -0
- package/dist/generated-plugin-diagnostics.js +1 -0
- package/dist/generated-plugin-execution.d.ts +27 -0
- package/dist/generated-plugin-execution.js +1 -0
- package/dist/generated-plugin-models.d.ts +64 -0
- package/dist/generated-plugin-models.js +1 -0
- package/dist/generated-plugin-pages.d.ts +76 -0
- package/dist/generated-plugin-pages.js +59 -0
- package/dist/generated-plugin-recovery.d.ts +32 -0
- package/dist/generated-plugin-recovery.js +1 -0
- package/dist/generated-plugin-state.d.ts +50 -0
- package/dist/generated-plugin-state.js +1 -0
- package/dist/generated-plugin-tasks.d.ts +74 -0
- package/dist/generated-plugin-tasks.js +7 -0
- package/dist/generated-toolbox-icon.d.ts +10 -0
- package/dist/generated-toolbox-icon.js +37 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +6 -0
- package/dist/plugin-task-host.d.ts +1 -0
- package/dist/plugin-task-host.js +14 -0
- package/dist/plugin-worker.d.ts +1 -0
- package/dist/plugin-worker.js +17 -0
- package/dist/plugin.d.ts +57 -4
- package/dist/plugin.js +296 -15
- package/dist/pluginCliOutput.d.ts +2 -0
- package/dist/pluginCliOutput.js +7 -0
- package/dist/pluginCommandWorker.d.ts +2 -0
- package/dist/pluginCommandWorker.js +174 -0
- package/dist/pluginDiagnostics.d.ts +26 -0
- package/dist/pluginDiagnostics.js +309 -0
- package/dist/pluginExecution.d.ts +21 -0
- package/dist/pluginExecution.js +43 -0
- package/dist/pluginExecutor.d.ts +29 -0
- package/dist/pluginExecutor.js +223 -0
- package/dist/pluginExecutorProtocol.d.ts +55 -0
- package/dist/pluginExecutorProtocol.js +47 -0
- package/dist/pluginExecutorSupervisor.d.ts +4 -0
- package/dist/pluginExecutorSupervisor.js +63 -0
- package/dist/pluginHost.d.ts +20 -0
- package/dist/pluginHost.js +53 -0
- package/dist/pluginHostConnection.d.ts +35 -0
- package/dist/pluginHostConnection.js +257 -0
- package/dist/pluginHostFiles.d.ts +1 -0
- package/dist/pluginHostFiles.js +21 -0
- package/dist/pluginHostPaths.d.ts +11 -0
- package/dist/pluginHostPaths.js +22 -0
- package/dist/pluginHostServer.d.ts +7 -0
- package/dist/pluginHostServer.js +252 -0
- package/dist/pluginModelCatalog.d.ts +14 -0
- package/dist/pluginModelCatalog.js +51 -0
- package/dist/pluginModelDefinition.d.ts +6 -0
- package/dist/pluginModelDefinition.js +75 -0
- package/dist/pluginModelSessionWorker.d.ts +23 -0
- package/dist/pluginModelSessionWorker.js +60 -0
- package/dist/pluginModelSessions.d.ts +16 -0
- package/dist/pluginModelSessions.js +234 -0
- package/dist/pluginModels.d.ts +17 -0
- package/dist/pluginModels.js +47 -0
- package/dist/pluginPageBundle.js +74 -17
- package/dist/pluginPageDefinition.d.ts +3 -0
- package/dist/pluginPageDefinition.js +62 -0
- package/dist/pluginPageManager.d.ts +57 -0
- package/dist/pluginPageManager.js +877 -0
- package/dist/pluginPages.d.ts +21 -0
- package/dist/pluginPages.js +46 -0
- package/dist/pluginProcessTree.d.ts +2 -0
- package/dist/pluginProcessTree.js +31 -0
- package/dist/pluginRecoveryDefinition.d.ts +3 -0
- package/dist/pluginRecoveryDefinition.js +56 -0
- package/dist/pluginRegistry.d.ts +2 -3
- package/dist/pluginRegistry.js +14 -13
- package/dist/pluginRevision.d.ts +14 -0
- package/dist/pluginRevision.js +227 -0
- package/dist/pluginRuntimeCleanup.d.ts +2 -0
- package/dist/pluginRuntimeCleanup.js +55 -0
- package/dist/pluginState.d.ts +26 -0
- package/dist/pluginState.js +28 -0
- package/dist/pluginStateDefinition.d.ts +4 -0
- package/dist/pluginStateDefinition.js +68 -0
- package/dist/pluginStateManager.d.ts +24 -0
- package/dist/pluginStateManager.js +132 -0
- package/dist/pluginStateStore.d.ts +57 -0
- package/dist/pluginStateStore.js +396 -0
- package/dist/pluginTaskManager.d.ts +92 -0
- package/dist/pluginTaskManager.js +1080 -0
- package/dist/pluginTaskProtocol.d.ts +47 -0
- package/dist/pluginTaskProtocol.js +1 -0
- package/dist/pluginTaskStore.d.ts +53 -0
- package/dist/pluginTaskStore.js +319 -0
- package/dist/pluginTaskWire.d.ts +4 -0
- package/dist/pluginTaskWire.js +42 -0
- package/dist/pluginTasks.d.ts +51 -0
- package/dist/pluginTasks.js +181 -0
- package/dist/pythonPlugin.d.ts +13 -2
- package/dist/pythonPlugin.js +235 -30
- package/dist/socket.js +5 -2
- package/dist/unified.d.ts +1 -0
- package/dist/unified.js +81 -0
- package/package.json +7 -3
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:net';
|
|
3
|
+
import { chmodSync, rmSync } from 'node:fs';
|
|
4
|
+
import { claimRecord, isProcessOwnerAlive, processIdentity, readRecord, removeInvalidRecord, removeRecordIfPid, writeRecordIfPid, } from '@mearl/daemon-core';
|
|
5
|
+
import { PLUGIN_TASK_PROTOCOL_VERSION } from './generated-plugin-tasks.js';
|
|
6
|
+
import { pluginHostPaths } from './pluginHostPaths.js';
|
|
7
|
+
import { PluginHost } from './pluginHost.js';
|
|
8
|
+
import { readPluginTaskMessages, writePluginTaskMessage } from './pluginTaskWire.js';
|
|
9
|
+
import { pluginExecutionError, serializePluginExecutionError } from './pluginExecutorProtocol.js';
|
|
10
|
+
export async function startPluginHostServer(configuration = {}) {
|
|
11
|
+
const paths = pluginHostPaths();
|
|
12
|
+
const existing = readRecord(paths.lease);
|
|
13
|
+
if (existing && !isProcessOwnerAlive(existing))
|
|
14
|
+
removeRecordIfPid(paths.lease, existing.pid);
|
|
15
|
+
else if (!existing)
|
|
16
|
+
removeInvalidRecord(paths.lease);
|
|
17
|
+
const owner = {
|
|
18
|
+
pid: process.pid,
|
|
19
|
+
protocolVersion: PLUGIN_TASK_PROTOCOL_VERSION,
|
|
20
|
+
processIdentity: processIdentity(process.pid),
|
|
21
|
+
};
|
|
22
|
+
if (!claimRecord(paths.lease, owner))
|
|
23
|
+
throw Object.assign(new Error('Plugin execution service already has an owner'), {
|
|
24
|
+
code: 'PLUGIN_TASK_HOST_OWNED',
|
|
25
|
+
});
|
|
26
|
+
let host;
|
|
27
|
+
try {
|
|
28
|
+
host = new PluginHost(configuration);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
removeRecordIfPid(paths.lease, process.pid);
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
const { tasks: manager, pages, state, models } = host;
|
|
35
|
+
if (!writeRecordIfPid(paths.lease, process.pid, {
|
|
36
|
+
...owner,
|
|
37
|
+
...manager.host,
|
|
38
|
+
socketPath: paths.socket,
|
|
39
|
+
startedAt: new Date().toISOString(),
|
|
40
|
+
}))
|
|
41
|
+
throw new Error('Plugin host lease ownership was lost');
|
|
42
|
+
const sessions = new Set();
|
|
43
|
+
let closing;
|
|
44
|
+
const server = createServer(socket => {
|
|
45
|
+
sessions.add(socket);
|
|
46
|
+
const disconnected = new AbortController();
|
|
47
|
+
let scope;
|
|
48
|
+
let sequence = 0;
|
|
49
|
+
let requests = 0;
|
|
50
|
+
const pending = new Map();
|
|
51
|
+
const bridge = {
|
|
52
|
+
id: randomUUID(),
|
|
53
|
+
get scope() {
|
|
54
|
+
if (!scope)
|
|
55
|
+
throw new Error('Plugin task connection is not initialized');
|
|
56
|
+
return scope;
|
|
57
|
+
},
|
|
58
|
+
invoke(action, payload, options) {
|
|
59
|
+
if (socket.destroyed)
|
|
60
|
+
return Promise.reject(Object.assign(new Error('The browser invocation bridge is unavailable'), {
|
|
61
|
+
code: 'PLUGIN_TASK_BRIDGE_UNAVAILABLE',
|
|
62
|
+
}));
|
|
63
|
+
const id = ++sequence;
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
pending.delete(id);
|
|
67
|
+
reject(Object.assign(new Error('The browser invocation bridge timed out; external operation status is unknown'), { code: 'PLUGIN_TASK_BRIDGE_UNAVAILABLE', details: { action } }));
|
|
68
|
+
}, Math.min(Math.max(options.timeoutSec ?? 180, 1), 3600) * 1000);
|
|
69
|
+
pending.set(id, { resolve, reject, timer });
|
|
70
|
+
void writePluginTaskMessage(socket, {
|
|
71
|
+
type: 'invoke',
|
|
72
|
+
id,
|
|
73
|
+
action,
|
|
74
|
+
payload,
|
|
75
|
+
options,
|
|
76
|
+
}).catch(error => {
|
|
77
|
+
pending.delete(id);
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
reject(error);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
const handle = async (method, input) => {
|
|
85
|
+
if (method === 'hello') {
|
|
86
|
+
if (scope || input.protocolVersion !== PLUGIN_TASK_PROTOCOL_VERSION)
|
|
87
|
+
throw new Error('Plugin task protocol version mismatch');
|
|
88
|
+
if (!['public', 'toolbox', 'runtime'].includes(String(input.access)) ||
|
|
89
|
+
!['service', 'caller'].includes(String(input.bridge)) ||
|
|
90
|
+
!['cli', 'sdk', 'mcp', 'toolbox', 'runtime'].includes(String(input.source)) ||
|
|
91
|
+
(input.plugin !== undefined &&
|
|
92
|
+
(typeof input.plugin !== 'string' || !/^[a-z][a-z0-9-]*$/.test(input.plugin))) ||
|
|
93
|
+
(input.scopeId !== undefined &&
|
|
94
|
+
(typeof input.scopeId !== 'string' || input.scopeId.length > 256)) ||
|
|
95
|
+
(input.access !== 'public' && (!input.plugin || input.bridge !== 'caller')))
|
|
96
|
+
throw new Error('Invalid plugin task caller scope');
|
|
97
|
+
scope = {
|
|
98
|
+
access: input.access,
|
|
99
|
+
bridge: input.bridge,
|
|
100
|
+
source: input.source,
|
|
101
|
+
...(input.plugin ? { plugin: String(input.plugin) } : {}),
|
|
102
|
+
...(input.scopeId ? { scopeId: String(input.scopeId) } : {}),
|
|
103
|
+
};
|
|
104
|
+
return {
|
|
105
|
+
protocolVersion: PLUGIN_TASK_PROTOCOL_VERSION,
|
|
106
|
+
host: manager.host,
|
|
107
|
+
pid: process.pid,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
if (!scope)
|
|
111
|
+
throw new Error('Plugin task connection requires a handshake');
|
|
112
|
+
if (method === 'status')
|
|
113
|
+
return {
|
|
114
|
+
protocolVersion: PLUGIN_TASK_PROTOCOL_VERSION,
|
|
115
|
+
host: manager.host,
|
|
116
|
+
pid: process.pid,
|
|
117
|
+
};
|
|
118
|
+
if (method === 'diagnose')
|
|
119
|
+
return host.diagnose(input, scope);
|
|
120
|
+
if (method === 'state.call')
|
|
121
|
+
return state.call(String(input.plugin), input.input, bridge, input.options);
|
|
122
|
+
if (method === 'model.list')
|
|
123
|
+
return models.list(input.provider);
|
|
124
|
+
if (method === 'model.capabilities')
|
|
125
|
+
return (await import('./pluginModelDefinition.js')).pluginModelCapabilities();
|
|
126
|
+
if (method === 'page.call') {
|
|
127
|
+
const execution = input.execution;
|
|
128
|
+
return pages.call(String(input.plugin), input.input, bridge, input.options, execution
|
|
129
|
+
? {
|
|
130
|
+
cwd: execution.cwd,
|
|
131
|
+
environment: execution.environment,
|
|
132
|
+
signal: disconnected.signal,
|
|
133
|
+
}
|
|
134
|
+
: undefined);
|
|
135
|
+
}
|
|
136
|
+
if (method === 'page.event') {
|
|
137
|
+
if (scope.access !== 'runtime')
|
|
138
|
+
throw new Error('Page events require the browser runtime bridge');
|
|
139
|
+
return pages.receive(String(input.plugin), input.input, bridge, input.options);
|
|
140
|
+
}
|
|
141
|
+
if (method === 'start')
|
|
142
|
+
return manager.start(input, bridge);
|
|
143
|
+
if (method === 'resume')
|
|
144
|
+
return manager.resume(input, bridge);
|
|
145
|
+
if (method === 'list')
|
|
146
|
+
return manager.list(scope, input.activeOnly === true, input.cursor, input.limit);
|
|
147
|
+
if (method === 'shutdown') {
|
|
148
|
+
if (scope.access !== 'public')
|
|
149
|
+
throw new Error('Tool pages cannot stop the plugin host');
|
|
150
|
+
return { stopped: true };
|
|
151
|
+
}
|
|
152
|
+
if (typeof input.taskId !== 'string')
|
|
153
|
+
throw new Error('Plugin task ID is required');
|
|
154
|
+
switch (method) {
|
|
155
|
+
case 'get':
|
|
156
|
+
return manager.get(input.taskId, scope);
|
|
157
|
+
case 'checkpoint':
|
|
158
|
+
return manager.checkpoint(input.taskId, scope);
|
|
159
|
+
case 'cancel':
|
|
160
|
+
return manager.cancel(input.taskId, scope);
|
|
161
|
+
case 'events':
|
|
162
|
+
return manager.events(input.taskId, scope, input.cursor, input.waitMs, disconnected.signal);
|
|
163
|
+
case 'result':
|
|
164
|
+
return manager.resultChunk(input.taskId, scope, input.index);
|
|
165
|
+
default:
|
|
166
|
+
throw new Error(`Unknown plugin task method: ${method}`);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
readPluginTaskMessages(socket, message => {
|
|
170
|
+
if (message.type === 'invoke-result') {
|
|
171
|
+
const request = pending.get(message.id);
|
|
172
|
+
if (!request)
|
|
173
|
+
return;
|
|
174
|
+
clearTimeout(request.timer);
|
|
175
|
+
pending.delete(message.id);
|
|
176
|
+
if (message.error)
|
|
177
|
+
request.reject(pluginExecutionError(message.error));
|
|
178
|
+
else
|
|
179
|
+
request.resolve(message.result);
|
|
180
|
+
}
|
|
181
|
+
else if (message.type === 'request') {
|
|
182
|
+
if (++requests > 64 ||
|
|
183
|
+
!message.input ||
|
|
184
|
+
typeof message.input !== 'object' ||
|
|
185
|
+
Array.isArray(message.input)) {
|
|
186
|
+
socket.destroy(new Error('Invalid or excessive plugin task requests'));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
void Promise.resolve()
|
|
190
|
+
.then(() => handle(message.method, message.input))
|
|
191
|
+
.then(async (result) => {
|
|
192
|
+
await writePluginTaskMessage(socket, { type: 'response', id: message.id, result });
|
|
193
|
+
if (message.method === 'shutdown')
|
|
194
|
+
setImmediate(() => {
|
|
195
|
+
void close().catch(() => { });
|
|
196
|
+
});
|
|
197
|
+
}, error => writePluginTaskMessage(socket, {
|
|
198
|
+
type: 'response',
|
|
199
|
+
id: message.id,
|
|
200
|
+
error: serializePluginExecutionError(error),
|
|
201
|
+
}))
|
|
202
|
+
.catch(() => socket.destroy())
|
|
203
|
+
.finally(() => {
|
|
204
|
+
requests--;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
else
|
|
208
|
+
socket.destroy(new Error('Invalid plugin task message direction'));
|
|
209
|
+
});
|
|
210
|
+
socket.on('error', () => { });
|
|
211
|
+
socket.once('close', () => {
|
|
212
|
+
disconnected.abort();
|
|
213
|
+
sessions.delete(socket);
|
|
214
|
+
for (const request of pending.values()) {
|
|
215
|
+
clearTimeout(request.timer);
|
|
216
|
+
request.reject(Object.assign(new Error('The invocation bridge disconnected; external operation status may need reconciliation'), { code: 'PLUGIN_TASK_BRIDGE_UNAVAILABLE' }));
|
|
217
|
+
}
|
|
218
|
+
pending.clear();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
async function close() {
|
|
222
|
+
closing ??= (async () => {
|
|
223
|
+
const servicesClosed = host.close();
|
|
224
|
+
for (const socket of sessions)
|
|
225
|
+
socket.destroy();
|
|
226
|
+
await servicesClosed;
|
|
227
|
+
await new Promise(resolve => server.close(() => resolve()));
|
|
228
|
+
if (removeRecordIfPid(paths.lease, process.pid) && process.platform !== 'win32')
|
|
229
|
+
rmSync(paths.socket, { force: true });
|
|
230
|
+
})();
|
|
231
|
+
return closing;
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
if (process.platform !== 'win32')
|
|
235
|
+
rmSync(paths.socket, { force: true });
|
|
236
|
+
await new Promise((resolve, reject) => {
|
|
237
|
+
server.once('error', reject);
|
|
238
|
+
server.listen(paths.socket, () => {
|
|
239
|
+
server.off('error', reject);
|
|
240
|
+
resolve();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
if (process.platform !== 'win32')
|
|
244
|
+
chmodSync(paths.socket, 0o600);
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
removeRecordIfPid(paths.lease, process.pid);
|
|
248
|
+
await host.close();
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
return { close, host, socketPath: paths.socket };
|
|
252
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PluginModelList, PluginModelOption } from './generated-plugin-models.js';
|
|
2
|
+
/** A bounded shared lookup avoids starting a discovery process for each open tool. */
|
|
3
|
+
export declare class PluginModelCatalog {
|
|
4
|
+
private readonly discover;
|
|
5
|
+
private readonly now;
|
|
6
|
+
private readonly controller;
|
|
7
|
+
private cached?;
|
|
8
|
+
private pending?;
|
|
9
|
+
constructor(discover?: (signal: AbortSignal) => Promise<PluginModelOption[]>, now?: () => number);
|
|
10
|
+
list(provider?: unknown): Promise<PluginModelList>;
|
|
11
|
+
private refresh;
|
|
12
|
+
private load;
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** A bounded shared lookup avoids starting a discovery process for each open tool. */
|
|
2
|
+
export class PluginModelCatalog {
|
|
3
|
+
discover;
|
|
4
|
+
now;
|
|
5
|
+
controller = new AbortController();
|
|
6
|
+
cached;
|
|
7
|
+
pending;
|
|
8
|
+
constructor(discover = async (signal) => {
|
|
9
|
+
const { listCodexModels } = await import('@mearl/agent-runtime/codex');
|
|
10
|
+
return listCodexModels(signal);
|
|
11
|
+
}, now = Date.now) {
|
|
12
|
+
this.discover = discover;
|
|
13
|
+
this.now = now;
|
|
14
|
+
}
|
|
15
|
+
list(provider = 'codex') {
|
|
16
|
+
this.controller.signal.throwIfAborted();
|
|
17
|
+
if (provider !== 'codex')
|
|
18
|
+
throw new Error('Unsupported model provider');
|
|
19
|
+
if (this.cached) {
|
|
20
|
+
if (this.cached.expires <= this.now())
|
|
21
|
+
void this.refresh().catch(() => { });
|
|
22
|
+
return Promise.resolve(structuredClone(this.cached.value));
|
|
23
|
+
}
|
|
24
|
+
return this.refresh().then(value => structuredClone(value));
|
|
25
|
+
}
|
|
26
|
+
refresh() {
|
|
27
|
+
this.pending ??= this.load()
|
|
28
|
+
.catch(error => {
|
|
29
|
+
// A failed refresh keeps the last usable catalog and bounds repeat discovery.
|
|
30
|
+
if (this.cached)
|
|
31
|
+
this.cached.expires = this.now() + 30000;
|
|
32
|
+
throw error;
|
|
33
|
+
})
|
|
34
|
+
.finally(() => {
|
|
35
|
+
this.pending = undefined;
|
|
36
|
+
});
|
|
37
|
+
return this.pending;
|
|
38
|
+
}
|
|
39
|
+
async load() {
|
|
40
|
+
const signal = AbortSignal.any([this.controller.signal, AbortSignal.timeout(15000)]);
|
|
41
|
+
const value = { provider: 'codex', models: await this.discover(signal) };
|
|
42
|
+
signal.throwIfAborted();
|
|
43
|
+
this.cached = { value, expires: this.now() + 120000 };
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
async close() {
|
|
47
|
+
this.controller.abort();
|
|
48
|
+
await this.pending?.catch(() => { });
|
|
49
|
+
this.cached = undefined;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PluginModelCapability, PluginModelRequest, PluginModelResult } from './generated-plugin-models.js';
|
|
2
|
+
export declare function pluginModelCapabilities(): {
|
|
3
|
+
providers: PluginModelCapability[];
|
|
4
|
+
};
|
|
5
|
+
export declare function normalizePluginModel(value: unknown): PluginModelRequest;
|
|
6
|
+
export declare function validatePluginModelResult(request: PluginModelRequest, value: unknown): PluginModelResult;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { CODEX_MODEL_CAPABILITY } from '@mearl/agent-runtime/capabilities';
|
|
2
|
+
import { compilePluginStateSchema } from './pluginStateDefinition.js';
|
|
3
|
+
export function pluginModelCapabilities() {
|
|
4
|
+
return { providers: [CODEX_MODEL_CAPABILITY] };
|
|
5
|
+
}
|
|
6
|
+
export function normalizePluginModel(value) {
|
|
7
|
+
const request = value;
|
|
8
|
+
if (!request ||
|
|
9
|
+
typeof request !== 'object' ||
|
|
10
|
+
Array.isArray(request) ||
|
|
11
|
+
typeof request.prompt !== 'string' ||
|
|
12
|
+
!request.prompt.trim() ||
|
|
13
|
+
Buffer.byteLength(JSON.stringify(request)) > 256 * 1024 ||
|
|
14
|
+
(request.instructions !== undefined && typeof request.instructions !== 'string') ||
|
|
15
|
+
(request.conversation !== undefined &&
|
|
16
|
+
(typeof request.conversation !== 'string' ||
|
|
17
|
+
!/^[a-zA-Z0-9:_-]{1,80}$/.test(request.conversation))) ||
|
|
18
|
+
(request.model !== undefined &&
|
|
19
|
+
(typeof request.model !== 'string' || !request.model.trim() || request.model.length > 256)) ||
|
|
20
|
+
(request.reasoningEffort !== undefined &&
|
|
21
|
+
!['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'].includes(request.reasoningEffort)) ||
|
|
22
|
+
Object.keys(request).some(key => ![
|
|
23
|
+
'provider',
|
|
24
|
+
'mode',
|
|
25
|
+
'prompt',
|
|
26
|
+
'instructions',
|
|
27
|
+
'model',
|
|
28
|
+
'reasoningEffort',
|
|
29
|
+
'outputSchema',
|
|
30
|
+
'permissions',
|
|
31
|
+
'conversation',
|
|
32
|
+
].includes(key)))
|
|
33
|
+
throw new Error('Invalid model execution request');
|
|
34
|
+
const capability = pluginModelCapabilities().providers.find(item => item.provider === request.provider);
|
|
35
|
+
if (!capability?.modes.includes(request.mode) ||
|
|
36
|
+
(request.mode === 'agent' && !capability.permissions.includes(request.permissions)) ||
|
|
37
|
+
(request.mode === 'generate' && request.permissions !== undefined))
|
|
38
|
+
throw Object.assign(new Error('Requested model mode or permissions are unsupported; choose an advertised capability explicitly'), { code: 'PLUGIN_MODEL_CAPABILITY_UNSUPPORTED' });
|
|
39
|
+
if (request.outputSchema !== undefined) {
|
|
40
|
+
if (!capability.structuredOutput ||
|
|
41
|
+
!request.outputSchema ||
|
|
42
|
+
typeof request.outputSchema !== 'object' ||
|
|
43
|
+
Array.isArray(request.outputSchema) ||
|
|
44
|
+
request.outputSchema.$async ||
|
|
45
|
+
Buffer.byteLength(JSON.stringify(request.outputSchema)) > 64 * 1024)
|
|
46
|
+
throw new Error('Invalid model output schema');
|
|
47
|
+
compilePluginStateSchema(request.outputSchema);
|
|
48
|
+
}
|
|
49
|
+
return JSON.parse(JSON.stringify(request));
|
|
50
|
+
}
|
|
51
|
+
export function validatePluginModelResult(request, value) {
|
|
52
|
+
const result = value;
|
|
53
|
+
if (!result ||
|
|
54
|
+
typeof result.text !== 'string' ||
|
|
55
|
+
result.provider !== request.provider ||
|
|
56
|
+
result.mode !== request.mode)
|
|
57
|
+
throw new Error('Invalid model execution result');
|
|
58
|
+
if (request.outputSchema) {
|
|
59
|
+
let output;
|
|
60
|
+
try {
|
|
61
|
+
output = JSON.parse(result.text);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw Object.assign(new Error('Model returned invalid JSON'), {
|
|
65
|
+
code: 'PLUGIN_MODEL_SCHEMA_INVALID',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (!compilePluginStateSchema(request.outputSchema)(output))
|
|
69
|
+
throw Object.assign(new Error('Model result does not match the output schema'), {
|
|
70
|
+
code: 'PLUGIN_MODEL_SCHEMA_INVALID',
|
|
71
|
+
});
|
|
72
|
+
return { ...result, output };
|
|
73
|
+
}
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { PluginModelDiagnostic, PluginModelRequest, PluginModelResult } from './generated-plugin-models.js';
|
|
2
|
+
import type { PluginExecutionError } from './generated-plugin-execution.js';
|
|
3
|
+
export type ModelSessionRequest = {
|
|
4
|
+
request: PluginModelRequest;
|
|
5
|
+
};
|
|
6
|
+
export type ModelSessionEvent = {
|
|
7
|
+
type: 'diagnostic';
|
|
8
|
+
metadata: PluginModelDiagnostic;
|
|
9
|
+
} | {
|
|
10
|
+
type: 'delta';
|
|
11
|
+
delta: string;
|
|
12
|
+
index: number;
|
|
13
|
+
metadata?: {
|
|
14
|
+
itemId?: string;
|
|
15
|
+
phase?: string;
|
|
16
|
+
};
|
|
17
|
+
} | {
|
|
18
|
+
type: 'result';
|
|
19
|
+
result: PluginModelResult;
|
|
20
|
+
} | {
|
|
21
|
+
type: 'error';
|
|
22
|
+
error: PluginExecutionError;
|
|
23
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { serializePluginExecutionError } from './pluginExecutorProtocol.js';
|
|
3
|
+
import { supervisePluginExecutor } from './pluginExecutorSupervisor.js';
|
|
4
|
+
if (process.argv[2] !== '--command') {
|
|
5
|
+
supervisePluginExecutor(import.meta.url);
|
|
6
|
+
}
|
|
7
|
+
else {
|
|
8
|
+
const ready = import('@mearl/agent-runtime/codex').then(runtime => ({
|
|
9
|
+
runtime,
|
|
10
|
+
provider: new runtime.CodexAgentProvider(() => { }, JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version),
|
|
11
|
+
}));
|
|
12
|
+
const conversation = {};
|
|
13
|
+
let busy = false;
|
|
14
|
+
let turns = 0;
|
|
15
|
+
const diagnostic = {};
|
|
16
|
+
const send = (value) => {
|
|
17
|
+
if (process.connected)
|
|
18
|
+
process.send?.(value);
|
|
19
|
+
};
|
|
20
|
+
process.on('disconnect', () => process.exit(1));
|
|
21
|
+
process.on('message', (message) => {
|
|
22
|
+
if (busy) {
|
|
23
|
+
process.exit(1);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
busy = true;
|
|
27
|
+
void ready
|
|
28
|
+
.then(async ({ runtime, provider }) => {
|
|
29
|
+
send({ type: 'diagnostic', metadata: diagnostic });
|
|
30
|
+
if (turns >= 32) {
|
|
31
|
+
if (conversation.threadId)
|
|
32
|
+
await provider.closeSession(conversation.threadId);
|
|
33
|
+
delete conversation.threadId;
|
|
34
|
+
turns = 0;
|
|
35
|
+
}
|
|
36
|
+
const result = await runtime.runCodexModel(message.request, {
|
|
37
|
+
cwd: process.cwd(),
|
|
38
|
+
conversation,
|
|
39
|
+
signal: new AbortController().signal,
|
|
40
|
+
onDiagnostic: metadata => {
|
|
41
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
42
|
+
if (!['retryCount', 'activeModel'].includes(key))
|
|
43
|
+
Object.assign(diagnostic, { [key]: value });
|
|
44
|
+
}
|
|
45
|
+
send({ type: 'diagnostic', metadata });
|
|
46
|
+
},
|
|
47
|
+
onDelta: (delta, index, metadata) => send({ type: 'delta', delta, index, metadata }),
|
|
48
|
+
}, provider);
|
|
49
|
+
turns++;
|
|
50
|
+
return result;
|
|
51
|
+
})
|
|
52
|
+
.then(result => {
|
|
53
|
+
busy = false;
|
|
54
|
+
send({ type: 'result', result });
|
|
55
|
+
}, error => {
|
|
56
|
+
busy = false;
|
|
57
|
+
send({ type: 'error', error: serializePluginExecutionError(error) });
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { executeRegisteredPlugin } from './pluginExecutor.js';
|
|
2
|
+
/** Opt-in conversations retain a supervised process and thread within one execution owner. */
|
|
3
|
+
export declare class PluginModelSessions {
|
|
4
|
+
private readonly idleMs;
|
|
5
|
+
private readonly limit;
|
|
6
|
+
private readonly workerPath;
|
|
7
|
+
private readonly sessions;
|
|
8
|
+
private readonly closing;
|
|
9
|
+
private closed;
|
|
10
|
+
constructor(idleMs?: number, limit?: number, workerPath?: string);
|
|
11
|
+
execute(owner: string, ...args: Parameters<typeof executeRegisteredPlugin>): Promise<unknown>;
|
|
12
|
+
private run;
|
|
13
|
+
private remove;
|
|
14
|
+
release(owner: string): Promise<void>;
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
}
|