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/a2a/runtime.js
CHANGED
|
@@ -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();
|
|
@@ -57,6 +59,8 @@ export class A2AClientRuntime {
|
|
|
57
59
|
this.promptTimeoutMs = promptTimeoutMs;
|
|
58
60
|
}
|
|
59
61
|
async start() {
|
|
62
|
+
if (this.disposed)
|
|
63
|
+
throw new Error('A2A runtime is disposed');
|
|
60
64
|
const resolver = createResolver(this.config);
|
|
61
65
|
this.card = await resolveAgentCard(resolver, this.config);
|
|
62
66
|
const fetchImpl = authenticatedFetch(this.config);
|
|
@@ -70,6 +74,8 @@ export class A2AClientRuntime {
|
|
|
70
74
|
]
|
|
71
75
|
});
|
|
72
76
|
this.client = await factory.createFromAgentCard(this.card);
|
|
77
|
+
if (this.disposed)
|
|
78
|
+
throw new Error('A2A runtime is disposed');
|
|
73
79
|
this.sink.setState('created');
|
|
74
80
|
}
|
|
75
81
|
async prompt(message, attachments = []) {
|
|
@@ -80,6 +86,8 @@ export class A2AClientRuntime {
|
|
|
80
86
|
if (this.disposed)
|
|
81
87
|
throw new Error('A2A runtime is disposed');
|
|
82
88
|
this.prompting = true;
|
|
89
|
+
let finish;
|
|
90
|
+
this.promptFinished = new Promise((resolve) => { finish = resolve; });
|
|
83
91
|
this.sawTaskStatus = false;
|
|
84
92
|
this.sawAgentMessage = false;
|
|
85
93
|
this.completionProof = undefined;
|
|
@@ -94,9 +102,15 @@ export class A2AClientRuntime {
|
|
|
94
102
|
for await (const response of this.client.sendMessageStream(request, {
|
|
95
103
|
signal: controller.signal
|
|
96
104
|
})) {
|
|
105
|
+
if (controller.signal.aborted || this.disposed)
|
|
106
|
+
break;
|
|
107
|
+
// An accepted answer supersedes the old input-required stream.
|
|
108
|
+
// Its trailing status must not reopen the answered request.
|
|
109
|
+
if (this.queuedResponse)
|
|
110
|
+
continue;
|
|
97
111
|
this.processResponse(response);
|
|
98
112
|
}
|
|
99
|
-
if (this.sink.state === 'running') {
|
|
113
|
+
if (this.sink.state === 'running' && !this.queuedResponse) {
|
|
100
114
|
if (this.completionProof) {
|
|
101
115
|
this.sink.completeTurn(this.completionProof);
|
|
102
116
|
}
|
|
@@ -114,7 +128,7 @@ export class A2AClientRuntime {
|
|
|
114
128
|
}
|
|
115
129
|
}
|
|
116
130
|
catch (error) {
|
|
117
|
-
if (this.sink.state !== 'canceled') {
|
|
131
|
+
if (this.sink.state !== 'canceled' && !this.queuedResponse) {
|
|
118
132
|
this.sink.setState('failed', abortMessage(error, controller.signal));
|
|
119
133
|
}
|
|
120
134
|
}
|
|
@@ -123,9 +137,13 @@ export class A2AClientRuntime {
|
|
|
123
137
|
if (this.activeController === controller)
|
|
124
138
|
this.activeController = undefined;
|
|
125
139
|
this.prompting = false;
|
|
140
|
+
finish();
|
|
141
|
+
this.promptFinished = undefined;
|
|
126
142
|
}
|
|
127
143
|
}
|
|
128
144
|
async respondPending(response, attachments = []) {
|
|
145
|
+
if (!this.isAvailable())
|
|
146
|
+
throw new Error('A2A runtime is unavailable');
|
|
129
147
|
if (this.sink.state !== 'input_required') {
|
|
130
148
|
throw new Error('A2A session is not waiting for input');
|
|
131
149
|
}
|
|
@@ -138,10 +156,23 @@ export class A2AClientRuntime {
|
|
|
138
156
|
: response.message ?? response.optionId ?? '';
|
|
139
157
|
if (!message.trim())
|
|
140
158
|
throw new Error('A2A pending response is empty');
|
|
159
|
+
const previous = this.promptFinished || Promise.resolve();
|
|
160
|
+
this.queuedResponse = true;
|
|
141
161
|
this.sink.clearPending();
|
|
142
|
-
|
|
162
|
+
this.sink.setState('running');
|
|
163
|
+
void previous.then(async () => {
|
|
164
|
+
this.queuedResponse = false;
|
|
165
|
+
if (this.disposed || this.sink.state === 'canceled')
|
|
166
|
+
return;
|
|
167
|
+
await this.prompt(message, attachments);
|
|
168
|
+
}).catch((error) => {
|
|
169
|
+
if (!this.disposed && this.sink.state !== 'canceled')
|
|
170
|
+
this.sink.setState('failed', errorMessage(error));
|
|
171
|
+
});
|
|
143
172
|
}
|
|
144
173
|
async cancel() {
|
|
174
|
+
this.sink.clearPending();
|
|
175
|
+
this.sink.setState('canceled');
|
|
145
176
|
this.activeController?.abort(new Error('A2A session canceled'));
|
|
146
177
|
if (this.client && this.taskId) {
|
|
147
178
|
try {
|
|
@@ -162,6 +193,9 @@ export class A2AClientRuntime {
|
|
|
162
193
|
this.activeController?.abort(new Error('A2A runtime disposed'));
|
|
163
194
|
this.activeController = undefined;
|
|
164
195
|
}
|
|
196
|
+
isAvailable() {
|
|
197
|
+
return !this.disposed && Boolean(this.client && this.card);
|
|
198
|
+
}
|
|
165
199
|
messageRequest(text, attachments) {
|
|
166
200
|
return {
|
|
167
201
|
tenant: '',
|
|
@@ -338,12 +372,43 @@ function createResolver(config) {
|
|
|
338
372
|
legacyCompat: { enabled: true }
|
|
339
373
|
});
|
|
340
374
|
}
|
|
341
|
-
function resolveAgentCard(resolver, config) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
375
|
+
async function resolveAgentCard(resolver, config) {
|
|
376
|
+
const origin = configuredOrigin(config);
|
|
377
|
+
const card = config.agentCardUrl
|
|
378
|
+
? await resolver.resolve(config.agentCardUrl, '')
|
|
379
|
+
: await resolver.resolve(config.agentUrl);
|
|
380
|
+
// The resolver normalizes v0.3 url/additionalInterfaces into this array.
|
|
381
|
+
// Validate every advertised endpoint before transport selection or probing.
|
|
382
|
+
if (!Array.isArray(card.supportedInterfaces) || !card.supportedInterfaces.length) {
|
|
383
|
+
throw new Error('A2A Agent Card must declare at least one interface URL');
|
|
384
|
+
}
|
|
385
|
+
for (const endpoint of card.supportedInterfaces)
|
|
386
|
+
assertSameOrigin(endpoint?.url, origin);
|
|
387
|
+
return card;
|
|
388
|
+
}
|
|
389
|
+
function a2aUrl(value) {
|
|
390
|
+
let url;
|
|
391
|
+
try {
|
|
392
|
+
url = new URL(value);
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
throw new Error('A2A URL is invalid');
|
|
396
|
+
}
|
|
397
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) {
|
|
398
|
+
throw new Error('A2A URL must use http(s) without credentials or a fragment');
|
|
399
|
+
}
|
|
400
|
+
return url;
|
|
401
|
+
}
|
|
402
|
+
function configuredOrigin(config) {
|
|
403
|
+
const value = config.agentCardUrl || config.agentUrl;
|
|
404
|
+
if (!value)
|
|
405
|
+
throw new Error('A2A Agent Card URL is required');
|
|
406
|
+
return a2aUrl(value).origin;
|
|
407
|
+
}
|
|
408
|
+
function assertSameOrigin(value, origin) {
|
|
409
|
+
if (a2aUrl(value).origin !== origin) {
|
|
410
|
+
throw new Error('A2A URL must have the same origin as the configured Agent Card URL');
|
|
411
|
+
}
|
|
347
412
|
}
|
|
348
413
|
function preferredTransports(config) {
|
|
349
414
|
if (config.preferredTransport === 'jsonrpc')
|
|
@@ -353,8 +418,12 @@ function preferredTransports(config) {
|
|
|
353
418
|
return undefined;
|
|
354
419
|
}
|
|
355
420
|
function authenticatedFetch(config) {
|
|
421
|
+
const origin = configuredOrigin(config);
|
|
356
422
|
return async (input, init = {}) => {
|
|
357
|
-
|
|
423
|
+
// The final guard also covers SDK-derived paths and future card flows.
|
|
424
|
+
// Check before attaching credentials; never let fetch follow redirects.
|
|
425
|
+
assertSameOrigin(input instanceof Request ? input.url : String(input), origin);
|
|
426
|
+
const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined));
|
|
358
427
|
if (config.auth?.type === 'bearer' && config.auth.value) {
|
|
359
428
|
headers.set('Authorization', `Bearer ${config.auth.value}`);
|
|
360
429
|
}
|
|
@@ -366,6 +435,7 @@ function authenticatedFetch(config) {
|
|
|
366
435
|
return fetch(input, {
|
|
367
436
|
...init,
|
|
368
437
|
headers,
|
|
438
|
+
redirect: 'error',
|
|
369
439
|
signal: init.signal || AbortSignal.timeout(config.timeoutMs || 60_000)
|
|
370
440
|
});
|
|
371
441
|
};
|
package/dist/acp/runtime.d.ts
CHANGED
|
@@ -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;
|
package/dist/acp/runtime.js
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { mkdir,
|
|
2
|
+
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { Readable, Writable } from 'node:stream';
|
|
5
5
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
6
|
import * as acp from '@agentclientprotocol/sdk';
|
|
7
7
|
import { terminateProcessTree } from '../process-tree.js';
|
|
8
8
|
import { TURN_COMPLETION_CONTRACT } from '../completion-contract.js';
|
|
9
|
-
|
|
9
|
+
import { readContainedFile } from '../workspace.js';
|
|
10
|
+
// Matches MAX_PUBLISHED_FILE_BYTES in session.ts; larger files are encoded only to be dropped by the base64 budget.
|
|
11
|
+
const MAX_MEDIA_MARKER_FILE_BYTES = 12 * 1024 * 1024;
|
|
12
|
+
// The agent controls how many markers it emits, so bound the reads per turn.
|
|
13
|
+
const MAX_MEDIA_MARKER_ARTIFACTS = 8;
|
|
10
14
|
export class AcpProcessRuntime {
|
|
11
15
|
driver;
|
|
12
16
|
sink;
|
|
17
|
+
allowedRoots;
|
|
18
|
+
startupTimeoutMs;
|
|
13
19
|
process;
|
|
14
20
|
connection;
|
|
15
21
|
pending;
|
|
@@ -22,9 +28,11 @@ export class AcpProcessRuntime {
|
|
|
22
28
|
inputDirectory;
|
|
23
29
|
workspace;
|
|
24
30
|
promptOutput = '';
|
|
25
|
-
constructor(driver, sink, maxStderrChunkChars = 16 * 1024, promptTimeoutMs = 30 * 60 * 1000) {
|
|
31
|
+
constructor(driver, sink, maxStderrChunkChars = 16 * 1024, promptTimeoutMs = 30 * 60 * 1000, allowedRoots = [], startupTimeoutMs = 30_000) {
|
|
26
32
|
this.driver = driver;
|
|
27
33
|
this.sink = sink;
|
|
34
|
+
this.allowedRoots = allowedRoots;
|
|
35
|
+
this.startupTimeoutMs = startupTimeoutMs;
|
|
28
36
|
const limit = Number.isFinite(maxStderrChunkChars)
|
|
29
37
|
? Math.floor(maxStderrChunkChars)
|
|
30
38
|
: 16 * 1024;
|
|
@@ -32,6 +40,21 @@ export class AcpProcessRuntime {
|
|
|
32
40
|
this.promptTimeoutMs = Math.max(10_000, Math.trunc(promptTimeoutMs));
|
|
33
41
|
}
|
|
34
42
|
async start(workspace) {
|
|
43
|
+
if (this.disposed)
|
|
44
|
+
throw new Error('ACP runtime is disposed');
|
|
45
|
+
if (this.process)
|
|
46
|
+
throw new Error('ACP runtime is already started');
|
|
47
|
+
try {
|
|
48
|
+
await withTimeout(this.connect(workspace), this.startupTimeoutMs, 'ACP startup timed out');
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (this.sink.state !== 'canceled')
|
|
52
|
+
this.onProcessFailure(error);
|
|
53
|
+
await this.dispose();
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async connect(workspace) {
|
|
35
58
|
if (this.process)
|
|
36
59
|
throw new Error('ACP runtime is already started');
|
|
37
60
|
this.workspace = workspace;
|
|
@@ -69,6 +92,8 @@ export class AcpProcessRuntime {
|
|
|
69
92
|
version: '0.2.2'
|
|
70
93
|
}
|
|
71
94
|
});
|
|
95
|
+
if (this.disposed)
|
|
96
|
+
throw new Error('ACP runtime is disposed');
|
|
72
97
|
if (initialize.protocolVersion !== acp.PROTOCOL_VERSION) {
|
|
73
98
|
this.sink.emit('terminal_output', {
|
|
74
99
|
stream: 'system',
|
|
@@ -80,10 +105,14 @@ export class AcpProcessRuntime {
|
|
|
80
105
|
cwd: workspace,
|
|
81
106
|
mcpServers: []
|
|
82
107
|
});
|
|
108
|
+
if (this.disposed)
|
|
109
|
+
throw new Error('ACP runtime is disposed');
|
|
83
110
|
this.sink.setAcpSessionId(String(session.sessionId));
|
|
84
111
|
this.sink.setState('created');
|
|
85
112
|
}
|
|
86
113
|
async prompt(message, attachments = []) {
|
|
114
|
+
if (this.disposed)
|
|
115
|
+
throw new Error('ACP runtime is disposed');
|
|
87
116
|
if (!this.connection)
|
|
88
117
|
throw new Error('ACP runtime is not connected');
|
|
89
118
|
if (this.prompting)
|
|
@@ -199,11 +228,12 @@ export class AcpProcessRuntime {
|
|
|
199
228
|
if (this.pendingInput) {
|
|
200
229
|
this.finishInput('cancel');
|
|
201
230
|
}
|
|
202
|
-
|
|
231
|
+
this.sink.setState('canceled');
|
|
232
|
+
if (this.connection) {
|
|
203
233
|
try {
|
|
204
|
-
await this.connection.agent.notify(acp.methods.agent.session.cancel, {
|
|
234
|
+
await withTimeout(this.connection.agent.notify(acp.methods.agent.session.cancel, {
|
|
205
235
|
sessionId: this.requireSessionId()
|
|
206
|
-
});
|
|
236
|
+
}), 2_000, 'ACP cancellation notification timed out');
|
|
207
237
|
}
|
|
208
238
|
catch (error) {
|
|
209
239
|
this.sink.emit('terminal_output', {
|
|
@@ -229,8 +259,16 @@ export class AcpProcessRuntime {
|
|
|
229
259
|
this.pendingInput.resolve({ action: 'cancel' });
|
|
230
260
|
this.pendingInput = undefined;
|
|
231
261
|
}
|
|
232
|
-
this.connection
|
|
262
|
+
const connection = this.connection;
|
|
233
263
|
this.connection = undefined;
|
|
264
|
+
try {
|
|
265
|
+
connection?.close();
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
this.sink.emit('terminal_output', {
|
|
269
|
+
stream: 'system', text: `ACP connection close failed: ${error instanceof Error ? error.message : String(error)}`
|
|
270
|
+
});
|
|
271
|
+
}
|
|
234
272
|
const child = this.process;
|
|
235
273
|
this.process = undefined;
|
|
236
274
|
if (child) {
|
|
@@ -243,6 +281,10 @@ export class AcpProcessRuntime {
|
|
|
243
281
|
this.inputDirectory = undefined;
|
|
244
282
|
}
|
|
245
283
|
}
|
|
284
|
+
isAvailable() {
|
|
285
|
+
return !this.disposed && Boolean(this.connection) &&
|
|
286
|
+
(!this.process || (this.process.exitCode === null && this.process.signalCode === null));
|
|
287
|
+
}
|
|
246
288
|
requestPermission(params) {
|
|
247
289
|
if (this.driver.permissionPolicy === 'deny') {
|
|
248
290
|
const reject = params.options.find((option) => option.kind.startsWith('reject'));
|
|
@@ -428,13 +470,23 @@ export class AcpProcessRuntime {
|
|
|
428
470
|
if (!markers.length)
|
|
429
471
|
return;
|
|
430
472
|
this.sink.replaceOutput(withoutMediaMarkers(this.promptOutput));
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
473
|
+
const roots = this.allowedRoots.length
|
|
474
|
+
? this.allowedRoots
|
|
475
|
+
: this.workspace
|
|
476
|
+
? [this.workspace]
|
|
477
|
+
: [];
|
|
478
|
+
const unique = Array.from(new Set(markers));
|
|
479
|
+
const accepted = unique.slice(0, MAX_MEDIA_MARKER_ARTIFACTS);
|
|
480
|
+
const ignored = unique.length - accepted.length;
|
|
481
|
+
if (ignored > 0) {
|
|
482
|
+
this.sink.emit('terminal_output', {
|
|
483
|
+
stream: 'system',
|
|
484
|
+
text: `Ignored ${ignored} MEDIA marker(s); at most ${MAX_MEDIA_MARKER_ARTIFACTS} artifacts can be attached per turn.`
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
for (const marker of accepted) {
|
|
436
488
|
try {
|
|
437
|
-
const artifact = await artifactFromMediaMarker(marker, `media:${randomUUID()}`,
|
|
489
|
+
const artifact = await artifactFromMediaMarker(marker, `media:${randomUUID()}`, roots);
|
|
438
490
|
this.sink.addArtifact(artifact);
|
|
439
491
|
this.sink.emit('artifact', artifact);
|
|
440
492
|
}
|
|
@@ -679,7 +731,7 @@ function withoutMediaMarkers(output) {
|
|
|
679
731
|
.join('\n')
|
|
680
732
|
.trim();
|
|
681
733
|
}
|
|
682
|
-
async function artifactFromMediaMarker(marker, id,
|
|
734
|
+
async function artifactFromMediaMarker(marker, id, roots) {
|
|
683
735
|
if (/^https?:\/\//i.test(marker)) {
|
|
684
736
|
const filename = artifactFilename(marker, undefined, 'resource', undefined);
|
|
685
737
|
return {
|
|
@@ -690,29 +742,19 @@ async function artifactFromMediaMarker(marker, id, workspace) {
|
|
|
690
742
|
metadata: { source: 'acp_media_marker' }
|
|
691
743
|
};
|
|
692
744
|
}
|
|
693
|
-
const localPath = marker.startsWith('file:')
|
|
694
|
-
|
|
695
|
-
:
|
|
696
|
-
|
|
697
|
-
? localPath
|
|
698
|
-
: path.resolve(workspace || process.cwd(), localPath));
|
|
699
|
-
const details = await stat(resolved);
|
|
700
|
-
if (!details.isFile()) {
|
|
701
|
-
throw new Error('declared path is not a regular file');
|
|
702
|
-
}
|
|
703
|
-
if (details.size > MAX_MEDIA_MARKER_FILE_BYTES) {
|
|
704
|
-
throw new Error(`declared file exceeds the ${MAX_MEDIA_MARKER_FILE_BYTES} byte limit`);
|
|
705
|
-
}
|
|
706
|
-
const filename = path.basename(resolved);
|
|
745
|
+
const localPath = marker.startsWith('file:') ? fileURLToPath(marker) : marker;
|
|
746
|
+
const file = await readContainedFile(roots, localPath, {
|
|
747
|
+
maxBytes: MAX_MEDIA_MARKER_FILE_BYTES
|
|
748
|
+
});
|
|
707
749
|
return {
|
|
708
750
|
id,
|
|
709
|
-
name: filename,
|
|
710
|
-
filename,
|
|
711
|
-
mediaType: mediaTypeForPath(filename),
|
|
712
|
-
bytesBase64:
|
|
751
|
+
name: file.filename,
|
|
752
|
+
filename: file.filename,
|
|
753
|
+
mediaType: mediaTypeForPath(file.filename),
|
|
754
|
+
bytesBase64: file.bytes.toString('base64'),
|
|
713
755
|
metadata: {
|
|
714
756
|
source: 'acp_media_marker',
|
|
715
|
-
size:
|
|
757
|
+
size: file.size
|
|
716
758
|
}
|
|
717
759
|
};
|
|
718
760
|
}
|
|
@@ -4,6 +4,6 @@ export const TURN_COMPLETION_CONTRACT = [
|
|
|
4
4
|
'If you need user input or permission, use the protocol input/permission request and wait for the response.',
|
|
5
5
|
'Before ending the turn, verify the requested work and provide a non-empty final response.',
|
|
6
6
|
'The final response must summarize the outcome, verification performed, and workspace-relative paths for generated files.',
|
|
7
|
-
'Attach generated resources or artifacts through the protocol when supported. If the runtime only reports a local deliverable path, put each requested file on its own final line as MEDIA:<path
|
|
7
|
+
'Attach generated resources or artifacts through the protocol when supported. If the runtime only reports a local deliverable path, put each requested file on its own final line as MEDIA:<path>, where <path> stays inside the configured workspace roots; Agent Nexus snapshots that declared file as an artifact before the turn completes, refuses paths outside those roots, and attaches at most 8 files per turn.',
|
|
8
8
|
'</agent_nexus_completion_contract>'
|
|
9
9
|
].join('\n');
|
package/dist/control-plane.js
CHANGED
|
@@ -188,7 +188,17 @@ export class AgentdControlPlane {
|
|
|
188
188
|
if (!workspaceRoots.length) {
|
|
189
189
|
throw new ControlPlaneError(400, 'workspaceRoots must contain at least one path');
|
|
190
190
|
}
|
|
191
|
-
await WorkspacePolicy.create(workspaceRoots);
|
|
191
|
+
const policy = await WorkspacePolicy.create(workspaceRoots);
|
|
192
|
+
for (const [id, agent] of Object.entries(this.config.agents)) {
|
|
193
|
+
if (agent.protocol === 'a2a' || !agent.workspace)
|
|
194
|
+
continue;
|
|
195
|
+
try {
|
|
196
|
+
await policy.resolve(agent.workspace);
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
throw new ControlPlaneError(409, `Workspace change would exclude agent: ${id}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
192
202
|
const raw = await this.readRawConfig();
|
|
193
203
|
raw.workspaceRoots = workspaceRoots;
|
|
194
204
|
await this.persist(raw);
|
package/dist/index.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export declare function startAgentd(configPath: string): Promise<{
|
|
|
8
8
|
sessions: SessionManager;
|
|
9
9
|
runStore: RunStore;
|
|
10
10
|
controlPlane: AgentdControlPlane;
|
|
11
|
-
close()
|
|
11
|
+
close: () => Promise<void>;
|
|
12
12
|
}>;
|
|
13
13
|
export declare function closeServer(server: Server, graceMs?: number): Promise<void>;
|
|
14
14
|
export * from './config.js';
|
package/dist/index.js
CHANGED
|
@@ -16,17 +16,35 @@ export async function startAgentd(configPath) {
|
|
|
16
16
|
sessions.startCleanup();
|
|
17
17
|
const controlPlane = new AgentdControlPlane(absoluteConfigPath, config, sessions);
|
|
18
18
|
const server = createAgentdServer(config, sessions, controlPlane);
|
|
19
|
-
|
|
19
|
+
let closing;
|
|
20
|
+
const close = () => closing ??= (async () => {
|
|
21
|
+
const results = await Promise.allSettled([closeServer(server), sessions.shutdown()]);
|
|
22
|
+
await runStore.flush();
|
|
23
|
+
const failed = results.find((result) => result.status === 'rejected');
|
|
24
|
+
if (failed)
|
|
25
|
+
throw failed.reason;
|
|
26
|
+
})();
|
|
27
|
+
try {
|
|
28
|
+
await listen(server, config.listen.port, config.listen.host);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
await sessions.shutdown();
|
|
32
|
+
await runStore.flush();
|
|
33
|
+
throw error;
|
|
34
|
+
}
|
|
35
|
+
server.on('error', (error) => {
|
|
36
|
+
console.error(JSON.stringify({ level: 'error', event: 'server_failed', message: error.message }));
|
|
37
|
+
void close().catch((closeError) => {
|
|
38
|
+
console.error(JSON.stringify({ level: 'error', event: 'server_shutdown_failed', message: String(closeError) }));
|
|
39
|
+
});
|
|
40
|
+
});
|
|
20
41
|
return {
|
|
21
42
|
config,
|
|
22
43
|
server,
|
|
23
44
|
sessions,
|
|
24
45
|
runStore,
|
|
25
46
|
controlPlane,
|
|
26
|
-
|
|
27
|
-
await Promise.all([closeServer(server), sessions.shutdown()]);
|
|
28
|
-
await runStore.flush();
|
|
29
|
-
}
|
|
47
|
+
close
|
|
30
48
|
};
|
|
31
49
|
}
|
|
32
50
|
function listen(server, port, host) {
|
package/dist/run-store.d.ts
CHANGED
package/dist/run-store.js
CHANGED
|
@@ -144,8 +144,17 @@ export class RunStore {
|
|
|
144
144
|
run.agentName.toLowerCase().includes(needle) ||
|
|
145
145
|
run.id.toLowerCase().includes(needle));
|
|
146
146
|
const total = matched.length;
|
|
147
|
+
const stats = { active: 0, completed: 0, failed: 0 };
|
|
148
|
+
for (const run of matched) {
|
|
149
|
+
if (isActive(run.state))
|
|
150
|
+
stats.active++;
|
|
151
|
+
if (run.state === 'completed')
|
|
152
|
+
stats.completed++;
|
|
153
|
+
if (run.state === 'failed')
|
|
154
|
+
stats.failed++;
|
|
155
|
+
}
|
|
147
156
|
const runs = matched.slice(0, limit).map(publicView);
|
|
148
|
-
return { runs, total };
|
|
157
|
+
return { runs, total, stats };
|
|
149
158
|
}
|
|
150
159
|
async flush() {
|
|
151
160
|
this.assertInitialized();
|
|
@@ -18,6 +18,7 @@ export interface AcpSessionSink extends AgentSessionSink {
|
|
|
18
18
|
setAcpSessionId(id: string): void;
|
|
19
19
|
}
|
|
20
20
|
export interface AgentSessionRuntime {
|
|
21
|
+
isAvailable?(): boolean;
|
|
21
22
|
start(workspace?: string): Promise<void>;
|
|
22
23
|
prompt(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
|
|
23
24
|
respondPending(response: AgentdPendingResponse | string, attachments?: AgentdInputAttachment[]): Promise<void>;
|
package/dist/session.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { SessionEventLog } from './events.js';
|
|
|
3
3
|
import { RunStore, type RunListQuery } from './run-store.js';
|
|
4
4
|
import type { AcpSessionSink, AgentSessionRuntime, AgentSessionSink } from './session-contract.js';
|
|
5
5
|
import type { AgentdAgentView, AgentdArtifact, AgentdConfig, AgentdEvent, AgentdEventType, AgentdInputAttachmentView, AgentdPendingRequest, AgentdPendingResponse, AgentdProtocol, AgentdSessionState, AgentdSessionView, AgentdTurnCompletionProof } from './types.js';
|
|
6
|
-
import type
|
|
6
|
+
import { type WorkspacePolicy } from './workspace.js';
|
|
7
7
|
export declare class ManagedSession implements AgentSessionSink, AcpSessionSink {
|
|
8
8
|
readonly agentId: string;
|
|
9
9
|
readonly protocol: AgentdProtocol;
|
|
@@ -74,6 +74,11 @@ export declare class SessionManager {
|
|
|
74
74
|
listRuns(query?: RunListQuery): {
|
|
75
75
|
runs: import("./types.js").AgentdRunView[];
|
|
76
76
|
total: number;
|
|
77
|
+
stats: {
|
|
78
|
+
active: number;
|
|
79
|
+
completed: number;
|
|
80
|
+
failed: number;
|
|
81
|
+
};
|
|
77
82
|
};
|
|
78
83
|
getRun(id: string): import("./types.js").AgentdRunDetail;
|
|
79
84
|
message(id: string, message: string, attachmentIds?: string[]): Promise<AgentdSessionView>;
|