nexus-agentd 0.2.5 → 0.2.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/LICENSE +22 -22
- package/README.md +270 -250
- package/dist/a2a/runtime.d.ts +3 -0
- package/dist/a2a/runtime.js +80 -10
- package/dist/acp/runtime.d.ts +5 -1
- package/dist/acp/runtime.js +75 -33
- package/dist/completion-contract.js +1 -1
- package/dist/control-plane.js +11 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +23 -5
- package/dist/run-store.d.ts +5 -0
- package/dist/run-store.js +10 -1
- package/dist/session-contract.d.ts +1 -0
- package/dist/session.d.ts +6 -1
- package/dist/session.js +79 -52
- package/dist/webui/app/sources.js +10 -10
- package/dist/webui/markup.js +67 -66
- package/dist/webui/styles.js +746 -616
- package/dist/workspace.d.ts +30 -0
- package/dist/workspace.js +67 -2
- package/nexus-agentd.example.json +40 -40
- package/package.json +4 -2
package/dist/session.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { realpath } from 'node:fs/promises';
|
|
3
|
+
import { extname } from 'node:path';
|
|
4
4
|
import { AcpProcessRuntime } from './acp/runtime.js';
|
|
5
5
|
import { A2AClientRuntime, probeA2AAgent } from './a2a/runtime.js';
|
|
6
6
|
import { SessionEventLog } from './events.js';
|
|
7
|
+
import { ContainedReadError, readContainedFile } from './workspace.js';
|
|
7
8
|
const MAX_SESSION_ARTIFACTS = 64;
|
|
8
9
|
const MAX_ARTIFACT_BASE64_CHARS = 16 * 1024 * 1024;
|
|
9
10
|
const MAX_SESSION_BASE64_CHARS = 32 * 1024 * 1024;
|
|
@@ -193,40 +194,21 @@ export class ManagedSession {
|
|
|
193
194
|
return artifacts;
|
|
194
195
|
}
|
|
195
196
|
async publishSingleFile(requestedPath) {
|
|
196
|
-
let handle;
|
|
197
197
|
try {
|
|
198
198
|
const workspaceRoot = await realpath(this.workspace);
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
handle = await open(target, 'r');
|
|
203
|
-
// Resolve again after opening so a swapped symlink cannot silently
|
|
204
|
-
// redirect a publish request outside the configured workspace.
|
|
205
|
-
const verifiedTarget = await realpath(unresolved);
|
|
206
|
-
assertWorkspacePath(workspaceRoot, verifiedTarget);
|
|
207
|
-
if (verifiedTarget !== target) {
|
|
208
|
-
throw new SessionRequestError(409, 'Published file changed while it was being opened');
|
|
209
|
-
}
|
|
210
|
-
const stats = await handle.stat();
|
|
211
|
-
if (!stats.isFile()) {
|
|
212
|
-
throw new SessionRequestError(400, 'Only regular files can be published');
|
|
213
|
-
}
|
|
214
|
-
if (stats.size > MAX_PUBLISHED_FILE_BYTES) {
|
|
215
|
-
throw new SessionRequestError(413, `Published file exceeds the ${MAX_PUBLISHED_FILE_BYTES} byte limit`);
|
|
216
|
-
}
|
|
217
|
-
const bytes = await handle.readFile();
|
|
218
|
-
const filename = basename(verifiedTarget);
|
|
219
|
-
const workspacePath = relative(workspaceRoot, verifiedTarget).split(sep).join('/');
|
|
199
|
+
const file = await readContainedFile([workspaceRoot], requestedPath, {
|
|
200
|
+
maxBytes: MAX_PUBLISHED_FILE_BYTES
|
|
201
|
+
});
|
|
220
202
|
const artifact = {
|
|
221
203
|
id: `published:${randomUUID()}`,
|
|
222
|
-
name: filename,
|
|
223
|
-
filename,
|
|
224
|
-
mediaType: mediaTypeForPath(filename),
|
|
225
|
-
bytesBase64: bytes.toString('base64'),
|
|
204
|
+
name: file.filename,
|
|
205
|
+
filename: file.filename,
|
|
206
|
+
mediaType: mediaTypeForPath(file.filename),
|
|
207
|
+
bytesBase64: file.bytes.toString('base64'),
|
|
226
208
|
metadata: {
|
|
227
209
|
source: 'workspace_publish',
|
|
228
|
-
path:
|
|
229
|
-
size:
|
|
210
|
+
path: file.relative,
|
|
211
|
+
size: file.size
|
|
230
212
|
}
|
|
231
213
|
};
|
|
232
214
|
this.addArtifact(artifact);
|
|
@@ -236,6 +218,8 @@ export class ManagedSession {
|
|
|
236
218
|
catch (error) {
|
|
237
219
|
if (error instanceof SessionRequestError)
|
|
238
220
|
throw error;
|
|
221
|
+
if (error instanceof ContainedReadError)
|
|
222
|
+
throw publishFailure(error);
|
|
239
223
|
const code = error?.code;
|
|
240
224
|
if (code === 'ENOENT') {
|
|
241
225
|
throw new SessionRequestError(404, 'Published file was not found');
|
|
@@ -245,9 +229,6 @@ export class ManagedSession {
|
|
|
245
229
|
}
|
|
246
230
|
throw error;
|
|
247
231
|
}
|
|
248
|
-
finally {
|
|
249
|
-
await handle?.close();
|
|
250
|
-
}
|
|
251
232
|
}
|
|
252
233
|
setPending(request) {
|
|
253
234
|
this.completion = undefined;
|
|
@@ -297,7 +278,7 @@ export class ManagedSession {
|
|
|
297
278
|
return attachmentView(attachment);
|
|
298
279
|
}
|
|
299
280
|
async message(message, attachmentIds = []) {
|
|
300
|
-
if (!this.runtime)
|
|
281
|
+
if (!this.runtime || this.runtime.isAvailable?.() === false)
|
|
301
282
|
throw new SessionRequestError(409, 'Agent runtime is unavailable');
|
|
302
283
|
const attachments = this.inputAttachmentsFor(attachmentIds);
|
|
303
284
|
if (this.pendingRequest) {
|
|
@@ -326,13 +307,14 @@ export class ManagedSession {
|
|
|
326
307
|
task: message,
|
|
327
308
|
inputAttachmentCount: attachments.length
|
|
328
309
|
}).id;
|
|
310
|
+
this.setState('running');
|
|
329
311
|
void this.runtime.prompt(message, attachments).catch((error) => {
|
|
330
312
|
if (this.state !== 'canceled')
|
|
331
313
|
this.setState('failed', errorMessage(error));
|
|
332
314
|
});
|
|
333
315
|
}
|
|
334
316
|
async resolvePending(response, attachmentIds = []) {
|
|
335
|
-
if (!this.runtime) {
|
|
317
|
+
if (!this.runtime || this.runtime.isAvailable?.() === false) {
|
|
336
318
|
throw new SessionRequestError(409, 'Agent runtime is unavailable');
|
|
337
319
|
}
|
|
338
320
|
const pending = this.pendingRequest;
|
|
@@ -349,11 +331,31 @@ export class ManagedSession {
|
|
|
349
331
|
await this.runtime.respondPending(response, attachments);
|
|
350
332
|
}
|
|
351
333
|
async cancel() {
|
|
352
|
-
|
|
334
|
+
const runtime = this.runtime;
|
|
335
|
+
this.runtime = undefined;
|
|
336
|
+
this.clearPending();
|
|
337
|
+
this.setState('canceled');
|
|
338
|
+
try {
|
|
339
|
+
await runtime?.cancel();
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
try {
|
|
343
|
+
await runtime?.dispose();
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
this.inputAttachments.clear();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
353
349
|
}
|
|
354
350
|
async dispose() {
|
|
355
|
-
|
|
356
|
-
this.
|
|
351
|
+
const runtime = this.runtime;
|
|
352
|
+
this.runtime = undefined;
|
|
353
|
+
try {
|
|
354
|
+
await runtime?.dispose();
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
this.inputAttachments.clear();
|
|
358
|
+
}
|
|
357
359
|
}
|
|
358
360
|
snapshot() {
|
|
359
361
|
return {
|
|
@@ -535,7 +537,7 @@ export class SessionManager {
|
|
|
535
537
|
const session = new ManagedSession(agentId, config.protocol === 'a2a' ? 'a2a' : 'acp', workspace, ownerKeyId, this.config.maxEventsPerSession, this.config.maxOutputChars, config.name || agentId, this.runStore, this.instanceId);
|
|
536
538
|
const runtime = config.protocol === 'a2a'
|
|
537
539
|
? new A2AClientRuntime(config, session, this.config.promptTimeoutMs || 30 * 60_000)
|
|
538
|
-
: new AcpProcessRuntime(driver, session, this.config.maxOutputChars, this.config.promptTimeoutMs || 30 * 60_000);
|
|
540
|
+
: new AcpProcessRuntime(driver, session, this.config.maxOutputChars, this.config.promptTimeoutMs || 30 * 60_000, this.workspacePolicy.listRoots());
|
|
539
541
|
this.sessions.set(session.id, session);
|
|
540
542
|
session.attach(runtime);
|
|
541
543
|
try {
|
|
@@ -544,8 +546,15 @@ export class SessionManager {
|
|
|
544
546
|
}
|
|
545
547
|
catch (error) {
|
|
546
548
|
session.setState('failed', errorMessage(error));
|
|
547
|
-
|
|
548
|
-
|
|
549
|
+
try {
|
|
550
|
+
await session.dispose();
|
|
551
|
+
}
|
|
552
|
+
catch (cleanupError) {
|
|
553
|
+
console.error(JSON.stringify({ level: 'error', event: 'session_start_cleanup_failed', sessionId: session.id, message: errorMessage(cleanupError) }));
|
|
554
|
+
}
|
|
555
|
+
finally {
|
|
556
|
+
this.sessions.delete(session.id);
|
|
557
|
+
}
|
|
549
558
|
throw new SessionRequestError(502, `Agent failed to start: ${errorMessage(error)}`);
|
|
550
559
|
}
|
|
551
560
|
}
|
|
@@ -559,7 +568,7 @@ export class SessionManager {
|
|
|
559
568
|
return this.sessions.size;
|
|
560
569
|
}
|
|
561
570
|
listRuns(query = {}) {
|
|
562
|
-
return this.runStore?.list(query) || { runs: [], total: 0 };
|
|
571
|
+
return this.runStore?.list(query) || { runs: [], total: 0, stats: { active: 0, completed: 0, failed: 0 } };
|
|
563
572
|
}
|
|
564
573
|
getRun(id) {
|
|
565
574
|
const run = this.runStore?.get(id);
|
|
@@ -678,17 +687,29 @@ export class SessionManager {
|
|
|
678
687
|
for (const [id, session] of this.sessions) {
|
|
679
688
|
if (session.updatedAt > cutoff)
|
|
680
689
|
continue;
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
690
|
+
await this.withSessionLock(id, async () => {
|
|
691
|
+
if (this.sessions.get(id) !== session || session.updatedAt > cutoff)
|
|
692
|
+
return;
|
|
693
|
+
if (!isTerminal(session.state))
|
|
694
|
+
session.setState('failed', 'Session expired after exceeding its lifetime');
|
|
695
|
+
try {
|
|
696
|
+
await session.dispose();
|
|
697
|
+
}
|
|
698
|
+
catch (error) {
|
|
699
|
+
console.error(JSON.stringify({ level: 'error', event: 'session_cleanup_failed', sessionId: id, message: errorMessage(error) }));
|
|
700
|
+
}
|
|
701
|
+
finally {
|
|
702
|
+
this.sessions.delete(id);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
686
705
|
}
|
|
687
706
|
}
|
|
688
707
|
restartCleanup() {
|
|
689
708
|
if (this.cleanupTimer)
|
|
690
709
|
clearInterval(this.cleanupTimer);
|
|
691
|
-
this.cleanupTimer = setInterval(() => void this.cleanup()
|
|
710
|
+
this.cleanupTimer = setInterval(() => void this.cleanup().catch((error) => {
|
|
711
|
+
console.error(JSON.stringify({ level: 'error', event: 'session_cleanup_failed', message: errorMessage(error) }));
|
|
712
|
+
}), this.config.cleanupIntervalMs || 60_000);
|
|
692
713
|
this.cleanupTimer.unref?.();
|
|
693
714
|
}
|
|
694
715
|
defaultWorkspace(agentId) {
|
|
@@ -746,10 +767,16 @@ function runArtifact(artifact) {
|
|
|
746
767
|
metadata: artifact.metadata ? structuredClone(artifact.metadata) : undefined
|
|
747
768
|
};
|
|
748
769
|
}
|
|
749
|
-
function
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
770
|
+
function publishFailure(error) {
|
|
771
|
+
switch (error.failure) {
|
|
772
|
+
case 'swapped':
|
|
773
|
+
return new SessionRequestError(409, 'Published file changed while it was being opened');
|
|
774
|
+
case 'not-a-file':
|
|
775
|
+
return new SessionRequestError(400, 'Only regular files can be published');
|
|
776
|
+
case 'too-large':
|
|
777
|
+
return new SessionRequestError(413, `Published file exceeds the ${MAX_PUBLISHED_FILE_BYTES} byte limit`);
|
|
778
|
+
case 'outside':
|
|
779
|
+
return new SessionRequestError(403, 'Published files must stay inside the session workspace');
|
|
753
780
|
}
|
|
754
781
|
}
|
|
755
782
|
function mediaTypeForPath(path) {
|