dsh-agy-cli 0.0.0-stage → 0.12.1
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/LICENSE +31 -0
- package/README.en.md +48 -0
- package/README.md +67 -2
- package/agents/dsh-agy-image-view.md +17 -0
- package/agents/dsh-agy-mcp-executor.md +32 -0
- package/agents/dsh-agy-read-only.md +20 -0
- package/agents/dsh-agy-tool-free.md +16 -0
- package/agents/dsh-agy-workspace-write.md +23 -0
- package/bin/dsh-agy-cli.js +9 -0
- package/bin/win32-x64/agy-launcher.exe +0 -0
- package/cordis.patch.yml +16 -0
- package/dist/client.js +4259 -0
- package/lib/agent-installer.d.ts +66 -0
- package/lib/agent-installer.js +147 -0
- package/lib/agent-presets.d.ts +42 -0
- package/lib/agent-presets.js +130 -0
- package/lib/agents-cli.d.ts +3 -0
- package/lib/agents-cli.js +110 -0
- package/lib/agy/diagnostics.d.ts +29 -0
- package/lib/agy/diagnostics.js +109 -0
- package/lib/agy/errors.d.ts +20 -0
- package/lib/agy/errors.js +77 -0
- package/lib/agy/experimental-transport.d.ts +79 -0
- package/lib/agy/experimental-transport.js +612 -0
- package/lib/agy/keepalive.d.ts +140 -0
- package/lib/agy/keepalive.js +445 -0
- package/lib/agy/limiter.d.ts +32 -0
- package/lib/agy/limiter.js +111 -0
- package/lib/agy/log.d.ts +214 -0
- package/lib/agy/log.js +503 -0
- package/lib/agy/models.d.ts +49 -0
- package/lib/agy/models.js +203 -0
- package/lib/agy/overrides.d.ts +17 -0
- package/lib/agy/overrides.js +73 -0
- package/lib/agy/parser.d.ts +102 -0
- package/lib/agy/parser.js +492 -0
- package/lib/agy/persistent-transport.d.ts +101 -0
- package/lib/agy/persistent-transport.js +753 -0
- package/lib/agy/process.d.ts +144 -0
- package/lib/agy/process.js +452 -0
- package/lib/agy/redact.d.ts +3 -0
- package/lib/agy/redact.js +24 -0
- package/lib/agy/stream-protocol.d.ts +50 -0
- package/lib/agy/stream-protocol.js +156 -0
- package/lib/agy/windows-launcher.d.ts +21 -0
- package/lib/agy/windows-launcher.js +76 -0
- package/lib/diagnostics.d.ts +78 -0
- package/lib/diagnostics.js +162 -0
- package/lib/doctor.d.ts +138 -0
- package/lib/doctor.js +940 -0
- package/lib/dsh/context.d.ts +133 -0
- package/lib/dsh/context.js +352 -0
- package/lib/dsh-home.d.ts +6 -0
- package/lib/dsh-home.js +11 -0
- package/lib/index.d.ts +54 -0
- package/lib/index.js +267 -0
- package/lib/mcp/agy-registration.d.ts +70 -0
- package/lib/mcp/agy-registration.js +245 -0
- package/lib/mcp/bridge.d.ts +73 -0
- package/lib/mcp/bridge.js +295 -0
- package/lib/mcp/endpoint.d.ts +41 -0
- package/lib/mcp/endpoint.js +142 -0
- package/lib/mcp/protocol.d.ts +107 -0
- package/lib/mcp/protocol.js +197 -0
- package/lib/mcp/tool-projection.d.ts +64 -0
- package/lib/mcp/tool-projection.js +168 -0
- package/lib/provider/agy.d.ts +153 -0
- package/lib/provider/agy.js +2159 -0
- package/lib/provider/config.d.ts +226 -0
- package/lib/provider/config.js +294 -0
- package/lib/provider/error-codes.d.ts +22 -0
- package/lib/provider/error-codes.js +29 -0
- package/lib/provider/image-bridge.d.ts +60 -0
- package/lib/provider/image-bridge.js +290 -0
- package/lib/provider/mock.d.ts +15 -0
- package/lib/provider/mock.js +78 -0
- package/lib/provider/prompt-budget.d.ts +45 -0
- package/lib/provider/prompt-budget.js +596 -0
- package/lib/provider/serialize.d.ts +15 -0
- package/lib/provider/serialize.js +82 -0
- package/lib/provider/tool-protocol.d.ts +80 -0
- package/lib/provider/tool-protocol.js +848 -0
- package/lib/provider/tool-schema-file.d.ts +11 -0
- package/lib/provider/tool-schema-file.js +27 -0
- package/lib/session/file-store.d.ts +56 -0
- package/lib/session/file-store.js +186 -0
- package/lib/session/store.d.ts +49 -0
- package/lib/session/store.js +62 -0
- package/lib/web/router.d.ts +19 -0
- package/lib/web/router.js +994 -0
- package/package.json +115 -4
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
export class PersistentTransportError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(message, code, options) {
|
|
6
|
+
super(message, options);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.name = 'PersistentTransportError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const DEFAULT_MAX_WORKERS = 8;
|
|
12
|
+
const DEFAULT_MAX_FRAME_BYTES = 256 * 1024;
|
|
13
|
+
const DEFAULT_MAX_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
14
|
+
const DEFAULT_MAX_STDERR_BYTES = 64 * 1024;
|
|
15
|
+
const DEFAULT_IDLE_TTL_MS = 30_000;
|
|
16
|
+
const DEFAULT_READY_TIMEOUT_MS = 5_000;
|
|
17
|
+
const DEFAULT_SHUTDOWN_TIMEOUT_MS = 1_000;
|
|
18
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
|
19
|
+
function delay(milliseconds) {
|
|
20
|
+
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
|
21
|
+
}
|
|
22
|
+
function validateInteger(name, value, minimum, maximum) {
|
|
23
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
|
24
|
+
throw new RangeError(`${name} must be an integer between ${minimum} and ${maximum}`);
|
|
25
|
+
}
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
function normalizeOptions(options) {
|
|
29
|
+
if (options.executable.trim().length === 0) {
|
|
30
|
+
throw new RangeError('executable must not be empty');
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
executable: options.executable,
|
|
34
|
+
args: options.args ?? [],
|
|
35
|
+
cwd: options.cwd,
|
|
36
|
+
env: options.env,
|
|
37
|
+
maxWorkers: validateInteger('maxWorkers', options.maxWorkers ?? DEFAULT_MAX_WORKERS, 1, 64),
|
|
38
|
+
maxFrameBytes: validateInteger('maxFrameBytes', options.maxFrameBytes ?? DEFAULT_MAX_FRAME_BYTES, 128, 16 * 1024 * 1024),
|
|
39
|
+
maxOutputBytes: validateInteger('maxOutputBytes', options.maxOutputBytes ?? DEFAULT_MAX_OUTPUT_BYTES, 1, 64 * 1024 * 1024),
|
|
40
|
+
maxStderrBytes: validateInteger('maxStderrBytes', options.maxStderrBytes ?? DEFAULT_MAX_STDERR_BYTES, 1, 8 * 1024 * 1024),
|
|
41
|
+
idleTtlMs: validateInteger('idleTtlMs', options.idleTtlMs ?? DEFAULT_IDLE_TTL_MS, 0, 3_600_000),
|
|
42
|
+
readyTimeoutMs: validateInteger('readyTimeoutMs', options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS, 1, 60_000),
|
|
43
|
+
shutdownTimeoutMs: validateInteger('shutdownTimeoutMs', options.shutdownTimeoutMs ?? DEFAULT_SHUTDOWN_TIMEOUT_MS, 1, 60_000),
|
|
44
|
+
defaultRequestTimeoutMs: validateInteger('defaultRequestTimeoutMs', options.defaultRequestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS, 1, 3_600_000),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function isRecord(value) {
|
|
48
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
49
|
+
}
|
|
50
|
+
function byteLength(value) {
|
|
51
|
+
return Buffer.byteLength(value, 'utf8');
|
|
52
|
+
}
|
|
53
|
+
/** Encode one bounded NDJSON frame, including its trailing newline. */
|
|
54
|
+
export function encodePersistentFrame(frame, maxFrameBytes) {
|
|
55
|
+
const line = JSON.stringify(frame);
|
|
56
|
+
if (line === undefined) {
|
|
57
|
+
throw new PersistentTransportError('Persistent transport frame could not be encoded', 'PROTOCOL_ERROR');
|
|
58
|
+
}
|
|
59
|
+
const encoded = `${line}\n`;
|
|
60
|
+
if (byteLength(encoded) > maxFrameBytes) {
|
|
61
|
+
throw new PersistentTransportError('Persistent transport input frame exceeds the configured limit', 'FRAME_TOO_LARGE');
|
|
62
|
+
}
|
|
63
|
+
return encoded;
|
|
64
|
+
}
|
|
65
|
+
/** Parse and validate the small protocol envelope; payload remains opaque. */
|
|
66
|
+
export function parsePersistentFrame(line) {
|
|
67
|
+
if (line.trim().length === 0) {
|
|
68
|
+
throw new PersistentTransportError('Persistent transport emitted an empty frame', 'PROTOCOL_ERROR');
|
|
69
|
+
}
|
|
70
|
+
let value;
|
|
71
|
+
try {
|
|
72
|
+
value = JSON.parse(line);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
throw new PersistentTransportError('Persistent transport emitted malformed JSON', 'PROTOCOL_ERROR', {
|
|
76
|
+
cause: error,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (!isRecord(value) || typeof value.kind !== 'string'
|
|
80
|
+
|| !['ready', 'event', 'complete', 'error'].includes(value.kind)) {
|
|
81
|
+
throw new PersistentTransportError('Persistent transport emitted an invalid frame envelope', 'PROTOCOL_ERROR');
|
|
82
|
+
}
|
|
83
|
+
if (value.requestId !== undefined && typeof value.requestId !== 'string') {
|
|
84
|
+
throw new PersistentTransportError('Persistent transport frame requestId is invalid', 'PROTOCOL_ERROR');
|
|
85
|
+
}
|
|
86
|
+
if (value.sessionId !== undefined && typeof value.sessionId !== 'string') {
|
|
87
|
+
throw new PersistentTransportError('Persistent transport frame sessionId is invalid', 'PROTOCOL_ERROR');
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
kind: value.kind,
|
|
91
|
+
...(value.requestId === undefined ? {} : { requestId: value.requestId }),
|
|
92
|
+
...(value.sessionId === undefined ? {} : { sessionId: value.sessionId }),
|
|
93
|
+
...(Object.hasOwn(value, 'payload') ? { payload: value.payload } : {}),
|
|
94
|
+
...(value.message === undefined ? {} : { message: String(value.message).slice(0, 256) }),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
function terminateWorkerTree(child) {
|
|
98
|
+
const pid = child.pid;
|
|
99
|
+
if (pid === undefined) {
|
|
100
|
+
child.kill();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (process.platform === 'win32') {
|
|
104
|
+
const killer = spawn('taskkill', ['/pid', String(pid), '/t', '/f'], {
|
|
105
|
+
shell: false,
|
|
106
|
+
windowsHide: true,
|
|
107
|
+
stdio: 'ignore',
|
|
108
|
+
});
|
|
109
|
+
const fallback = setTimeout(() => {
|
|
110
|
+
try {
|
|
111
|
+
child.kill();
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// The child may have exited while taskkill was traversing the tree.
|
|
115
|
+
}
|
|
116
|
+
}, 250);
|
|
117
|
+
try {
|
|
118
|
+
// Ask the direct child to close immediately so the parent can finish
|
|
119
|
+
// its lifecycle without waiting for taskkill's process to exit.
|
|
120
|
+
child.kill();
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// taskkill remains responsible for descendants and the close handler
|
|
124
|
+
// still completes the worker state transition.
|
|
125
|
+
}
|
|
126
|
+
killer.once('error', () => {
|
|
127
|
+
clearTimeout(fallback);
|
|
128
|
+
try {
|
|
129
|
+
child.kill();
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// The child may already be closed.
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
killer.once('close', () => clearTimeout(fallback));
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
process.kill(-pid, 'SIGKILL');
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
child.kill('SIGKILL');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function errorFromUnknown(error, fallbackCode, fallbackMessage) {
|
|
146
|
+
if (error instanceof PersistentTransportError)
|
|
147
|
+
return error;
|
|
148
|
+
return new PersistentTransportError(fallbackMessage, fallbackCode, {
|
|
149
|
+
cause: error instanceof Error ? error : new Error(String(error)),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
class PersistentAgyWorker {
|
|
153
|
+
options;
|
|
154
|
+
sessionId;
|
|
155
|
+
onStopped;
|
|
156
|
+
state = 'starting';
|
|
157
|
+
child;
|
|
158
|
+
active;
|
|
159
|
+
idleTimer;
|
|
160
|
+
startPromise;
|
|
161
|
+
closePromise = Promise.resolve();
|
|
162
|
+
closeResolve;
|
|
163
|
+
closed = true;
|
|
164
|
+
notifiedStopped = false;
|
|
165
|
+
stdoutBuffer = '';
|
|
166
|
+
stderrBytes = 0;
|
|
167
|
+
readyResolve;
|
|
168
|
+
readyReject;
|
|
169
|
+
readySettled = false;
|
|
170
|
+
constructor(sessionId, options, onStopped) {
|
|
171
|
+
this.sessionId = sessionId;
|
|
172
|
+
this.options = options;
|
|
173
|
+
this.onStopped = onStopped;
|
|
174
|
+
}
|
|
175
|
+
getState() {
|
|
176
|
+
return this.state;
|
|
177
|
+
}
|
|
178
|
+
async start() {
|
|
179
|
+
if (this.state === 'stopped' || this.state === 'stopping') {
|
|
180
|
+
throw new PersistentTransportError('Persistent AGY worker is stopped', 'WORKER_STOPPED');
|
|
181
|
+
}
|
|
182
|
+
if (this.state === 'idle' || this.state === 'busy')
|
|
183
|
+
return;
|
|
184
|
+
if (this.startPromise !== undefined)
|
|
185
|
+
return this.startPromise;
|
|
186
|
+
this.startPromise = this.spawnAndWaitForReady();
|
|
187
|
+
try {
|
|
188
|
+
await this.startPromise;
|
|
189
|
+
}
|
|
190
|
+
finally {
|
|
191
|
+
this.startPromise = undefined;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
async request(input) {
|
|
195
|
+
await this.start();
|
|
196
|
+
if (this.state !== 'idle') {
|
|
197
|
+
throw new PersistentTransportError('Persistent AGY worker already has an active request', 'WORKER_BUSY');
|
|
198
|
+
}
|
|
199
|
+
if (input.signal?.aborted) {
|
|
200
|
+
await this.stop('reset');
|
|
201
|
+
throw new PersistentTransportError('Persistent AGY request aborted before write', 'ABORTED');
|
|
202
|
+
}
|
|
203
|
+
const timeoutMs = input.timeoutMs ?? this.options.defaultRequestTimeoutMs;
|
|
204
|
+
validateInteger('timeoutMs', timeoutMs, 1, 3_600_000);
|
|
205
|
+
const maxOutputBytes = Math.min(input.maxOutputBytes ?? this.options.maxOutputBytes, this.options.maxOutputBytes);
|
|
206
|
+
validateInteger('maxOutputBytes', maxOutputBytes, 1, this.options.maxOutputBytes);
|
|
207
|
+
this.clearIdleTimer();
|
|
208
|
+
this.state = 'busy';
|
|
209
|
+
return new Promise((resolve, reject) => {
|
|
210
|
+
const active = {
|
|
211
|
+
requestId: randomUUID(),
|
|
212
|
+
sessionId: this.sessionId,
|
|
213
|
+
startedAt: Date.now(),
|
|
214
|
+
maxOutputBytes,
|
|
215
|
+
events: [],
|
|
216
|
+
onEvent: input.onEvent,
|
|
217
|
+
signal: input.signal,
|
|
218
|
+
resolve,
|
|
219
|
+
reject,
|
|
220
|
+
outputBytes: 0,
|
|
221
|
+
firstEventAt: undefined,
|
|
222
|
+
timer: undefined,
|
|
223
|
+
abortListener: undefined,
|
|
224
|
+
};
|
|
225
|
+
this.active = active;
|
|
226
|
+
active.abortListener = () => this.failActiveAndReset(new PersistentTransportError('Persistent AGY request aborted', 'ABORTED'));
|
|
227
|
+
input.signal?.addEventListener('abort', active.abortListener, { once: true });
|
|
228
|
+
active.timer = setTimeout(() => this.failActiveAndReset(new PersistentTransportError('Persistent AGY request exceeded its timeout', 'TIMEOUT')), timeoutMs);
|
|
229
|
+
void this.sendRequest(active, input.payload);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
async stop(reason = 'dispose') {
|
|
233
|
+
if (this.state === 'stopped')
|
|
234
|
+
return;
|
|
235
|
+
if (this.state === 'stopping') {
|
|
236
|
+
await this.closePromise;
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
this.clearIdleTimer();
|
|
240
|
+
if (this.state === 'starting') {
|
|
241
|
+
this.rejectReady(reason === 'dispose'
|
|
242
|
+
? new PersistentTransportError('Persistent AGY transport was disposed during worker startup', 'DISPOSED')
|
|
243
|
+
: new PersistentTransportError('Persistent AGY worker stopped before startup completed', 'WORKER_STOPPED'));
|
|
244
|
+
}
|
|
245
|
+
if (this.active !== undefined) {
|
|
246
|
+
this.settleActiveError(new PersistentTransportError(reason === 'dispose'
|
|
247
|
+
? 'Persistent AGY transport was disposed'
|
|
248
|
+
: 'Persistent AGY worker was stopped before request completion', reason === 'dispose' ? 'DISPOSED' : 'WORKER_STOPPED'));
|
|
249
|
+
}
|
|
250
|
+
this.state = 'stopping';
|
|
251
|
+
const child = this.child;
|
|
252
|
+
if (child === undefined || this.closed) {
|
|
253
|
+
this.state = 'stopped';
|
|
254
|
+
this.notifyStopped();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (reason === 'dispose' || reason === 'idle') {
|
|
258
|
+
try {
|
|
259
|
+
child.stdin.end(encodePersistentFrame({ kind: 'shutdown' }, this.options.maxFrameBytes));
|
|
260
|
+
}
|
|
261
|
+
catch {
|
|
262
|
+
// The close handler below still performs the final state transition.
|
|
263
|
+
}
|
|
264
|
+
await Promise.race([this.closePromise, delay(this.options.shutdownTimeoutMs)]);
|
|
265
|
+
}
|
|
266
|
+
if (!this.closed)
|
|
267
|
+
terminateWorkerTree(child);
|
|
268
|
+
await this.closePromise;
|
|
269
|
+
}
|
|
270
|
+
async spawnAndWaitForReady() {
|
|
271
|
+
this.state = 'starting';
|
|
272
|
+
this.closed = false;
|
|
273
|
+
this.stdoutBuffer = '';
|
|
274
|
+
this.stderrBytes = 0;
|
|
275
|
+
this.readySettled = false;
|
|
276
|
+
this.closePromise = new Promise(resolve => { this.closeResolve = resolve; });
|
|
277
|
+
const readyPromise = new Promise((resolve, reject) => {
|
|
278
|
+
this.readyResolve = resolve;
|
|
279
|
+
this.readyReject = reject;
|
|
280
|
+
});
|
|
281
|
+
try {
|
|
282
|
+
const child = spawn(this.options.executable, [...this.options.args], {
|
|
283
|
+
cwd: this.options.cwd,
|
|
284
|
+
env: {
|
|
285
|
+
...process.env,
|
|
286
|
+
...this.options.env,
|
|
287
|
+
DSH_AGY_EXPERIMENTAL_SESSION: this.sessionId,
|
|
288
|
+
},
|
|
289
|
+
shell: false,
|
|
290
|
+
detached: process.platform !== 'win32',
|
|
291
|
+
windowsHide: true,
|
|
292
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
293
|
+
});
|
|
294
|
+
this.child = child;
|
|
295
|
+
this.attachChild(child);
|
|
296
|
+
const timeout = setTimeout(() => {
|
|
297
|
+
this.rejectReady(new PersistentTransportError('Persistent AGY worker did not become ready in time', 'WORKER_START_FAILED'));
|
|
298
|
+
}, this.options.readyTimeoutMs);
|
|
299
|
+
try {
|
|
300
|
+
await readyPromise;
|
|
301
|
+
}
|
|
302
|
+
finally {
|
|
303
|
+
clearTimeout(timeout);
|
|
304
|
+
}
|
|
305
|
+
const stateAfterReady = this.getState();
|
|
306
|
+
if (stateAfterReady === 'stopped' || stateAfterReady === 'stopping') {
|
|
307
|
+
throw new PersistentTransportError('Persistent AGY worker stopped during startup', 'WORKER_START_FAILED');
|
|
308
|
+
}
|
|
309
|
+
this.state = 'idle';
|
|
310
|
+
this.armIdleTimer();
|
|
311
|
+
}
|
|
312
|
+
catch (error) {
|
|
313
|
+
const mapped = errorFromUnknown(error, 'WORKER_START_FAILED', 'Persistent AGY worker failed to start');
|
|
314
|
+
await this.stop('reset');
|
|
315
|
+
throw mapped;
|
|
316
|
+
}
|
|
317
|
+
finally {
|
|
318
|
+
this.readyResolve = undefined;
|
|
319
|
+
this.readyReject = undefined;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
attachChild(child) {
|
|
323
|
+
child.stdout.setEncoding('utf8');
|
|
324
|
+
child.stderr.setEncoding('utf8');
|
|
325
|
+
child.stdout.on('data', (chunk) => this.handleStdout(chunk));
|
|
326
|
+
child.stderr.on('data', (chunk) => {
|
|
327
|
+
this.stderrBytes += byteLength(chunk);
|
|
328
|
+
if (this.stderrBytes > this.options.maxStderrBytes) {
|
|
329
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker stderr exceeded its capture limit', 'OUTPUT_LIMIT'));
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
child.once('error', error => {
|
|
333
|
+
const code = this.state === 'starting'
|
|
334
|
+
? 'WORKER_START_FAILED'
|
|
335
|
+
: 'WORKER_CRASHED';
|
|
336
|
+
const mapped = new PersistentTransportError(code === 'WORKER_START_FAILED'
|
|
337
|
+
? 'Persistent AGY worker could not be started'
|
|
338
|
+
: 'Persistent AGY worker process failed', code, { cause: error });
|
|
339
|
+
this.rejectReady(mapped);
|
|
340
|
+
if (this.active !== undefined)
|
|
341
|
+
this.settleActiveError(mapped);
|
|
342
|
+
});
|
|
343
|
+
child.once('close', (exitCode, signal) => {
|
|
344
|
+
this.closed = true;
|
|
345
|
+
this.clearIdleTimer();
|
|
346
|
+
if (!this.readySettled) {
|
|
347
|
+
this.rejectReady(new PersistentTransportError('Persistent AGY worker exited before ready', 'WORKER_START_FAILED'));
|
|
348
|
+
}
|
|
349
|
+
if (this.active !== undefined) {
|
|
350
|
+
this.settleActiveError(new PersistentTransportError(`Persistent AGY worker exited before request completion (${exitCode ?? 'signal'})`, 'WORKER_CRASHED', { cause: signal === null ? undefined : new Error(signal) }));
|
|
351
|
+
}
|
|
352
|
+
this.state = 'stopped';
|
|
353
|
+
this.closeResolve?.();
|
|
354
|
+
this.closeResolve = undefined;
|
|
355
|
+
this.notifyStopped();
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
handleStdout(chunk) {
|
|
359
|
+
this.stdoutBuffer += chunk;
|
|
360
|
+
// Same runaway-line guard as the persistent transport: the byte valve only
|
|
361
|
+
// runs on completed lines, so an unsplit endless line would grow freely.
|
|
362
|
+
if (this.stdoutBuffer.length > this.options.maxFrameBytes) {
|
|
363
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY emitted a stdout line beyond the frame limit', 'OUTPUT_LIMIT'));
|
|
364
|
+
this.stdoutBuffer = '';
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
let newlineIndex = this.stdoutBuffer.indexOf('\n');
|
|
368
|
+
while (newlineIndex >= 0) {
|
|
369
|
+
const rawLine = this.stdoutBuffer.slice(0, newlineIndex);
|
|
370
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
371
|
+
this.handleLine(rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine);
|
|
372
|
+
newlineIndex = this.stdoutBuffer.indexOf('\n');
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
handleLine(line) {
|
|
376
|
+
let frame;
|
|
377
|
+
try {
|
|
378
|
+
frame = parsePersistentFrame(line);
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
this.failActiveAndReset(errorFromUnknown(error, 'PROTOCOL_ERROR', 'Persistent AGY worker emitted an invalid frame'));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (frame.kind === 'ready') {
|
|
385
|
+
if (frame.sessionId !== undefined && frame.sessionId !== this.sessionId) {
|
|
386
|
+
this.rejectReady(new PersistentTransportError('Persistent AGY worker ready frame belongs to another session', 'PROTOCOL_ERROR'));
|
|
387
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker session ownership check failed', 'PROTOCOL_ERROR'));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
this.resolveReady();
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const active = this.active;
|
|
394
|
+
if (active === undefined) {
|
|
395
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker emitted a response without an active request', 'PROTOCOL_ERROR'));
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
active.outputBytes += byteLength(`${line}\n`);
|
|
399
|
+
if (active.outputBytes > active.maxOutputBytes) {
|
|
400
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker response exceeded its capture limit', 'OUTPUT_LIMIT'));
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
if (frame.requestId !== active.requestId || frame.sessionId !== this.sessionId) {
|
|
404
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker response correlation failed', 'PROTOCOL_ERROR'));
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
if (frame.kind === 'event') {
|
|
408
|
+
active.firstEventAt ??= Date.now();
|
|
409
|
+
active.events.push(frame.payload);
|
|
410
|
+
try {
|
|
411
|
+
active.onEvent?.(frame.payload);
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker event handler failed', 'EVENT_HANDLER_FAILED', { cause: error instanceof Error ? error : new Error(String(error)) }));
|
|
415
|
+
}
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (frame.kind === 'error') {
|
|
419
|
+
this.failActiveAndReset(new PersistentTransportError('Persistent AGY worker returned an error frame', 'WORKER_ERROR'));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
this.settleActiveSuccess();
|
|
423
|
+
}
|
|
424
|
+
async sendRequest(active, payload) {
|
|
425
|
+
try {
|
|
426
|
+
const frame = encodePersistentFrame({
|
|
427
|
+
kind: 'request',
|
|
428
|
+
requestId: active.requestId,
|
|
429
|
+
sessionId: active.sessionId,
|
|
430
|
+
payload,
|
|
431
|
+
}, this.options.maxFrameBytes);
|
|
432
|
+
await this.writeFrame(frame);
|
|
433
|
+
}
|
|
434
|
+
catch (error) {
|
|
435
|
+
if (this.active !== active)
|
|
436
|
+
return;
|
|
437
|
+
this.failActiveAndReset(errorFromUnknown(error, 'PROTOCOL_ERROR', 'Persistent AGY request could not be written'));
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
async writeFrame(frame) {
|
|
441
|
+
const stdin = this.child?.stdin;
|
|
442
|
+
if (stdin === undefined || !stdin.writable) {
|
|
443
|
+
throw new PersistentTransportError('Persistent AGY worker stdin is closed', 'WORKER_STOPPED');
|
|
444
|
+
}
|
|
445
|
+
if (stdin.write(frame))
|
|
446
|
+
return;
|
|
447
|
+
await new Promise((resolve, reject) => {
|
|
448
|
+
const onDrain = () => {
|
|
449
|
+
cleanup();
|
|
450
|
+
resolve();
|
|
451
|
+
};
|
|
452
|
+
const onError = (error) => {
|
|
453
|
+
cleanup();
|
|
454
|
+
reject(error);
|
|
455
|
+
};
|
|
456
|
+
const cleanup = () => {
|
|
457
|
+
stdin.removeListener('drain', onDrain);
|
|
458
|
+
stdin.removeListener('error', onError);
|
|
459
|
+
};
|
|
460
|
+
stdin.once('drain', onDrain);
|
|
461
|
+
stdin.once('error', onError);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
settleActiveSuccess() {
|
|
465
|
+
const active = this.active;
|
|
466
|
+
if (active === undefined)
|
|
467
|
+
return;
|
|
468
|
+
this.active = undefined;
|
|
469
|
+
this.clearActive(active);
|
|
470
|
+
this.state = 'idle';
|
|
471
|
+
this.armIdleTimer();
|
|
472
|
+
active.resolve({
|
|
473
|
+
requestId: active.requestId,
|
|
474
|
+
sessionId: active.sessionId,
|
|
475
|
+
events: [...active.events],
|
|
476
|
+
durationMs: Date.now() - active.startedAt,
|
|
477
|
+
...(active.firstEventAt === undefined ? {} : { firstEventMs: active.firstEventAt - active.startedAt }),
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
settleActiveError(error) {
|
|
481
|
+
const active = this.active;
|
|
482
|
+
if (active === undefined)
|
|
483
|
+
return;
|
|
484
|
+
this.active = undefined;
|
|
485
|
+
this.clearActive(active);
|
|
486
|
+
active.reject(error);
|
|
487
|
+
}
|
|
488
|
+
clearActive(active) {
|
|
489
|
+
if (active.timer !== undefined)
|
|
490
|
+
clearTimeout(active.timer);
|
|
491
|
+
if (active.abortListener !== undefined)
|
|
492
|
+
active.signal?.removeEventListener('abort', active.abortListener);
|
|
493
|
+
active.abortListener = undefined;
|
|
494
|
+
}
|
|
495
|
+
failActiveAndReset(error) {
|
|
496
|
+
this.settleActiveError(error);
|
|
497
|
+
void this.stop('reset');
|
|
498
|
+
}
|
|
499
|
+
resolveReady() {
|
|
500
|
+
if (this.readySettled)
|
|
501
|
+
return;
|
|
502
|
+
this.readySettled = true;
|
|
503
|
+
this.readyResolve?.();
|
|
504
|
+
}
|
|
505
|
+
rejectReady(error) {
|
|
506
|
+
if (this.readySettled)
|
|
507
|
+
return;
|
|
508
|
+
this.readySettled = true;
|
|
509
|
+
this.readyReject?.(error);
|
|
510
|
+
}
|
|
511
|
+
armIdleTimer() {
|
|
512
|
+
this.clearIdleTimer();
|
|
513
|
+
if (this.options.idleTtlMs === 0 || this.state !== 'idle')
|
|
514
|
+
return;
|
|
515
|
+
this.idleTimer = setTimeout(() => { void this.stop('idle'); }, this.options.idleTtlMs);
|
|
516
|
+
}
|
|
517
|
+
clearIdleTimer() {
|
|
518
|
+
if (this.idleTimer !== undefined)
|
|
519
|
+
clearTimeout(this.idleTimer);
|
|
520
|
+
this.idleTimer = undefined;
|
|
521
|
+
}
|
|
522
|
+
notifyStopped() {
|
|
523
|
+
if (this.notifiedStopped)
|
|
524
|
+
return;
|
|
525
|
+
this.notifiedStopped = true;
|
|
526
|
+
this.onStopped(this);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Experimental worker-per-session transport. It is intentionally not wired
|
|
531
|
+
* into AgyAdapter; callers must opt into this prototype directly.
|
|
532
|
+
*/
|
|
533
|
+
export class ExperimentalAgyTransport {
|
|
534
|
+
options;
|
|
535
|
+
workers = new Map();
|
|
536
|
+
disposed = false;
|
|
537
|
+
constructor(options) {
|
|
538
|
+
this.options = normalizeOptions(options);
|
|
539
|
+
}
|
|
540
|
+
async request(input) {
|
|
541
|
+
if (this.disposed) {
|
|
542
|
+
throw new PersistentTransportError('Persistent AGY transport is disposed', 'DISPOSED');
|
|
543
|
+
}
|
|
544
|
+
if (input.sessionId.trim().length === 0) {
|
|
545
|
+
throw new RangeError('sessionId must not be empty');
|
|
546
|
+
}
|
|
547
|
+
let worker = this.workers.get(input.sessionId);
|
|
548
|
+
if (worker?.getState() === 'stopped') {
|
|
549
|
+
this.workers.delete(input.sessionId);
|
|
550
|
+
worker = undefined;
|
|
551
|
+
}
|
|
552
|
+
if (worker === undefined) {
|
|
553
|
+
if (this.workers.size >= this.options.maxWorkers) {
|
|
554
|
+
throw new PersistentTransportError('Persistent AGY worker limit reached', 'WORKER_LIMIT');
|
|
555
|
+
}
|
|
556
|
+
worker = new PersistentAgyWorker(input.sessionId, this.options, stoppedWorker => {
|
|
557
|
+
if (this.workers.get(input.sessionId) === stoppedWorker)
|
|
558
|
+
this.workers.delete(input.sessionId);
|
|
559
|
+
});
|
|
560
|
+
this.workers.set(input.sessionId, worker);
|
|
561
|
+
}
|
|
562
|
+
try {
|
|
563
|
+
return await worker.request(input);
|
|
564
|
+
}
|
|
565
|
+
catch (error) {
|
|
566
|
+
if (worker.getState() === 'stopped')
|
|
567
|
+
this.workers.delete(input.sessionId);
|
|
568
|
+
throw error;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
async dispose() {
|
|
572
|
+
if (this.disposed)
|
|
573
|
+
return;
|
|
574
|
+
this.disposed = true;
|
|
575
|
+
const workers = [...this.workers.values()];
|
|
576
|
+
await Promise.all(workers.map(worker => worker.stop('dispose')));
|
|
577
|
+
this.workers.clear();
|
|
578
|
+
}
|
|
579
|
+
getStats() {
|
|
580
|
+
let startingWorkers = 0;
|
|
581
|
+
let idleWorkers = 0;
|
|
582
|
+
let busyWorkers = 0;
|
|
583
|
+
let stoppingWorkers = 0;
|
|
584
|
+
for (const worker of this.workers.values()) {
|
|
585
|
+
switch (worker.getState()) {
|
|
586
|
+
case 'starting':
|
|
587
|
+
startingWorkers += 1;
|
|
588
|
+
break;
|
|
589
|
+
case 'idle':
|
|
590
|
+
idleWorkers += 1;
|
|
591
|
+
break;
|
|
592
|
+
case 'busy':
|
|
593
|
+
busyWorkers += 1;
|
|
594
|
+
break;
|
|
595
|
+
case 'stopping':
|
|
596
|
+
stoppingWorkers += 1;
|
|
597
|
+
break;
|
|
598
|
+
case 'stopped': break;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return {
|
|
602
|
+
totalWorkers: this.workers.size,
|
|
603
|
+
startingWorkers,
|
|
604
|
+
idleWorkers,
|
|
605
|
+
busyWorkers,
|
|
606
|
+
stoppingWorkers,
|
|
607
|
+
maxWorkers: this.options.maxWorkers,
|
|
608
|
+
disposed: this.disposed,
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
//# sourceMappingURL=experimental-transport.js.map
|