nexus-agentd 0.2.5 → 0.3.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.
Files changed (61) hide show
  1. package/README.md +359 -250
  2. package/dist/a2a/runtime.d.ts +7 -1
  3. package/dist/a2a/runtime.js +290 -17
  4. package/dist/acp/runtime.d.ts +5 -1
  5. package/dist/acp/runtime.js +88 -39
  6. package/dist/artifact-store.d.ts +162 -0
  7. package/dist/artifact-store.js +1081 -0
  8. package/dist/cli.js +13 -14
  9. package/dist/completion-contract.js +1 -1
  10. package/dist/config.js +45 -5
  11. package/dist/control-plane-validation.d.ts +54 -0
  12. package/dist/control-plane-validation.js +152 -0
  13. package/dist/control-plane.d.ts +9 -32
  14. package/dist/control-plane.js +52 -158
  15. package/dist/drivers/index.js +12 -0
  16. package/dist/drivers/pi.js +1 -1
  17. package/dist/drivers/stdio.d.ts +4 -1
  18. package/dist/drivers/stdio.js +135 -19
  19. package/dist/events.d.ts +9 -0
  20. package/dist/events.js +17 -4
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.js +33 -6
  23. package/dist/process-tree.js +15 -6
  24. package/dist/run-store.d.ts +57 -7
  25. package/dist/run-store.js +188 -23
  26. package/dist/server/auth-policy.d.ts +27 -0
  27. package/dist/server/auth-policy.js +143 -0
  28. package/dist/server/connection.d.ts +8 -0
  29. package/dist/server/connection.js +67 -0
  30. package/dist/server/diagnostics.d.ts +40 -0
  31. package/dist/server/diagnostics.js +130 -0
  32. package/dist/server/metrics.d.ts +51 -0
  33. package/dist/server/metrics.js +56 -0
  34. package/dist/server/quota.d.ts +59 -0
  35. package/dist/server/quota.js +176 -0
  36. package/dist/server/validation.d.ts +29 -0
  37. package/dist/server/validation.js +221 -0
  38. package/dist/server.d.ts +3 -0
  39. package/dist/server.js +682 -418
  40. package/dist/session/admin-runs.d.ts +73 -0
  41. package/dist/session/admin-runs.js +297 -0
  42. package/dist/session/admission.d.ts +32 -0
  43. package/dist/session/admission.js +62 -0
  44. package/dist/session/readiness.d.ts +33 -0
  45. package/dist/session/readiness.js +174 -0
  46. package/dist/session-contract.d.ts +1 -0
  47. package/dist/session.d.ts +64 -7
  48. package/dist/session.js +321 -159
  49. package/dist/sse-writer.d.ts +7 -0
  50. package/dist/sse-writer.js +60 -0
  51. package/dist/types.d.ts +44 -2
  52. package/dist/types.js +1 -0
  53. package/dist/webui/app/sources.js +18 -10
  54. package/dist/webui/index.js +11 -11
  55. package/dist/webui/markup.js +69 -66
  56. package/dist/webui/styles.js +791 -616
  57. package/dist/workspace.d.ts +30 -0
  58. package/dist/workspace.js +67 -2
  59. package/examples/client.mjs +102 -0
  60. package/nexus-agentd.example.json +21 -2
  61. package/package.json +6 -3
@@ -10,6 +10,8 @@ export declare class A2AClientRuntime implements AgentSessionRuntime {
10
10
  private taskId;
11
11
  private contextId;
12
12
  private prompting;
13
+ private promptFinished?;
14
+ private queuedResponse;
13
15
  private disposed;
14
16
  private activeController?;
15
17
  private readonly artifactCache;
@@ -17,12 +19,16 @@ export declare class A2AClientRuntime implements AgentSessionRuntime {
17
19
  private sawTaskStatus;
18
20
  private sawAgentMessage;
19
21
  private completionProof?;
22
+ private readonly taskTimeoutMs;
23
+ private readonly streamIdleTimeoutMs;
24
+ private taskDeadlineAt?;
20
25
  constructor(config: AgentdA2AConfig, sink: AgentSessionSink, promptTimeoutMs: number);
21
26
  start(): Promise<void>;
22
- prompt(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
27
+ prompt(message: string, attachments?: AgentdInputAttachment[], continuation?: boolean): Promise<void>;
23
28
  respondPending(response: AgentdPendingResponse | string, attachments?: AgentdInputAttachment[]): Promise<void>;
24
29
  cancel(): Promise<void>;
25
30
  dispose(): Promise<void>;
31
+ isAvailable(): boolean;
26
32
  private messageRequest;
27
33
  private processResponse;
28
34
  private processTask;
@@ -44,6 +44,8 @@ export class A2AClientRuntime {
44
44
  taskId = '';
45
45
  contextId = '';
46
46
  prompting = false;
47
+ promptFinished;
48
+ queuedResponse = false;
47
49
  disposed = false;
48
50
  activeController;
49
51
  artifactCache = new Map();
@@ -51,12 +53,23 @@ export class A2AClientRuntime {
51
53
  sawTaskStatus = false;
52
54
  sawAgentMessage = false;
53
55
  completionProof;
56
+ taskTimeoutMs;
57
+ streamIdleTimeoutMs;
58
+ taskDeadlineAt;
54
59
  constructor(config, sink, promptTimeoutMs) {
55
60
  this.config = config;
56
61
  this.sink = sink;
57
62
  this.promptTimeoutMs = promptTimeoutMs;
63
+ // Keep inheritance at runtime so changing the gateway-wide prompt
64
+ // timeout continues to affect agents that did not opt into a local
65
+ // task deadline. The per-request timeout is deliberately excluded
66
+ // from this calculation: it only bounds connection/headers.
67
+ this.taskTimeoutMs = positiveTimeout(config.taskTimeoutMs ?? promptTimeoutMs, promptTimeoutMs);
68
+ this.streamIdleTimeoutMs = positiveTimeout(config.streamIdleTimeoutMs ?? config.timeoutMs, config.timeoutMs || 60_000);
58
69
  }
59
70
  async start() {
71
+ if (this.disposed)
72
+ throw new Error('A2A runtime is disposed');
60
73
  const resolver = createResolver(this.config);
61
74
  this.card = await resolveAgentCard(resolver, this.config);
62
75
  const fetchImpl = authenticatedFetch(this.config);
@@ -70,9 +83,11 @@ export class A2AClientRuntime {
70
83
  ]
71
84
  });
72
85
  this.client = await factory.createFromAgentCard(this.card);
86
+ if (this.disposed)
87
+ throw new Error('A2A runtime is disposed');
73
88
  this.sink.setState('created');
74
89
  }
75
- async prompt(message, attachments = []) {
90
+ async prompt(message, attachments = [], continuation = false) {
76
91
  if (!this.client || !this.card)
77
92
  throw new Error('A2A runtime is not connected');
78
93
  if (this.prompting)
@@ -80,23 +95,36 @@ export class A2AClientRuntime {
80
95
  if (this.disposed)
81
96
  throw new Error('A2A runtime is disposed');
82
97
  this.prompting = true;
98
+ let finish;
99
+ this.promptFinished = new Promise((resolve) => { finish = resolve; });
83
100
  this.sawTaskStatus = false;
84
101
  this.sawAgentMessage = false;
85
102
  this.completionProof = undefined;
86
103
  this.sink.clearPending();
87
104
  this.sink.setState('running');
105
+ const taskDeadlineAt = continuation && this.taskDeadlineAt !== undefined
106
+ ? this.taskDeadlineAt
107
+ : Date.now() + this.taskTimeoutMs;
108
+ this.taskDeadlineAt = taskDeadlineAt;
88
109
  const controller = new AbortController();
89
110
  this.activeController = controller;
90
- const timer = setTimeout(() => controller.abort(new Error('A2A message timed out')), Math.min(this.promptTimeoutMs, this.config.timeoutMs || this.promptTimeoutMs));
111
+ const remainingTaskMs = Math.max(1, taskDeadlineAt - Date.now());
112
+ const timer = setTimeout(() => controller.abort(new A2ATaskTimeoutError(this.taskTimeoutMs)), remainingTaskMs);
91
113
  timer.unref?.();
92
114
  try {
93
115
  const request = this.messageRequest(message, attachments);
94
116
  for await (const response of this.client.sendMessageStream(request, {
95
117
  signal: controller.signal
96
118
  })) {
119
+ if (controller.signal.aborted || this.disposed)
120
+ break;
121
+ // An accepted answer supersedes the old input-required stream.
122
+ // Its trailing status must not reopen the answered request.
123
+ if (this.queuedResponse)
124
+ continue;
97
125
  this.processResponse(response);
98
126
  }
99
- if (this.sink.state === 'running') {
127
+ if (this.sink.state === 'running' && !this.queuedResponse) {
100
128
  if (this.completionProof) {
101
129
  this.sink.completeTurn(this.completionProof);
102
130
  }
@@ -114,7 +142,7 @@ export class A2AClientRuntime {
114
142
  }
115
143
  }
116
144
  catch (error) {
117
- if (this.sink.state !== 'canceled') {
145
+ if (this.sink.state !== 'canceled' && !this.queuedResponse) {
118
146
  this.sink.setState('failed', abortMessage(error, controller.signal));
119
147
  }
120
148
  }
@@ -123,9 +151,16 @@ export class A2AClientRuntime {
123
151
  if (this.activeController === controller)
124
152
  this.activeController = undefined;
125
153
  this.prompting = false;
154
+ if (this.sink.state !== 'input_required' && !this.queuedResponse) {
155
+ this.taskDeadlineAt = undefined;
156
+ }
157
+ finish();
158
+ this.promptFinished = undefined;
126
159
  }
127
160
  }
128
161
  async respondPending(response, attachments = []) {
162
+ if (!this.isAvailable())
163
+ throw new Error('A2A runtime is unavailable');
129
164
  if (this.sink.state !== 'input_required') {
130
165
  throw new Error('A2A session is not waiting for input');
131
166
  }
@@ -138,10 +173,27 @@ export class A2AClientRuntime {
138
173
  : response.message ?? response.optionId ?? '';
139
174
  if (!message.trim())
140
175
  throw new Error('A2A pending response is empty');
176
+ const previous = this.promptFinished || Promise.resolve();
177
+ this.queuedResponse = true;
141
178
  this.sink.clearPending();
142
- await this.prompt(message, attachments);
179
+ this.sink.setState('running');
180
+ void previous.then(async () => {
181
+ this.queuedResponse = false;
182
+ if (this.disposed ||
183
+ this.sink.state === 'canceled' ||
184
+ this.sink.state === 'failed' ||
185
+ (this.taskDeadlineAt !== undefined && this.taskDeadlineAt <= Date.now()))
186
+ return;
187
+ await this.prompt(message, attachments, true);
188
+ }).catch((error) => {
189
+ if (!this.disposed && this.sink.state !== 'canceled')
190
+ this.sink.setState('failed', errorMessage(error));
191
+ });
143
192
  }
144
193
  async cancel() {
194
+ this.taskDeadlineAt = undefined;
195
+ this.sink.clearPending();
196
+ this.sink.setState('canceled');
145
197
  this.activeController?.abort(new Error('A2A session canceled'));
146
198
  if (this.client && this.taskId) {
147
199
  try {
@@ -159,9 +211,13 @@ export class A2AClientRuntime {
159
211
  }
160
212
  async dispose() {
161
213
  this.disposed = true;
214
+ this.taskDeadlineAt = undefined;
162
215
  this.activeController?.abort(new Error('A2A runtime disposed'));
163
216
  this.activeController = undefined;
164
217
  }
218
+ isAvailable() {
219
+ return !this.disposed && Boolean(this.client && this.card);
220
+ }
165
221
  messageRequest(text, attachments) {
166
222
  return {
167
223
  tenant: '',
@@ -338,12 +394,43 @@ function createResolver(config) {
338
394
  legacyCompat: { enabled: true }
339
395
  });
340
396
  }
341
- function resolveAgentCard(resolver, config) {
342
- if (config.agentCardUrl)
343
- return resolver.resolve(config.agentCardUrl, '');
344
- if (config.agentUrl)
345
- return resolver.resolve(config.agentUrl);
346
- throw new Error('A2A Agent Card URL is required');
397
+ async function resolveAgentCard(resolver, config) {
398
+ const origin = configuredOrigin(config);
399
+ const card = config.agentCardUrl
400
+ ? await resolver.resolve(config.agentCardUrl, '')
401
+ : await resolver.resolve(config.agentUrl);
402
+ // The resolver normalizes v0.3 url/additionalInterfaces into this array.
403
+ // Validate every advertised endpoint before transport selection or probing.
404
+ if (!Array.isArray(card.supportedInterfaces) || !card.supportedInterfaces.length) {
405
+ throw new Error('A2A Agent Card must declare at least one interface URL');
406
+ }
407
+ for (const endpoint of card.supportedInterfaces)
408
+ assertSameOrigin(endpoint?.url, origin);
409
+ return card;
410
+ }
411
+ function a2aUrl(value) {
412
+ let url;
413
+ try {
414
+ url = new URL(value);
415
+ }
416
+ catch {
417
+ throw new Error('A2A URL is invalid');
418
+ }
419
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) {
420
+ throw new Error('A2A URL must use http(s) without credentials or a fragment');
421
+ }
422
+ return url;
423
+ }
424
+ function configuredOrigin(config) {
425
+ const value = config.agentCardUrl || config.agentUrl;
426
+ if (!value)
427
+ throw new Error('A2A Agent Card URL is required');
428
+ return a2aUrl(value).origin;
429
+ }
430
+ function assertSameOrigin(value, origin) {
431
+ if (a2aUrl(value).origin !== origin) {
432
+ throw new Error('A2A URL must have the same origin as the configured Agent Card URL');
433
+ }
347
434
  }
348
435
  function preferredTransports(config) {
349
436
  if (config.preferredTransport === 'jsonrpc')
@@ -353,8 +440,12 @@ function preferredTransports(config) {
353
440
  return undefined;
354
441
  }
355
442
  function authenticatedFetch(config) {
443
+ const origin = configuredOrigin(config);
356
444
  return async (input, init = {}) => {
357
- const headers = new Headers(init.headers);
445
+ // The final guard also covers SDK-derived paths and future card flows.
446
+ // Check before attaching credentials; never let fetch follow redirects.
447
+ assertSameOrigin(input instanceof Request ? input.url : String(input), origin);
448
+ const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined));
358
449
  if (config.auth?.type === 'bearer' && config.auth.value) {
359
450
  headers.set('Authorization', `Bearer ${config.auth.value}`);
360
451
  }
@@ -363,13 +454,195 @@ function authenticatedFetch(config) {
363
454
  config.auth.value) {
364
455
  headers.set(config.auth.headerName, config.auth.value);
365
456
  }
366
- return fetch(input, {
367
- ...init,
368
- headers,
369
- signal: init.signal || AbortSignal.timeout(config.timeoutMs || 60_000)
370
- });
457
+ const timeoutMs = config.timeoutMs || 60_000;
458
+ const timeoutController = new AbortController();
459
+ const signals = [timeoutController.signal];
460
+ if (init.signal)
461
+ signals.push(init.signal);
462
+ const signal = AbortSignal.any(signals);
463
+ const requestTimeout = new A2ARequestTimeoutError(timeoutMs);
464
+ let bodyAbort;
465
+ const timer = setTimeout(() => {
466
+ timeoutController.abort(requestTimeout);
467
+ bodyAbort?.();
468
+ }, timeoutMs);
469
+ timer.unref?.();
470
+ try {
471
+ const response = await fetch(input, {
472
+ ...init,
473
+ headers,
474
+ redirect: 'error',
475
+ signal
476
+ });
477
+ // Only an SSE response is an intentionally long-lived request.
478
+ // Card and ordinary JSON calls retain timeoutMs through the body;
479
+ // otherwise a slow/trickling Card could evade the request bound.
480
+ if (isEventStream(response)) {
481
+ clearTimeout(timer);
482
+ return withStreamIdleTimeout(response, config.streamIdleTimeoutMs || timeoutMs);
483
+ }
484
+ if (!response.body) {
485
+ clearTimeout(timer);
486
+ return response;
487
+ }
488
+ const deadline = responseDeadlineBody(response.body, requestTimeout, () => clearTimeout(timer));
489
+ bodyAbort = deadline.abort;
490
+ return new Response(deadline.stream, {
491
+ status: response.status,
492
+ statusText: response.statusText,
493
+ headers: response.headers
494
+ });
495
+ }
496
+ catch (error) {
497
+ clearTimeout(timer);
498
+ if (timeoutController.signal.aborted && !init.signal?.aborted) {
499
+ throw requestTimeout;
500
+ }
501
+ throw error;
502
+ }
371
503
  };
372
504
  }
505
+ class A2ARequestTimeoutError extends Error {
506
+ constructor(timeoutMs) {
507
+ super(`A2A request timed out after ${timeoutMs}ms`);
508
+ this.name = 'A2ARequestTimeoutError';
509
+ }
510
+ }
511
+ class A2ATaskTimeoutError extends Error {
512
+ constructor(timeoutMs) {
513
+ super(`A2A task timed out after ${timeoutMs}ms`);
514
+ this.name = 'A2ATaskTimeoutError';
515
+ }
516
+ }
517
+ class A2AStreamIdleTimeoutError extends Error {
518
+ constructor(timeoutMs) {
519
+ super(`A2A stream made no progress for ${timeoutMs}ms`);
520
+ this.name = 'A2AStreamIdleTimeoutError';
521
+ }
522
+ }
523
+ function withStreamIdleTimeout(response, timeoutMs) {
524
+ if (!response.body || !Number.isFinite(timeoutMs) || timeoutMs <= 0)
525
+ return response;
526
+ const body = idleTimeoutBody(response.body, timeoutMs);
527
+ return new Response(body, {
528
+ status: response.status,
529
+ statusText: response.statusText,
530
+ headers: response.headers
531
+ });
532
+ }
533
+ function isEventStream(response) {
534
+ return response.headers.get('content-type')?.split(';', 1)[0].trim().toLowerCase() ===
535
+ 'text/event-stream';
536
+ }
537
+ function responseDeadlineBody(body, timeoutError, cleanup) {
538
+ const reader = body.getReader();
539
+ let controllerRef;
540
+ let closed = false;
541
+ let timedOut = false;
542
+ const abort = () => {
543
+ if (closed || timedOut)
544
+ return;
545
+ timedOut = true;
546
+ void reader.cancel(timeoutError).catch(() => undefined);
547
+ controllerRef?.error(timeoutError);
548
+ };
549
+ const stream = new ReadableStream({
550
+ start(controller) {
551
+ controllerRef = controller;
552
+ },
553
+ async pull(controller) {
554
+ if (closed || timedOut)
555
+ return;
556
+ try {
557
+ const result = await reader.read();
558
+ if (timedOut)
559
+ return;
560
+ if (result.done) {
561
+ closed = true;
562
+ cleanup();
563
+ controller.close();
564
+ return;
565
+ }
566
+ controller.enqueue(result.value);
567
+ }
568
+ catch (error) {
569
+ if (timedOut)
570
+ return;
571
+ closed = true;
572
+ cleanup();
573
+ controller.error(error);
574
+ }
575
+ },
576
+ async cancel(reason) {
577
+ closed = true;
578
+ cleanup();
579
+ await reader.cancel(reason);
580
+ }
581
+ });
582
+ return { stream, abort };
583
+ }
584
+ function idleTimeoutBody(body, timeoutMs) {
585
+ const reader = body.getReader();
586
+ let controllerRef;
587
+ let timer;
588
+ let closed = false;
589
+ let timedOut = false;
590
+ const clearIdleTimer = () => {
591
+ if (timer)
592
+ clearTimeout(timer);
593
+ timer = undefined;
594
+ };
595
+ const armIdleTimer = () => {
596
+ clearIdleTimer();
597
+ timer = setTimeout(() => {
598
+ if (closed || timedOut)
599
+ return;
600
+ timedOut = true;
601
+ const error = new A2AStreamIdleTimeoutError(timeoutMs);
602
+ void reader.cancel(error).catch(() => undefined);
603
+ controllerRef?.error(error);
604
+ }, timeoutMs);
605
+ timer.unref?.();
606
+ };
607
+ return new ReadableStream({
608
+ start(controller) {
609
+ controllerRef = controller;
610
+ armIdleTimer();
611
+ },
612
+ async pull(controller) {
613
+ if (closed || timedOut)
614
+ return;
615
+ try {
616
+ const result = await reader.read();
617
+ if (timedOut)
618
+ return;
619
+ if (result.done) {
620
+ closed = true;
621
+ clearIdleTimer();
622
+ controller.close();
623
+ return;
624
+ }
625
+ armIdleTimer();
626
+ controller.enqueue(result.value);
627
+ }
628
+ catch (error) {
629
+ if (timedOut)
630
+ return;
631
+ closed = true;
632
+ clearIdleTimer();
633
+ controller.error(error);
634
+ }
635
+ },
636
+ async cancel(reason) {
637
+ closed = true;
638
+ clearIdleTimer();
639
+ await reader.cancel(reason);
640
+ }
641
+ });
642
+ }
643
+ function positiveTimeout(value, fallback) {
644
+ return Number.isFinite(value) && value > 0 ? Math.trunc(value) : fallback;
645
+ }
373
646
  function artifactFromA2A(artifact) {
374
647
  const text = textParts(artifact.parts).join('') || undefined;
375
648
  const urlPart = artifact.parts.find((part) => part.content?.$case === 'url');
@@ -4,6 +4,8 @@ import type { AgentdInputAttachment, AgentdPendingResponse } from '../types.js';
4
4
  export declare class AcpProcessRuntime {
5
5
  private readonly driver;
6
6
  private readonly sink;
7
+ private readonly allowedRoots;
8
+ private readonly startupTimeoutMs;
7
9
  private process?;
8
10
  private connection?;
9
11
  private pending?;
@@ -16,12 +18,14 @@ export declare class AcpProcessRuntime {
16
18
  private inputDirectory?;
17
19
  private workspace?;
18
20
  private promptOutput;
19
- constructor(driver: AgentDriver, sink: AcpSessionSink, maxStderrChunkChars?: number, promptTimeoutMs?: number);
21
+ constructor(driver: AgentDriver, sink: AcpSessionSink, maxStderrChunkChars?: number, promptTimeoutMs?: number, allowedRoots?: readonly string[], startupTimeoutMs?: number);
20
22
  start(workspace: string): Promise<void>;
23
+ private connect;
21
24
  prompt(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
22
25
  respondPending(input: AgentdPendingResponse | string, _attachments?: AgentdInputAttachment[]): Promise<void>;
23
26
  cancel(): Promise<void>;
24
27
  dispose(): Promise<void>;
28
+ isAvailable(): boolean;
25
29
  private requestPermission;
26
30
  private finishPermission;
27
31
  private requestInput;