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.
- package/LICENSE +22 -0
- package/README.md +351 -179
- package/dist/a2a/runtime.d.ts +30 -0
- package/dist/a2a/runtime.js +374 -0
- package/dist/acp/runtime.d.ts +12 -3
- package/dist/acp/runtime.js +248 -17
- package/dist/artifact-store.d.ts +41 -0
- package/dist/artifact-store.js +340 -0
- package/dist/auth.d.ts +13 -0
- package/dist/auth.js +77 -0
- package/dist/cli.js +58 -1
- package/dist/config.d.ts +10 -0
- package/dist/config.js +281 -53
- package/dist/control-plane.d.ts +91 -0
- package/dist/control-plane.js +629 -0
- package/dist/drivers/hermes.d.ts +2 -0
- package/dist/drivers/hermes.js +10 -0
- package/dist/drivers/index.js +6 -0
- package/dist/events.js +13 -2
- package/dist/index.d.ts +6 -0
- package/dist/index.js +15 -3
- package/dist/run-store.d.ts +49 -0
- package/dist/run-store.js +291 -0
- package/dist/server.d.ts +3 -2
- package/dist/server.js +911 -41
- package/dist/session-contract.d.ts +16 -4
- package/dist/session.d.ts +74 -8
- package/dist/session.js +433 -38
- package/dist/types.d.ts +193 -6
- package/dist/types.js +2 -1
- package/dist/webui/app.d.ts +1 -0
- package/dist/webui/app.js +1072 -0
- package/dist/webui/icons.d.ts +9 -0
- package/dist/webui/icons.js +10 -0
- package/dist/webui/index.d.ts +3 -0
- package/dist/webui/index.js +32 -0
- package/dist/webui/markup.d.ts +1 -0
- package/dist/webui/markup.js +68 -0
- package/dist/webui/styles.d.ts +1 -0
- package/dist/webui/styles.js +269 -0
- package/dist/workspace-files.d.ts +48 -0
- package/dist/workspace-files.js +269 -0
- package/nexus-agentd.example.json +30 -27
- package/package.json +6 -6
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { Role, TaskState } from '@a2a-js/sdk';
|
|
3
|
+
import { ClientFactory, DefaultAgentCardResolver, JsonRpcTransportFactory, RestTransportFactory } from '@a2a-js/sdk/client';
|
|
4
|
+
export async function probeA2AAgent(id, config) {
|
|
5
|
+
const startedAt = Date.now();
|
|
6
|
+
if (config.enabled === false)
|
|
7
|
+
return disabledView(id, config);
|
|
8
|
+
try {
|
|
9
|
+
const resolver = createResolver(config);
|
|
10
|
+
const card = await resolveAgentCard(resolver, config);
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
name: config.name || card.name || id,
|
|
14
|
+
description: config.description || card.description || undefined,
|
|
15
|
+
protocol: 'a2a',
|
|
16
|
+
ready: true,
|
|
17
|
+
enabled: true,
|
|
18
|
+
version: card.version || undefined,
|
|
19
|
+
checkedAt: Date.now(),
|
|
20
|
+
responseMs: Date.now() - startedAt
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return {
|
|
25
|
+
id,
|
|
26
|
+
name: config.name || id,
|
|
27
|
+
description: config.description,
|
|
28
|
+
protocol: 'a2a',
|
|
29
|
+
ready: false,
|
|
30
|
+
enabled: true,
|
|
31
|
+
error: errorMessage(error),
|
|
32
|
+
checkedAt: Date.now(),
|
|
33
|
+
responseMs: Date.now() - startedAt
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class A2AClientRuntime {
|
|
38
|
+
config;
|
|
39
|
+
sink;
|
|
40
|
+
promptTimeoutMs;
|
|
41
|
+
client;
|
|
42
|
+
card;
|
|
43
|
+
taskId = '';
|
|
44
|
+
contextId = '';
|
|
45
|
+
prompting = false;
|
|
46
|
+
disposed = false;
|
|
47
|
+
activeController;
|
|
48
|
+
artifactCache = new Map();
|
|
49
|
+
emittedMessages = new Set();
|
|
50
|
+
constructor(config, sink, promptTimeoutMs) {
|
|
51
|
+
this.config = config;
|
|
52
|
+
this.sink = sink;
|
|
53
|
+
this.promptTimeoutMs = promptTimeoutMs;
|
|
54
|
+
}
|
|
55
|
+
async start() {
|
|
56
|
+
const resolver = createResolver(this.config);
|
|
57
|
+
this.card = await resolveAgentCard(resolver, this.config);
|
|
58
|
+
const fetchImpl = authenticatedFetch(this.config);
|
|
59
|
+
const legacyCompat = { enabled: true };
|
|
60
|
+
const factory = new ClientFactory({
|
|
61
|
+
cardResolver: resolver,
|
|
62
|
+
preferredTransports: preferredTransports(this.config),
|
|
63
|
+
transports: [
|
|
64
|
+
new JsonRpcTransportFactory({ fetchImpl, legacyCompat }),
|
|
65
|
+
new RestTransportFactory({ fetchImpl, legacyCompat })
|
|
66
|
+
]
|
|
67
|
+
});
|
|
68
|
+
this.client = await factory.createFromAgentCard(this.card);
|
|
69
|
+
this.sink.setState('created');
|
|
70
|
+
}
|
|
71
|
+
async prompt(message, attachments = []) {
|
|
72
|
+
if (!this.client || !this.card)
|
|
73
|
+
throw new Error('A2A runtime is not connected');
|
|
74
|
+
if (this.prompting)
|
|
75
|
+
throw new Error('A2A session is already processing a message');
|
|
76
|
+
if (this.disposed)
|
|
77
|
+
throw new Error('A2A runtime is disposed');
|
|
78
|
+
this.prompting = true;
|
|
79
|
+
this.sink.clearPending();
|
|
80
|
+
this.sink.setState('running');
|
|
81
|
+
const controller = new AbortController();
|
|
82
|
+
this.activeController = controller;
|
|
83
|
+
const timer = setTimeout(() => controller.abort(new Error('A2A message timed out')), Math.min(this.promptTimeoutMs, this.config.timeoutMs || this.promptTimeoutMs));
|
|
84
|
+
timer.unref?.();
|
|
85
|
+
try {
|
|
86
|
+
const request = this.messageRequest(message, attachments);
|
|
87
|
+
for await (const response of this.client.sendMessageStream(request, {
|
|
88
|
+
signal: controller.signal
|
|
89
|
+
})) {
|
|
90
|
+
this.processResponse(response);
|
|
91
|
+
}
|
|
92
|
+
if (this.sink.state === 'running')
|
|
93
|
+
this.sink.setState('completed');
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
if (this.sink.state !== 'canceled') {
|
|
97
|
+
this.sink.setState('failed', abortMessage(error, controller.signal));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
if (this.activeController === controller)
|
|
103
|
+
this.activeController = undefined;
|
|
104
|
+
this.prompting = false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async respondPending(message, attachments = []) {
|
|
108
|
+
if (this.sink.state !== 'input_required') {
|
|
109
|
+
throw new Error('A2A session is not waiting for input');
|
|
110
|
+
}
|
|
111
|
+
this.sink.clearPending();
|
|
112
|
+
await this.prompt(message, attachments);
|
|
113
|
+
}
|
|
114
|
+
async cancel() {
|
|
115
|
+
this.activeController?.abort(new Error('A2A session canceled'));
|
|
116
|
+
if (this.client && this.taskId) {
|
|
117
|
+
try {
|
|
118
|
+
await this.client.cancelTask({ tenant: '', id: this.taskId, metadata: undefined }, { signal: AbortSignal.timeout(this.config.timeoutMs || 60_000) });
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
this.sink.emit('terminal_output', {
|
|
122
|
+
stream: 'system',
|
|
123
|
+
text: `A2A cancellation request failed: ${errorMessage(error)}`
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
this.sink.clearPending();
|
|
128
|
+
this.sink.setState('canceled');
|
|
129
|
+
}
|
|
130
|
+
async dispose() {
|
|
131
|
+
this.disposed = true;
|
|
132
|
+
this.activeController?.abort(new Error('A2A runtime disposed'));
|
|
133
|
+
this.activeController = undefined;
|
|
134
|
+
}
|
|
135
|
+
messageRequest(text, attachments) {
|
|
136
|
+
return {
|
|
137
|
+
tenant: '',
|
|
138
|
+
message: {
|
|
139
|
+
messageId: randomUUID(),
|
|
140
|
+
contextId: this.contextId,
|
|
141
|
+
taskId: this.taskId,
|
|
142
|
+
role: Role.ROLE_USER,
|
|
143
|
+
parts: [
|
|
144
|
+
{
|
|
145
|
+
content: { $case: 'text', value: text },
|
|
146
|
+
metadata: undefined,
|
|
147
|
+
filename: '',
|
|
148
|
+
mediaType: 'text/plain'
|
|
149
|
+
},
|
|
150
|
+
...attachments.map((attachment) => ({
|
|
151
|
+
content: {
|
|
152
|
+
$case: 'raw',
|
|
153
|
+
value: Buffer.from(attachment.bytes)
|
|
154
|
+
},
|
|
155
|
+
metadata: undefined,
|
|
156
|
+
filename: attachment.name,
|
|
157
|
+
mediaType: attachment.mediaType || 'application/octet-stream'
|
|
158
|
+
}))
|
|
159
|
+
],
|
|
160
|
+
metadata: undefined,
|
|
161
|
+
extensions: [],
|
|
162
|
+
referenceTaskIds: []
|
|
163
|
+
},
|
|
164
|
+
configuration: {
|
|
165
|
+
acceptedOutputModes: this.card?.defaultOutputModes?.length
|
|
166
|
+
? [...this.card.defaultOutputModes]
|
|
167
|
+
: ['text/plain', 'application/octet-stream'],
|
|
168
|
+
taskPushNotificationConfig: undefined,
|
|
169
|
+
returnImmediately: false
|
|
170
|
+
},
|
|
171
|
+
metadata: undefined
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
processResponse(response) {
|
|
175
|
+
const payload = response.payload;
|
|
176
|
+
if (!payload)
|
|
177
|
+
return;
|
|
178
|
+
switch (payload.$case) {
|
|
179
|
+
case 'message':
|
|
180
|
+
this.processMessage(payload.value);
|
|
181
|
+
break;
|
|
182
|
+
case 'task':
|
|
183
|
+
this.processTask(payload.value);
|
|
184
|
+
break;
|
|
185
|
+
case 'statusUpdate':
|
|
186
|
+
this.captureIds(payload.value.taskId, payload.value.contextId);
|
|
187
|
+
this.processStatus(payload.value.status);
|
|
188
|
+
break;
|
|
189
|
+
case 'artifactUpdate':
|
|
190
|
+
this.captureIds(payload.value.taskId, payload.value.contextId);
|
|
191
|
+
this.processArtifact(payload.value.artifact, payload.value.append, payload.value.lastChunk);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
processTask(task) {
|
|
196
|
+
this.captureIds(task.id, task.contextId);
|
|
197
|
+
for (const artifact of task.artifacts)
|
|
198
|
+
this.processArtifact(artifact, false, true);
|
|
199
|
+
this.processStatus(task.status);
|
|
200
|
+
}
|
|
201
|
+
processMessage(message) {
|
|
202
|
+
if (!message || message.role !== Role.ROLE_AGENT)
|
|
203
|
+
return;
|
|
204
|
+
this.captureIds(message.taskId, message.contextId);
|
|
205
|
+
if (message.messageId && this.emittedMessages.has(message.messageId))
|
|
206
|
+
return;
|
|
207
|
+
if (message.messageId)
|
|
208
|
+
this.emittedMessages.add(message.messageId);
|
|
209
|
+
for (const text of textParts(message.parts)) {
|
|
210
|
+
this.sink.appendOutput(text);
|
|
211
|
+
this.sink.emit('assistant_chunk', {
|
|
212
|
+
messageId: message.messageId || undefined,
|
|
213
|
+
content: { type: 'text', text }
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
processStatus(status) {
|
|
218
|
+
if (!status)
|
|
219
|
+
return;
|
|
220
|
+
this.processMessage(status.message);
|
|
221
|
+
const statusText = textParts(status.message?.parts || []).join('\n').trim();
|
|
222
|
+
switch (status.state) {
|
|
223
|
+
case TaskState.TASK_STATE_SUBMITTED:
|
|
224
|
+
case TaskState.TASK_STATE_WORKING:
|
|
225
|
+
this.sink.setState('running');
|
|
226
|
+
break;
|
|
227
|
+
case TaskState.TASK_STATE_COMPLETED:
|
|
228
|
+
this.sink.clearPending();
|
|
229
|
+
this.sink.setState('completed');
|
|
230
|
+
break;
|
|
231
|
+
case TaskState.TASK_STATE_CANCELED:
|
|
232
|
+
this.sink.clearPending();
|
|
233
|
+
this.sink.setState('canceled');
|
|
234
|
+
break;
|
|
235
|
+
case TaskState.TASK_STATE_INPUT_REQUIRED:
|
|
236
|
+
this.sink.setPending({
|
|
237
|
+
id: this.taskId || randomUUID(),
|
|
238
|
+
kind: 'input',
|
|
239
|
+
prompt: statusText || 'The A2A agent requires additional input.'
|
|
240
|
+
});
|
|
241
|
+
break;
|
|
242
|
+
case TaskState.TASK_STATE_FAILED:
|
|
243
|
+
case TaskState.TASK_STATE_REJECTED:
|
|
244
|
+
case TaskState.TASK_STATE_AUTH_REQUIRED:
|
|
245
|
+
this.sink.clearPending();
|
|
246
|
+
this.sink.setState('failed', statusText || taskStateError(status.state));
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
processArtifact(artifact, append, complete) {
|
|
251
|
+
if (!artifact)
|
|
252
|
+
return;
|
|
253
|
+
const mapped = artifactFromA2A(artifact);
|
|
254
|
+
const previous = this.artifactCache.get(mapped.id || '');
|
|
255
|
+
const next = append && previous
|
|
256
|
+
? {
|
|
257
|
+
...previous,
|
|
258
|
+
...mapped,
|
|
259
|
+
text: `${previous.text || ''}${mapped.text || ''}` || undefined,
|
|
260
|
+
metadata: {
|
|
261
|
+
...previous.metadata,
|
|
262
|
+
...mapped.metadata,
|
|
263
|
+
complete
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
: {
|
|
267
|
+
...mapped,
|
|
268
|
+
metadata: { ...mapped.metadata, complete }
|
|
269
|
+
};
|
|
270
|
+
if (next.id)
|
|
271
|
+
this.artifactCache.set(next.id, next);
|
|
272
|
+
this.sink.addArtifact(next);
|
|
273
|
+
this.sink.emit('artifact', next);
|
|
274
|
+
}
|
|
275
|
+
captureIds(taskId, contextId) {
|
|
276
|
+
if (taskId) {
|
|
277
|
+
this.taskId = taskId;
|
|
278
|
+
this.sink.setProtocolSessionId(taskId);
|
|
279
|
+
}
|
|
280
|
+
if (contextId)
|
|
281
|
+
this.contextId = contextId;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function createResolver(config) {
|
|
285
|
+
return new DefaultAgentCardResolver({
|
|
286
|
+
fetchImpl: authenticatedFetch(config),
|
|
287
|
+
legacyCompat: { enabled: true }
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
function resolveAgentCard(resolver, config) {
|
|
291
|
+
if (config.agentCardUrl)
|
|
292
|
+
return resolver.resolve(config.agentCardUrl, '');
|
|
293
|
+
if (config.agentUrl)
|
|
294
|
+
return resolver.resolve(config.agentUrl);
|
|
295
|
+
throw new Error('A2A Agent Card URL is required');
|
|
296
|
+
}
|
|
297
|
+
function preferredTransports(config) {
|
|
298
|
+
if (config.preferredTransport === 'jsonrpc')
|
|
299
|
+
return ['JSONRPC'];
|
|
300
|
+
if (config.preferredTransport === 'http-json')
|
|
301
|
+
return ['HTTP+JSON'];
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
function authenticatedFetch(config) {
|
|
305
|
+
return async (input, init = {}) => {
|
|
306
|
+
const headers = new Headers(init.headers);
|
|
307
|
+
if (config.auth?.type === 'bearer' && config.auth.value) {
|
|
308
|
+
headers.set('Authorization', `Bearer ${config.auth.value}`);
|
|
309
|
+
}
|
|
310
|
+
else if (config.auth?.type === 'header' &&
|
|
311
|
+
config.auth.headerName &&
|
|
312
|
+
config.auth.value) {
|
|
313
|
+
headers.set(config.auth.headerName, config.auth.value);
|
|
314
|
+
}
|
|
315
|
+
return fetch(input, {
|
|
316
|
+
...init,
|
|
317
|
+
headers,
|
|
318
|
+
signal: init.signal || AbortSignal.timeout(config.timeoutMs || 60_000)
|
|
319
|
+
});
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function artifactFromA2A(artifact) {
|
|
323
|
+
const text = textParts(artifact.parts).join('') || undefined;
|
|
324
|
+
const urlPart = artifact.parts.find((part) => part.content?.$case === 'url');
|
|
325
|
+
const rawPart = artifact.parts.find((part) => part.content?.$case === 'raw');
|
|
326
|
+
const dataParts = artifact.parts
|
|
327
|
+
.filter((part) => part.content?.$case === 'data')
|
|
328
|
+
.map((part) => part.content?.$case === 'data' ? part.content.value : undefined);
|
|
329
|
+
return {
|
|
330
|
+
id: artifact.artifactId,
|
|
331
|
+
name: artifact.name || artifact.artifactId,
|
|
332
|
+
description: artifact.description || undefined,
|
|
333
|
+
text,
|
|
334
|
+
url: urlPart?.content?.$case === 'url' ? urlPart.content.value : undefined,
|
|
335
|
+
bytesBase64: rawPart?.content?.$case === 'raw'
|
|
336
|
+
? Buffer.from(rawPart.content.value).toString('base64')
|
|
337
|
+
: undefined,
|
|
338
|
+
mediaType: artifact.parts.find((part) => part.mediaType)?.mediaType || undefined,
|
|
339
|
+
data: dataParts.length ? dataParts : undefined,
|
|
340
|
+
metadata: artifact.metadata || undefined
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function textParts(parts) {
|
|
344
|
+
return parts.flatMap((part) => part.content?.$case === 'text' && part.content.value ? [part.content.value] : []);
|
|
345
|
+
}
|
|
346
|
+
function taskStateError(state) {
|
|
347
|
+
if (state === TaskState.TASK_STATE_AUTH_REQUIRED) {
|
|
348
|
+
return 'A2A agent requires authentication';
|
|
349
|
+
}
|
|
350
|
+
if (state === TaskState.TASK_STATE_REJECTED)
|
|
351
|
+
return 'A2A agent rejected the task';
|
|
352
|
+
return 'A2A task failed';
|
|
353
|
+
}
|
|
354
|
+
function abortMessage(error, signal) {
|
|
355
|
+
if (signal.aborted) {
|
|
356
|
+
return signal.reason instanceof Error ? signal.reason.message : 'A2A request aborted';
|
|
357
|
+
}
|
|
358
|
+
return errorMessage(error);
|
|
359
|
+
}
|
|
360
|
+
function disabledView(id, config) {
|
|
361
|
+
return {
|
|
362
|
+
id,
|
|
363
|
+
name: config.name || id,
|
|
364
|
+
description: config.description,
|
|
365
|
+
protocol: 'a2a',
|
|
366
|
+
ready: false,
|
|
367
|
+
enabled: false,
|
|
368
|
+
error: 'Agent is disabled',
|
|
369
|
+
checkedAt: Date.now()
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function errorMessage(error) {
|
|
373
|
+
return error instanceof Error ? error.message : String(error);
|
|
374
|
+
}
|
package/dist/acp/runtime.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentDriver } from '../drivers/index.js';
|
|
2
2
|
import type { AcpSessionSink } from '../session-contract.js';
|
|
3
|
+
import type { AgentdInputAttachment } from '../types.js';
|
|
3
4
|
export declare class AcpProcessRuntime {
|
|
4
5
|
private readonly driver;
|
|
5
6
|
private readonly sink;
|
|
@@ -9,10 +10,14 @@ export declare class AcpProcessRuntime {
|
|
|
9
10
|
private pendingInput?;
|
|
10
11
|
private disposed;
|
|
11
12
|
private prompting;
|
|
12
|
-
|
|
13
|
+
private readonly maxStderrChunkChars;
|
|
14
|
+
private readonly promptTimeoutMs;
|
|
15
|
+
private promptCapabilities;
|
|
16
|
+
private inputDirectory?;
|
|
17
|
+
constructor(driver: AgentDriver, sink: AcpSessionSink, maxStderrChunkChars?: number, promptTimeoutMs?: number);
|
|
13
18
|
start(workspace: string): Promise<void>;
|
|
14
|
-
prompt(message: string): Promise<void>;
|
|
15
|
-
respondPending(message: string): Promise<void>;
|
|
19
|
+
prompt(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
|
|
20
|
+
respondPending(message: string, _attachments?: AgentdInputAttachment[]): Promise<void>;
|
|
16
21
|
cancel(): Promise<void>;
|
|
17
22
|
dispose(): Promise<void>;
|
|
18
23
|
private requestPermission;
|
|
@@ -20,7 +25,11 @@ export declare class AcpProcessRuntime {
|
|
|
20
25
|
private requestInput;
|
|
21
26
|
private finishInput;
|
|
22
27
|
private sessionUpdate;
|
|
28
|
+
private captureToolArtifacts;
|
|
29
|
+
private captureArtifact;
|
|
23
30
|
private captureStderr;
|
|
24
31
|
private onProcessFailure;
|
|
25
32
|
private requireSessionId;
|
|
33
|
+
private promptBlocks;
|
|
34
|
+
private writeResourceLink;
|
|
26
35
|
}
|