nexus-agentd 0.1.0 → 0.1.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.
Files changed (44) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +351 -179
  3. package/dist/a2a/runtime.d.ts +30 -0
  4. package/dist/a2a/runtime.js +374 -0
  5. package/dist/acp/runtime.d.ts +12 -3
  6. package/dist/acp/runtime.js +248 -17
  7. package/dist/artifact-store.d.ts +41 -0
  8. package/dist/artifact-store.js +340 -0
  9. package/dist/auth.d.ts +13 -0
  10. package/dist/auth.js +77 -0
  11. package/dist/cli.js +58 -1
  12. package/dist/config.d.ts +10 -0
  13. package/dist/config.js +281 -53
  14. package/dist/control-plane.d.ts +91 -0
  15. package/dist/control-plane.js +629 -0
  16. package/dist/drivers/hermes.d.ts +2 -0
  17. package/dist/drivers/hermes.js +10 -0
  18. package/dist/drivers/index.js +6 -0
  19. package/dist/events.js +13 -2
  20. package/dist/index.d.ts +6 -0
  21. package/dist/index.js +15 -3
  22. package/dist/run-store.d.ts +49 -0
  23. package/dist/run-store.js +291 -0
  24. package/dist/server.d.ts +3 -2
  25. package/dist/server.js +911 -41
  26. package/dist/session-contract.d.ts +16 -4
  27. package/dist/session.d.ts +74 -8
  28. package/dist/session.js +433 -38
  29. package/dist/types.d.ts +193 -6
  30. package/dist/types.js +2 -1
  31. package/dist/webui/app.d.ts +1 -0
  32. package/dist/webui/app.js +1072 -0
  33. package/dist/webui/icons.d.ts +9 -0
  34. package/dist/webui/icons.js +10 -0
  35. package/dist/webui/index.d.ts +3 -0
  36. package/dist/webui/index.js +32 -0
  37. package/dist/webui/markup.d.ts +1 -0
  38. package/dist/webui/markup.js +68 -0
  39. package/dist/webui/styles.d.ts +1 -0
  40. package/dist/webui/styles.js +269 -0
  41. package/dist/workspace-files.d.ts +48 -0
  42. package/dist/workspace-files.js +269 -0
  43. package/nexus-agentd.example.json +30 -27
  44. package/package.json +6 -6
@@ -1,5 +1,8 @@
1
1
  import { randomUUID } from 'node:crypto';
2
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
2
4
  import { Readable, Writable } from 'node:stream';
5
+ import { pathToFileURL } from 'node:url';
3
6
  import * as acp from '@agentclientprotocol/sdk';
4
7
  export class AcpProcessRuntime {
5
8
  driver;
@@ -10,13 +13,23 @@ export class AcpProcessRuntime {
10
13
  pendingInput;
11
14
  disposed = false;
12
15
  prompting = false;
13
- constructor(driver, sink) {
16
+ maxStderrChunkChars;
17
+ promptTimeoutMs;
18
+ promptCapabilities = {};
19
+ inputDirectory;
20
+ constructor(driver, sink, maxStderrChunkChars = 16 * 1024, promptTimeoutMs = 30 * 60 * 1000) {
14
21
  this.driver = driver;
15
22
  this.sink = sink;
23
+ const limit = Number.isFinite(maxStderrChunkChars)
24
+ ? Math.floor(maxStderrChunkChars)
25
+ : 16 * 1024;
26
+ this.maxStderrChunkChars = Math.max(1, Math.min(16 * 1024, limit));
27
+ this.promptTimeoutMs = Math.max(10_000, Math.trunc(promptTimeoutMs));
16
28
  }
17
29
  async start(workspace) {
18
30
  if (this.process)
19
31
  throw new Error('ACP runtime is already started');
32
+ this.inputDirectory = path.join(workspace, '.nexus-inputs', this.sink.id);
20
33
  const child = this.driver.spawn(workspace);
21
34
  this.process = child;
22
35
  this.captureStderr(child);
@@ -47,7 +60,7 @@ export class AcpProcessRuntime {
47
60
  },
48
61
  clientInfo: {
49
62
  name: 'nexus-agentd',
50
- version: '0.1.0'
63
+ version: '0.1.4'
51
64
  }
52
65
  });
53
66
  if (initialize.protocolVersion !== acp.PROTOCOL_VERSION) {
@@ -56,6 +69,7 @@ export class AcpProcessRuntime {
56
69
  text: `ACP negotiated protocol ${initialize.protocolVersion}`
57
70
  });
58
71
  }
72
+ this.promptCapabilities = initialize.agentCapabilities?.promptCapabilities || {};
59
73
  const session = await this.connection.agent.request(acp.methods.agent.session.new, {
60
74
  cwd: workspace,
61
75
  mcpServers: []
@@ -63,7 +77,7 @@ export class AcpProcessRuntime {
63
77
  this.sink.setAcpSessionId(String(session.sessionId));
64
78
  this.sink.setState('created');
65
79
  }
66
- async prompt(message) {
80
+ async prompt(message, attachments = []) {
67
81
  if (!this.connection)
68
82
  throw new Error('ACP runtime is not connected');
69
83
  if (this.prompting)
@@ -73,12 +87,13 @@ export class AcpProcessRuntime {
73
87
  this.sink.clearPending();
74
88
  this.sink.setState('running');
75
89
  try {
76
- const response = await this.connection.agent.request(acp.methods.agent.session.prompt, {
90
+ const response = await withTimeout(this.connection.agent.request(acp.methods.agent.session.prompt, {
77
91
  sessionId,
78
- prompt: [{ type: 'text', text: message }]
79
- });
80
- if (this.sink.state === 'canceled')
92
+ prompt: await this.promptBlocks(message, attachments)
93
+ }), this.promptTimeoutMs, 'ACP prompt timed out');
94
+ if (this.sink.state === 'canceled' || this.sink.state === 'failed') {
81
95
  return;
96
+ }
82
97
  if (response.stopReason === 'cancelled') {
83
98
  this.sink.setState('canceled');
84
99
  }
@@ -93,12 +108,14 @@ export class AcpProcessRuntime {
93
108
  if (this.sink.state !== 'canceled') {
94
109
  this.sink.setState('failed', error instanceof Error ? error.message : String(error));
95
110
  }
111
+ if (error instanceof PromptTimeoutError)
112
+ await this.dispose();
96
113
  }
97
114
  finally {
98
115
  this.prompting = false;
99
116
  }
100
117
  }
101
- async respondPending(message) {
118
+ async respondPending(message, _attachments = []) {
102
119
  if (this.pendingInput) {
103
120
  this.finishInput(message);
104
121
  return;
@@ -168,6 +185,10 @@ export class AcpProcessRuntime {
168
185
  }
169
186
  this.connection?.close();
170
187
  this.connection = undefined;
188
+ if (this.inputDirectory) {
189
+ await rm(this.inputDirectory, { recursive: true, force: true }).catch(() => undefined);
190
+ this.inputDirectory = undefined;
191
+ }
171
192
  const child = this.process;
172
193
  this.process = undefined;
173
194
  if (child && child.exitCode === null && child.signalCode === null) {
@@ -286,11 +307,15 @@ export class AcpProcessRuntime {
286
307
  content: update.content
287
308
  });
288
309
  }
310
+ else {
311
+ this.captureArtifact(update.content, `message:${String(update.messageId || 'assistant')}:0`);
312
+ }
289
313
  break;
290
314
  case 'agent_thought_chunk':
291
315
  this.sink.emit('thought_chunk', update);
292
316
  break;
293
317
  case 'tool_call':
318
+ this.captureToolArtifacts(String(update.toolCallId), update.content, update.title);
294
319
  this.sink.emit('tool_call', update);
295
320
  if (update.locations?.length) {
296
321
  this.sink.emit('file_activity', {
@@ -300,6 +325,7 @@ export class AcpProcessRuntime {
300
325
  }
301
326
  break;
302
327
  case 'tool_call_update':
328
+ this.captureToolArtifacts(String(update.toolCallId), update.content, update.title || update.name);
303
329
  this.sink.emit('tool_update', update);
304
330
  if (update.locations?.length) {
305
331
  this.sink.emit('file_activity', {
@@ -317,21 +343,57 @@ export class AcpProcessRuntime {
317
343
  break;
318
344
  }
319
345
  }
346
+ captureToolArtifacts(toolCallId, content, hint) {
347
+ content?.forEach((item, index) => {
348
+ if (item.type !== 'content')
349
+ return;
350
+ this.captureArtifact(item.content, `tool:${toolCallId}:${index}`, hint || undefined);
351
+ });
352
+ }
353
+ captureArtifact(content, id, hint) {
354
+ const artifact = artifactFromContent(content, id, hint);
355
+ if (artifact)
356
+ this.sink.addArtifact(artifact);
357
+ }
320
358
  captureStderr(child) {
321
359
  let buffer = '';
322
- child.stderr.on('data', (chunk) => {
323
- buffer += String(chunk);
360
+ let flushed = false;
361
+ const emit = (text) => {
362
+ if (!text)
363
+ return;
364
+ for (let offset = 0; offset < text.length; offset += this.maxStderrChunkChars) {
365
+ this.sink.emit('terminal_output', {
366
+ stream: 'stderr',
367
+ text: text.slice(offset, offset + this.maxStderrChunkChars)
368
+ });
369
+ }
370
+ };
371
+ const drain = (flush = false) => {
324
372
  const lines = buffer.split(/\r?\n/);
325
373
  buffer = lines.pop() || '';
326
- for (const line of lines) {
327
- if (line) {
328
- this.sink.emit('terminal_output', {
329
- stream: 'stderr',
330
- text: line
331
- });
332
- }
374
+ for (const line of lines)
375
+ emit(line);
376
+ while (buffer.length >= this.maxStderrChunkChars) {
377
+ emit(buffer.slice(0, this.maxStderrChunkChars));
378
+ buffer = buffer.slice(this.maxStderrChunkChars);
333
379
  }
380
+ if (flush && buffer) {
381
+ emit(buffer);
382
+ buffer = '';
383
+ }
384
+ };
385
+ const flush = () => {
386
+ if (flushed)
387
+ return;
388
+ flushed = true;
389
+ drain(true);
390
+ };
391
+ child.stderr.on('data', (chunk) => {
392
+ buffer += String(chunk);
393
+ drain();
334
394
  });
395
+ child.stderr.once('end', flush);
396
+ child.stderr.once('close', flush);
335
397
  }
336
398
  onProcessFailure(error) {
337
399
  if (this.disposed)
@@ -345,6 +407,175 @@ export class AcpProcessRuntime {
345
407
  }
346
408
  return value;
347
409
  }
410
+ async promptBlocks(message, attachments) {
411
+ const blocks = [{ type: 'text', text: message }];
412
+ for (const attachment of attachments) {
413
+ const mediaType = attachment.mediaType || 'application/octet-stream';
414
+ const data = attachment.bytes.toString('base64');
415
+ if (mediaType.startsWith('image/') && this.promptCapabilities.image) {
416
+ blocks.push({ type: 'image', data, mimeType: mediaType });
417
+ continue;
418
+ }
419
+ if (mediaType.startsWith('audio/') && this.promptCapabilities.audio) {
420
+ blocks.push({ type: 'audio', data, mimeType: mediaType });
421
+ continue;
422
+ }
423
+ if (this.promptCapabilities.embeddedContext) {
424
+ blocks.push({
425
+ type: 'resource',
426
+ resource: {
427
+ uri: `nexus-input://${this.sink.id}/${encodeURIComponent(attachment.name)}`,
428
+ blob: data,
429
+ mimeType: mediaType
430
+ }
431
+ });
432
+ continue;
433
+ }
434
+ blocks.push(await this.writeResourceLink(attachment, mediaType));
435
+ }
436
+ return blocks;
437
+ }
438
+ async writeResourceLink(attachment, mediaType) {
439
+ const directory = this.inputDirectory || path.join(process.cwd(), '.nexus-inputs', this.sink.id);
440
+ this.inputDirectory = directory;
441
+ await mkdir(directory, { recursive: true });
442
+ const filename = `${attachment.id}-${safeFilename(attachment.name) || 'attachment'}`;
443
+ const filePath = path.join(directory, filename);
444
+ await writeFile(filePath, attachment.bytes, { mode: 0o600 });
445
+ return {
446
+ type: 'resource_link',
447
+ name: attachment.name,
448
+ uri: pathToFileURL(filePath).toString(),
449
+ mimeType: mediaType,
450
+ size: attachment.bytes.length
451
+ };
452
+ }
453
+ }
454
+ class PromptTimeoutError extends Error {
455
+ }
456
+ function safeFilename(value) {
457
+ return String(value || '')
458
+ .replace(/[<>:"/\\|?*\u0000-\u001f]/g, '_')
459
+ .trim()
460
+ .slice(0, 180);
461
+ }
462
+ function withTimeout(promise, timeoutMs, message) {
463
+ let timer;
464
+ const timeout = new Promise((_, reject) => {
465
+ timer = setTimeout(() => reject(new PromptTimeoutError(message)), timeoutMs);
466
+ timer.unref?.();
467
+ });
468
+ return Promise.race([promise, timeout]).finally(() => {
469
+ if (timer)
470
+ clearTimeout(timer);
471
+ });
472
+ }
473
+ function artifactFromContent(content, id, hint) {
474
+ switch (content.type) {
475
+ case 'image': {
476
+ const filename = artifactFilename(content.uri, hint, `image-${artifactIndex(id)}`, content.mimeType);
477
+ return {
478
+ id,
479
+ name: filename,
480
+ filename,
481
+ mediaType: content.mimeType,
482
+ bytesBase64: content.data,
483
+ url: usableUrl(content.uri),
484
+ metadata: artifactMetadata(id, content.uri, content.annotations)
485
+ };
486
+ }
487
+ case 'audio': {
488
+ const filename = artifactFilename(undefined, hint, `audio-${artifactIndex(id)}`, content.mimeType);
489
+ return {
490
+ id,
491
+ name: filename,
492
+ filename,
493
+ mediaType: content.mimeType,
494
+ bytesBase64: content.data,
495
+ metadata: artifactMetadata(id, undefined, content.annotations)
496
+ };
497
+ }
498
+ case 'resource_link': {
499
+ const filename = artifactFilename(content.uri, content.name || hint, `resource-${artifactIndex(id)}`, content.mimeType || undefined);
500
+ return {
501
+ id,
502
+ name: content.title || content.name || filename,
503
+ description: content.description || undefined,
504
+ filename,
505
+ mediaType: content.mimeType || undefined,
506
+ url: content.uri,
507
+ metadata: artifactMetadata(id, content.uri, content.annotations, content.size ?? undefined)
508
+ };
509
+ }
510
+ case 'resource': {
511
+ const resource = content.resource;
512
+ const filename = artifactFilename(resource.uri, hint, `resource-${artifactIndex(id)}`, resource.mimeType || undefined);
513
+ return {
514
+ id,
515
+ name: filename,
516
+ filename,
517
+ mediaType: resource.mimeType || undefined,
518
+ text: 'text' in resource ? resource.text : undefined,
519
+ bytesBase64: 'blob' in resource ? resource.blob : undefined,
520
+ url: usableUrl(resource.uri),
521
+ metadata: artifactMetadata(id, resource.uri, content.annotations)
522
+ };
523
+ }
524
+ default:
525
+ return undefined;
526
+ }
527
+ }
528
+ function artifactMetadata(sourceId, uri, annotations, size) {
529
+ return {
530
+ sourceId,
531
+ ...(uri ? { uri } : {}),
532
+ ...(size !== undefined ? { size } : {}),
533
+ ...(annotations ? { annotations } : {})
534
+ };
535
+ }
536
+ function artifactFilename(uri, hint, fallback, mediaType) {
537
+ return (filenameFromValue(uri) ||
538
+ filenameFromValue(hint) ||
539
+ `${fallback}${extensionForMediaType(mediaType)}`);
540
+ }
541
+ function filenameFromValue(value) {
542
+ if (!value)
543
+ return undefined;
544
+ let candidate = value;
545
+ try {
546
+ candidate = new URL(value).pathname;
547
+ }
548
+ catch { }
549
+ candidate = candidate.split(/[\\/]/).pop() || candidate;
550
+ candidate = candidate.split(/[?#]/, 1)[0].trim();
551
+ const filename = candidate.match(/([^\s]+\.[A-Za-z0-9]{1,16})$/)?.[1];
552
+ if (!filename)
553
+ return undefined;
554
+ try {
555
+ return decodeURIComponent(filename);
556
+ }
557
+ catch {
558
+ return filename;
559
+ }
560
+ }
561
+ function usableUrl(value) {
562
+ return value && /^(?:https?|file):/i.test(value) ? value : undefined;
563
+ }
564
+ function artifactIndex(id) {
565
+ return id.split(':').pop() || 'output';
566
+ }
567
+ function extensionForMediaType(mediaType) {
568
+ const extensions = {
569
+ 'image/png': '.png',
570
+ 'image/jpeg': '.jpg',
571
+ 'image/gif': '.gif',
572
+ 'image/webp': '.webp',
573
+ 'audio/mpeg': '.mp3',
574
+ 'audio/wav': '.wav',
575
+ 'audio/ogg': '.ogg',
576
+ 'application/pdf': '.pdf'
577
+ };
578
+ return mediaType ? extensions[mediaType.toLowerCase()] || '' : '';
348
579
  }
349
580
  function elicitationOptions(params) {
350
581
  if (!isFormElicitation(params))
@@ -0,0 +1,41 @@
1
+ import { type FileHandle } from 'node:fs/promises';
2
+ import { type Readable } from 'node:stream';
3
+ import type { AgentdPublishedFile } from './types.js';
4
+ interface StoredArtifact extends AgentdPublishedFile {
5
+ filePath: string;
6
+ handle?: FileHandle;
7
+ }
8
+ export declare class ArtifactStoreError extends Error {
9
+ readonly status: number;
10
+ constructor(status: number, message: string);
11
+ }
12
+ export declare class GatewayArtifactStore {
13
+ private readonly directory;
14
+ private readonly ttlMs;
15
+ private readonly maxBytes;
16
+ private readonly maxStorageBytes;
17
+ private readonly maxFiles;
18
+ private readonly maxConcurrentPublishes;
19
+ private readonly byCacheKey;
20
+ private cleanupTimer?;
21
+ private capacityQueue;
22
+ private activePublishes;
23
+ private reservedBytes;
24
+ private reservedFiles;
25
+ constructor(directory: string, ttlMs: number, maxBytes: number, maxStorageBytes?: number, maxFiles?: number, maxConcurrentPublishes?: number);
26
+ startCleanup(intervalMs?: number): void;
27
+ stopCleanup(): void;
28
+ publishFile(filePath: string, preferredName?: string, mediaType?: string): Promise<AgentdPublishedFile>;
29
+ publishHandle(handle: FileHandle, preferredName: string, mediaType?: string): Promise<AgentdPublishedFile>;
30
+ publishBytes(bytes: Uint8Array, preferredName: string, mediaType?: string, cacheKey?: string): Promise<AgentdPublishedFile>;
31
+ publishStream(input: Readable, preferredName: string, mediaType?: string, declaredSize?: number, cacheKey?: string): Promise<AgentdPublishedFile>;
32
+ open(id: string, expiresInput: string, requestedName: string): Promise<StoredArtifact>;
33
+ cleanup(): Promise<void>;
34
+ private ensureDirectory;
35
+ private isAvailable;
36
+ private reserveCapacity;
37
+ private storageUsage;
38
+ }
39
+ export declare function contentDisposition(name: string): string;
40
+ export declare function mediaTypeForName(name: string): string;
41
+ export {};