nexus-agentd 0.1.8 → 0.2.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.
- package/LICENSE +22 -22
- package/README.md +224 -446
- package/dist/a2a/runtime.d.ts +5 -4
- package/dist/a2a/runtime.js +83 -13
- package/dist/acp/runtime.d.ts +3 -8
- package/dist/acp/runtime.js +63 -79
- package/dist/completion-contract.d.ts +1 -0
- package/dist/completion-contract.js +9 -0
- package/dist/config.js +1 -15
- package/dist/control-plane.d.ts +0 -1
- package/dist/control-plane.js +1 -19
- package/dist/run-store.d.ts +1 -1
- package/dist/server.js +63 -248
- package/dist/session-contract.d.ts +3 -2
- package/dist/session.d.ts +11 -2
- package/dist/session.js +202 -17
- package/dist/types.d.ts +25 -45
- package/dist/webui/app.js +912 -1074
- package/dist/webui/icons.d.ts +0 -1
- package/dist/webui/icons.js +0 -1
- package/dist/webui/index.js +10 -10
- package/dist/webui/markup.js +65 -66
- package/dist/webui/styles.js +256 -268
- package/nexus-agentd.example.json +40 -47
- package/package.json +52 -52
- package/dist/agent-instructions.d.ts +0 -11
- package/dist/agent-instructions.js +0 -44
- package/dist/artifact-store.d.ts +0 -41
- package/dist/artifact-store.js +0 -340
- package/dist/workspace-files.d.ts +0 -48
- package/dist/workspace-files.js +0 -269
package/dist/server.js
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import { randomUUID, timingSafeEqual } from 'node:crypto';
|
|
2
2
|
import http from 'node:http';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { pipeline } from 'node:stream/promises';
|
|
5
|
-
import { ArtifactStoreError, contentDisposition, GatewayArtifactStore, mediaTypeForName } from './artifact-store.js';
|
|
6
3
|
import { AdminSessionStore } from './auth.js';
|
|
7
4
|
import { ControlPlaneError } from './control-plane.js';
|
|
8
5
|
import { RunNotFoundError, SessionNotFoundError, SessionRequestError } from './session.js';
|
|
9
6
|
import { redirectToAgentdWebUi, writeAgentdWebUi } from './webui/index.js';
|
|
10
|
-
import { WorkspaceFileError, WorkspaceFiles } from './workspace-files.js';
|
|
11
7
|
const ADMIN_COOKIE = 'agent_nexus_admin';
|
|
12
8
|
export function createAgentdServer(config, sessions, controlPlane) {
|
|
13
9
|
const adminSessions = new AdminSessionStore(config.adminSessionTtlMs || 12 * 60 * 60 * 1000);
|
|
14
10
|
const loginLimiter = new FailureRateLimiter(8, 60_000, 5 * 60_000);
|
|
15
11
|
const apiLimiter = new FailureRateLimiter(30, 60_000, 60_000);
|
|
16
|
-
const artifactStore = new GatewayArtifactStore(config.artifactStoragePath || path.resolve('.nexus-agentd-artifacts'), config.artifactTtlMs || 24 * 60 * 60 * 1000, config.maxArtifactBytes || 512 * 1024 * 1024, config.maxArtifactStorageBytes || 4 * 1024 * 1024 * 1024, config.maxPublishedArtifacts || 4096, config.maxConcurrentArtifactPublishes || 4);
|
|
17
|
-
const workspaceFiles = new WorkspaceFiles(() => controlPlane?.snapshot().workspaceRoots || config.workspaceRoots, config.maxArtifactBytes || 512 * 1024 * 1024);
|
|
18
|
-
artifactStore.startCleanup(config.cleanupIntervalMs);
|
|
19
12
|
let sseConnections = 0;
|
|
20
13
|
const server = http.createServer((request, response) => {
|
|
21
14
|
const requestId = randomUUID();
|
|
@@ -27,8 +20,6 @@ export function createAgentdServer(config, sessions, controlPlane) {
|
|
|
27
20
|
adminSessions,
|
|
28
21
|
loginLimiter,
|
|
29
22
|
apiLimiter,
|
|
30
|
-
artifactStore,
|
|
31
|
-
workspaceFiles,
|
|
32
23
|
request,
|
|
33
24
|
response,
|
|
34
25
|
acquireSse() {
|
|
@@ -67,14 +58,11 @@ export function createAgentdServer(config, sessions, controlPlane) {
|
|
|
67
58
|
server.headersTimeout = Math.min(server.requestTimeout, 20_000);
|
|
68
59
|
server.keepAliveTimeout = 5_000;
|
|
69
60
|
server.maxConnections = config.maxConnections || 256;
|
|
70
|
-
server.once('close', () =>
|
|
71
|
-
adminSessions.clear();
|
|
72
|
-
artifactStore.stopCleanup();
|
|
73
|
-
});
|
|
61
|
+
server.once('close', () => adminSessions.clear());
|
|
74
62
|
return server;
|
|
75
63
|
}
|
|
76
64
|
async function handleRequest(context) {
|
|
77
|
-
const { config, sessions, controlPlane, adminSessions, loginLimiter, apiLimiter,
|
|
65
|
+
const { config, sessions, controlPlane, adminSessions, loginLimiter, apiLimiter, request, response } = context;
|
|
78
66
|
const url = new URL(request.url || '/', 'http://localhost');
|
|
79
67
|
if (url.pathname === '/' && request.method === 'GET') {
|
|
80
68
|
redirectToAgentdWebUi(response);
|
|
@@ -84,12 +72,6 @@ async function handleRequest(context) {
|
|
|
84
72
|
writeAgentdWebUi(response);
|
|
85
73
|
return;
|
|
86
74
|
}
|
|
87
|
-
const artifactMatch = url.pathname.match(/^\/v1\/artifacts\/([a-f0-9]{64})\/(\d+)\/([^/]+)$/);
|
|
88
|
-
if (artifactMatch && request.method === 'GET') {
|
|
89
|
-
const artifact = await artifactStore.open(artifactMatch[1], artifactMatch[2], decodePathSegment(artifactMatch[3]));
|
|
90
|
-
await streamDownload(response, artifact.handle, artifact.name, artifact.size, artifact.mediaType);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
75
|
if (url.pathname === '/health' && request.method === 'GET') {
|
|
94
76
|
writeJson(response, 200, {
|
|
95
77
|
ok: true,
|
|
@@ -168,6 +150,10 @@ async function handleRequest(context) {
|
|
|
168
150
|
return;
|
|
169
151
|
}
|
|
170
152
|
const principal = authenticateApiKey(request, config, controlPlane, apiLimiter);
|
|
153
|
+
if (url.pathname === '/v1/meta' && request.method === 'GET') {
|
|
154
|
+
writeJson(response, 200, { instanceId: sessions.instanceId });
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
171
157
|
if (url.pathname === '/v1/agents' && request.method === 'GET') {
|
|
172
158
|
const ids = principal.scope.allAgents
|
|
173
159
|
? undefined
|
|
@@ -183,37 +169,9 @@ async function handleRequest(context) {
|
|
|
183
169
|
assertOnlyKeys(body, ['agentId', 'workspace']);
|
|
184
170
|
const agentId = requiredString(body.agentId, 'agentId');
|
|
185
171
|
assertAgentScope(principal, agentId);
|
|
186
|
-
writeJson(response, 201, await
|
|
172
|
+
writeJson(response, 201, await sessions.create(agentId, optionalString(body.workspace), principal.id));
|
|
187
173
|
return;
|
|
188
174
|
}
|
|
189
|
-
const filesMatch = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/files(?:\/(content|publish))?$/);
|
|
190
|
-
if (filesMatch) {
|
|
191
|
-
const sessionId = decodePathSegment(filesMatch[1]);
|
|
192
|
-
const session = ownedSession(sessions, sessionId, principal);
|
|
193
|
-
const root = await workspaceFiles.sessionRoot(session.workspace);
|
|
194
|
-
const action = filesMatch[2];
|
|
195
|
-
if (!action && request.method === 'GET') {
|
|
196
|
-
writeJson(response, 200, await workspaceFiles.list(root, url.searchParams.get('path') || ''));
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (action === 'content' && request.method === 'GET') {
|
|
200
|
-
const file = await workspaceFiles.open(root, requiredQuery(url, 'path'));
|
|
201
|
-
await streamDownload(response, file.handle, file.name, file.size, mediaTypeForName(file.name));
|
|
202
|
-
return;
|
|
203
|
-
}
|
|
204
|
-
if (action === 'publish' && request.method === 'POST') {
|
|
205
|
-
assertJsonContentType(request);
|
|
206
|
-
const body = await readJsonBody(request, config.maxRequestBytes);
|
|
207
|
-
assertOnlyKeys(body, ['paths']);
|
|
208
|
-
const paths = requiredStringArray(body.paths, 'paths');
|
|
209
|
-
if (paths.length > 32)
|
|
210
|
-
throw new RequestError(400, 'paths must contain at most 32 values');
|
|
211
|
-
const result = await publishWorkspaceFiles(workspaceFiles, artifactStore, root, paths);
|
|
212
|
-
writeJson(response, result.errors.length ? 207 : 201, result);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
throw new RequestError(405, 'Method not allowed');
|
|
216
|
-
}
|
|
217
175
|
const attachmentMatch = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/attachments$/);
|
|
218
176
|
if (attachmentMatch && request.method === 'POST') {
|
|
219
177
|
const sessionId = decodeURIComponent(attachmentMatch[1]);
|
|
@@ -226,6 +184,46 @@ async function handleRequest(context) {
|
|
|
226
184
|
writeJson(response, 201, sessions.addInputAttachment(sessionId, decodeHeader(request.headers['x-nexus-file-name']) || 'attachment', normalizedMediaType(request.headers['content-type']), bytes));
|
|
227
185
|
return;
|
|
228
186
|
}
|
|
187
|
+
const resolveMatch = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/requests\/([^/]+)\/resolve$/);
|
|
188
|
+
if (resolveMatch && request.method === 'POST') {
|
|
189
|
+
const sessionId = decodeURIComponent(resolveMatch[1]);
|
|
190
|
+
const requestId = decodeURIComponent(resolveMatch[2]);
|
|
191
|
+
const session = sessions.get(sessionId);
|
|
192
|
+
if (!sessions.owns(sessionId, principal.id)) {
|
|
193
|
+
throw new RequestError(404, 'Session not found');
|
|
194
|
+
}
|
|
195
|
+
assertAgentScope(principal, session.agentId);
|
|
196
|
+
assertJsonContentType(request);
|
|
197
|
+
const body = await readJsonBody(request, config.maxRequestBytes);
|
|
198
|
+
assertOnlyKeys(body, ['message', 'optionId', 'action', 'attachments']);
|
|
199
|
+
const resolution = {
|
|
200
|
+
requestId,
|
|
201
|
+
message: optionalString(body.message),
|
|
202
|
+
optionId: optionalString(body.optionId),
|
|
203
|
+
action: optionalPendingAction(body.action)
|
|
204
|
+
};
|
|
205
|
+
if (!resolution.message &&
|
|
206
|
+
!resolution.optionId &&
|
|
207
|
+
!resolution.action) {
|
|
208
|
+
throw new RequestError(400, 'A pending response is required');
|
|
209
|
+
}
|
|
210
|
+
writeJson(response, 202, await sessions.resolvePending(sessionId, resolution, optionalAttachmentIds(body.attachments)));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const publishMatch = url.pathname.match(/^\/v1\/sessions\/([^/]+)\/artifacts\/publish$/);
|
|
214
|
+
if (publishMatch && request.method === 'POST') {
|
|
215
|
+
const sessionId = decodeURIComponent(publishMatch[1]);
|
|
216
|
+
const session = sessions.get(sessionId);
|
|
217
|
+
if (!sessions.owns(sessionId, principal.id)) {
|
|
218
|
+
throw new RequestError(404, 'Session not found');
|
|
219
|
+
}
|
|
220
|
+
assertAgentScope(principal, session.agentId);
|
|
221
|
+
assertJsonContentType(request);
|
|
222
|
+
const body = await readJsonBody(request, config.maxRequestBytes);
|
|
223
|
+
assertOnlyKeys(body, ['path']);
|
|
224
|
+
writeJson(response, 201, await sessions.publishFile(sessionId, requiredString(body.path, 'path')));
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
229
227
|
const match = url.pathname.match(/^\/v1\/sessions\/([^/]+)(?:\/(message|cancel|events))?$/);
|
|
230
228
|
if (!match)
|
|
231
229
|
throw new RequestError(404, 'Route not found');
|
|
@@ -237,19 +235,23 @@ async function handleRequest(context) {
|
|
|
237
235
|
}
|
|
238
236
|
assertAgentScope(principal, session.agentId);
|
|
239
237
|
if (!action && request.method === 'GET') {
|
|
240
|
-
writeJson(response, 200,
|
|
238
|
+
writeJson(response, 200, session);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (!action && request.method === 'DELETE') {
|
|
242
|
+
writeJson(response, 200, await sessions.close(sessionId));
|
|
241
243
|
return;
|
|
242
244
|
}
|
|
243
245
|
if (action === 'message' && request.method === 'POST') {
|
|
244
246
|
assertJsonContentType(request);
|
|
245
247
|
const body = await readJsonBody(request, config.maxRequestBytes);
|
|
246
248
|
assertOnlyKeys(body, ['message', 'attachments']);
|
|
247
|
-
writeJson(response, 202, await
|
|
249
|
+
writeJson(response, 202, await sessions.message(sessionId, requiredMessage(body.message), optionalAttachmentIds(body.attachments)));
|
|
248
250
|
return;
|
|
249
251
|
}
|
|
250
252
|
if (action === 'cancel' && request.method === 'POST') {
|
|
251
253
|
await assertEmptyJsonBody(request, config.maxRequestBytes);
|
|
252
|
-
writeJson(response, 200, await
|
|
254
|
+
writeJson(response, 200, await sessions.cancel(sessionId));
|
|
253
255
|
return;
|
|
254
256
|
}
|
|
255
257
|
if (action === 'events' && request.method === 'GET') {
|
|
@@ -262,66 +264,9 @@ async function handleRequest(context) {
|
|
|
262
264
|
throw new RequestError(405, 'Method not allowed');
|
|
263
265
|
}
|
|
264
266
|
async function handleAdminRoute(context, url) {
|
|
265
|
-
const { config, sessions, controlPlane, adminSessions,
|
|
267
|
+
const { config, sessions, controlPlane, adminSessions, request, response } = context;
|
|
266
268
|
if (!controlPlane)
|
|
267
269
|
throw new RequestError(404, 'Control plane is unavailable');
|
|
268
|
-
if (url.pathname === '/v1/admin/files/roots' && request.method === 'GET') {
|
|
269
|
-
writeJson(response, 200, { roots: await workspaceFiles.roots() });
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
if (url.pathname === '/v1/admin/files' && request.method === 'GET') {
|
|
273
|
-
const root = await workspaceFiles.adminRoot(requiredQuery(url, 'root'));
|
|
274
|
-
writeJson(response, 200, await workspaceFiles.list(root, url.searchParams.get('path') || ''));
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (url.pathname === '/v1/admin/files/content') {
|
|
278
|
-
const root = await workspaceFiles.adminRoot(requiredQuery(url, 'root'));
|
|
279
|
-
const filePath = requiredQuery(url, 'path');
|
|
280
|
-
if (request.method === 'GET') {
|
|
281
|
-
const file = await workspaceFiles.open(root, filePath);
|
|
282
|
-
await streamDownload(response, file.handle, file.name, file.size, mediaTypeForName(file.name));
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
if (request.method === 'PUT') {
|
|
286
|
-
const declared = optionalContentLength(request);
|
|
287
|
-
writeJson(response, 201, await workspaceFiles.write(root, filePath, request, declared));
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
if (request.method === 'DELETE') {
|
|
291
|
-
await assertEmptyJsonBody(request, config.maxRequestBytes);
|
|
292
|
-
writeJson(response, 200, await workspaceFiles.remove(root, filePath));
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
throw new RequestError(405, 'Method not allowed');
|
|
296
|
-
}
|
|
297
|
-
if (url.pathname === '/v1/admin/files/directory' && request.method === 'POST') {
|
|
298
|
-
assertJsonContentType(request);
|
|
299
|
-
const body = await readJsonBody(request, config.maxRequestBytes);
|
|
300
|
-
assertOnlyKeys(body, ['root', 'path']);
|
|
301
|
-
const root = await workspaceFiles.adminRoot(requiredString(body.root, 'root'));
|
|
302
|
-
writeJson(response, 201, await workspaceFiles.createDirectory(root, requiredString(body.path, 'path')));
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
if (url.pathname === '/v1/admin/files/move' && request.method === 'POST') {
|
|
306
|
-
assertJsonContentType(request);
|
|
307
|
-
const body = await readJsonBody(request, config.maxRequestBytes);
|
|
308
|
-
assertOnlyKeys(body, ['root', 'path', 'destination']);
|
|
309
|
-
const root = await workspaceFiles.adminRoot(requiredString(body.root, 'root'));
|
|
310
|
-
writeJson(response, 200, await workspaceFiles.move(root, requiredString(body.path, 'path'), requiredString(body.destination, 'destination')));
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
if (url.pathname === '/v1/admin/files/publish' && request.method === 'POST') {
|
|
314
|
-
assertJsonContentType(request);
|
|
315
|
-
const body = await readJsonBody(request, config.maxRequestBytes);
|
|
316
|
-
assertOnlyKeys(body, ['root', 'paths']);
|
|
317
|
-
const root = await workspaceFiles.adminRoot(requiredString(body.root, 'root'));
|
|
318
|
-
const paths = requiredStringArray(body.paths, 'paths');
|
|
319
|
-
if (paths.length > 32)
|
|
320
|
-
throw new RequestError(400, 'paths must contain at most 32 values');
|
|
321
|
-
const result = await publishWorkspaceFiles(workspaceFiles, artifactStore, root, paths);
|
|
322
|
-
writeJson(response, result.errors.length ? 207 : 201, result);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
270
|
if (url.pathname === '/v1/admin/overview' && request.method === 'GET') {
|
|
326
271
|
const agents = await sessions.listAgents(undefined, url.searchParams.get('refresh') === '1');
|
|
327
272
|
writeJson(response, 200, { agents, sessions: sessions.count() });
|
|
@@ -441,7 +386,6 @@ function readAgentUpdate(body) {
|
|
|
441
386
|
'driver',
|
|
442
387
|
'name',
|
|
443
388
|
'description',
|
|
444
|
-
'instructions',
|
|
445
389
|
'enabled',
|
|
446
390
|
'workspace',
|
|
447
391
|
'permissionPolicy',
|
|
@@ -459,7 +403,6 @@ function readAgentUpdate(body) {
|
|
|
459
403
|
driver: optionalString(body.driver),
|
|
460
404
|
name: optionalString(body.name),
|
|
461
405
|
description: optionalString(body.description),
|
|
462
|
-
instructions: optionalString(body.instructions),
|
|
463
406
|
enabled: optionalBoolean(body.enabled, 'enabled'),
|
|
464
407
|
workspace: optionalString(body.workspace),
|
|
465
408
|
permissionPolicy: optionalString(body.permissionPolicy),
|
|
@@ -759,6 +702,14 @@ function optionalString(value) {
|
|
|
759
702
|
throw new RequestError(400, 'Expected a string');
|
|
760
703
|
return value.trim() || undefined;
|
|
761
704
|
}
|
|
705
|
+
function optionalPendingAction(value) {
|
|
706
|
+
if (value === undefined || value === null || value === '')
|
|
707
|
+
return undefined;
|
|
708
|
+
if (value === 'accept' || value === 'decline' || value === 'cancel') {
|
|
709
|
+
return value;
|
|
710
|
+
}
|
|
711
|
+
throw new RequestError(400, 'action must be accept, decline, or cancel');
|
|
712
|
+
}
|
|
762
713
|
function optionalBoolean(value, name) {
|
|
763
714
|
if (value === undefined)
|
|
764
715
|
return undefined;
|
|
@@ -788,22 +739,6 @@ function requiredStringArray(value, name, allowEmpty = false) {
|
|
|
788
739
|
}
|
|
789
740
|
return result;
|
|
790
741
|
}
|
|
791
|
-
function requiredQuery(url, name) {
|
|
792
|
-
const value = url.searchParams.get(name)?.trim();
|
|
793
|
-
if (!value)
|
|
794
|
-
throw new RequestError(400, `${name} is required`);
|
|
795
|
-
return value;
|
|
796
|
-
}
|
|
797
|
-
function optionalContentLength(request) {
|
|
798
|
-
const raw = stringHeader(request.headers['content-length']);
|
|
799
|
-
if (!raw)
|
|
800
|
-
return undefined;
|
|
801
|
-
const value = Number(raw);
|
|
802
|
-
if (!Number.isSafeInteger(value) || value < 0) {
|
|
803
|
-
throw new RequestError(400, 'Content-Length is invalid');
|
|
804
|
-
}
|
|
805
|
-
return value;
|
|
806
|
-
}
|
|
807
742
|
function cookieValue(request, name) {
|
|
808
743
|
const cookie = stringHeader(request.headers.cookie);
|
|
809
744
|
for (const part of cookie.split(';')) {
|
|
@@ -856,122 +791,6 @@ function safePath(value) {
|
|
|
856
791
|
return '/invalid-url';
|
|
857
792
|
}
|
|
858
793
|
}
|
|
859
|
-
function decodePathSegment(value) {
|
|
860
|
-
try {
|
|
861
|
-
return decodeURIComponent(value);
|
|
862
|
-
}
|
|
863
|
-
catch {
|
|
864
|
-
throw new RequestError(400, 'URL path is invalid');
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
function ownedSession(sessions, sessionId, principal) {
|
|
868
|
-
const session = sessions.get(sessionId);
|
|
869
|
-
if (!sessions.owns(sessionId, principal.id)) {
|
|
870
|
-
throw new RequestError(404, 'Session not found');
|
|
871
|
-
}
|
|
872
|
-
assertAgentScope(principal, session.agentId);
|
|
873
|
-
return session;
|
|
874
|
-
}
|
|
875
|
-
async function streamDownload(response, handle, name, size, mediaType) {
|
|
876
|
-
try {
|
|
877
|
-
response.writeHead(200, {
|
|
878
|
-
'Content-Type': mediaType,
|
|
879
|
-
'Content-Length': size,
|
|
880
|
-
'Content-Disposition': contentDisposition(name),
|
|
881
|
-
'Cache-Control': 'no-store',
|
|
882
|
-
'X-Content-Type-Options': 'nosniff'
|
|
883
|
-
});
|
|
884
|
-
await pipeline(handle.createReadStream({ autoClose: false }), response);
|
|
885
|
-
}
|
|
886
|
-
finally {
|
|
887
|
-
await handle.close().catch(() => undefined);
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
async function publishWorkspaceFiles(workspaceFiles, artifactStore, root, paths) {
|
|
891
|
-
const files = [];
|
|
892
|
-
const errors = [];
|
|
893
|
-
let firstFailure;
|
|
894
|
-
for (const value of paths) {
|
|
895
|
-
let file;
|
|
896
|
-
try {
|
|
897
|
-
file = await workspaceFiles.publishable(root, value);
|
|
898
|
-
files.push(await artifactStore.publishHandle(file.handle, file.name));
|
|
899
|
-
}
|
|
900
|
-
catch (error) {
|
|
901
|
-
if (!(error instanceof WorkspaceFileError) && !(error instanceof ArtifactStoreError)) {
|
|
902
|
-
throw error;
|
|
903
|
-
}
|
|
904
|
-
firstFailure ||= { status: error.status, message: error.message };
|
|
905
|
-
errors.push({ path: value, error: error.message });
|
|
906
|
-
}
|
|
907
|
-
finally {
|
|
908
|
-
await file?.handle.close().catch(() => undefined);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
if (!files.length && firstFailure) {
|
|
912
|
-
throw new RequestError(firstFailure.status, firstFailure.message);
|
|
913
|
-
}
|
|
914
|
-
return { files, errors };
|
|
915
|
-
}
|
|
916
|
-
async function materializeSessionArtifacts(session, store) {
|
|
917
|
-
const artifacts = await Promise.all(session.artifacts.map(async (artifact, index) => {
|
|
918
|
-
if (!artifact.bytesBase64)
|
|
919
|
-
return artifact;
|
|
920
|
-
let bytes;
|
|
921
|
-
try {
|
|
922
|
-
bytes = decodeArtifactBase64(artifact.bytesBase64);
|
|
923
|
-
}
|
|
924
|
-
catch (error) {
|
|
925
|
-
return omittedArtifact(artifact, errorMessage(error));
|
|
926
|
-
}
|
|
927
|
-
try {
|
|
928
|
-
const published = await store.publishBytes(bytes, artifact.filename || artifact.name || `artifact-${artifact.id || randomUUID()}`, artifact.mediaType, `${session.id}:${artifact.id || index}`);
|
|
929
|
-
return {
|
|
930
|
-
...artifact,
|
|
931
|
-
name: artifact.name || published.name,
|
|
932
|
-
filename: artifact.filename || published.name,
|
|
933
|
-
url: published.url,
|
|
934
|
-
bytesBase64: undefined,
|
|
935
|
-
metadata: {
|
|
936
|
-
...artifact.metadata,
|
|
937
|
-
publishedArtifactId: published.id,
|
|
938
|
-
size: published.size,
|
|
939
|
-
sha256: published.sha256,
|
|
940
|
-
expiresAt: published.expiresAt
|
|
941
|
-
}
|
|
942
|
-
};
|
|
943
|
-
}
|
|
944
|
-
catch (error) {
|
|
945
|
-
if (error instanceof ArtifactStoreError && error.status === 413) {
|
|
946
|
-
return omittedArtifact(artifact, error.message);
|
|
947
|
-
}
|
|
948
|
-
throw error;
|
|
949
|
-
}
|
|
950
|
-
}));
|
|
951
|
-
return { ...session, artifacts };
|
|
952
|
-
}
|
|
953
|
-
function decodeArtifactBase64(value) {
|
|
954
|
-
const normalized = value.replace(/\s/g, '');
|
|
955
|
-
if (!normalized ||
|
|
956
|
-
normalized.length % 4 === 1 ||
|
|
957
|
-
!/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
|
|
958
|
-
throw new Error('Artifact contains invalid Base64 data');
|
|
959
|
-
}
|
|
960
|
-
const bytes = Buffer.from(normalized, 'base64');
|
|
961
|
-
if (bytes.toString('base64').replace(/=+$/, '') !== normalized.replace(/=+$/, '')) {
|
|
962
|
-
throw new Error('Artifact contains invalid Base64 data');
|
|
963
|
-
}
|
|
964
|
-
return bytes;
|
|
965
|
-
}
|
|
966
|
-
function omittedArtifact(artifact, reason) {
|
|
967
|
-
return {
|
|
968
|
-
...artifact,
|
|
969
|
-
bytesBase64: undefined,
|
|
970
|
-
description: [artifact.description, `[binary artifact omitted: ${reason}]`]
|
|
971
|
-
.filter(Boolean)
|
|
972
|
-
.join('\n')
|
|
973
|
-
};
|
|
974
|
-
}
|
|
975
794
|
function errorStatus(error) {
|
|
976
795
|
if (error instanceof RunNotFoundError)
|
|
977
796
|
return 404;
|
|
@@ -981,10 +800,6 @@ function errorStatus(error) {
|
|
|
981
800
|
return error.status;
|
|
982
801
|
if (error instanceof ControlPlaneError)
|
|
983
802
|
return error.status;
|
|
984
|
-
if (error instanceof ArtifactStoreError)
|
|
985
|
-
return error.status;
|
|
986
|
-
if (error instanceof WorkspaceFileError)
|
|
987
|
-
return error.status;
|
|
988
803
|
if (error instanceof RequestError)
|
|
989
804
|
return error.status;
|
|
990
805
|
return 500;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import type { AgentdArtifact, AgentdEventType, AgentdInputAttachment, AgentdPendingRequest, AgentdSessionState } from './types.js';
|
|
1
|
+
import type { AgentdArtifact, AgentdEventType, AgentdInputAttachment, AgentdPendingRequest, AgentdPendingResponse, AgentdSessionState, AgentdTurnCompletionProof } from './types.js';
|
|
2
2
|
export interface AgentSessionSink {
|
|
3
3
|
readonly id: string;
|
|
4
4
|
readonly state: AgentdSessionState;
|
|
5
5
|
readonly protocolSessionId?: string;
|
|
6
6
|
setProtocolSessionId(id: string): void;
|
|
7
7
|
setState(state: AgentdSessionState, error?: string): void;
|
|
8
|
+
completeTurn(proof: AgentdTurnCompletionProof): boolean;
|
|
8
9
|
appendOutput(text: string): void;
|
|
9
10
|
addArtifact(artifact: AgentdArtifact): void;
|
|
10
11
|
setPending(request: AgentdPendingRequest): void;
|
|
@@ -18,7 +19,7 @@ export interface AcpSessionSink extends AgentSessionSink {
|
|
|
18
19
|
export interface AgentSessionRuntime {
|
|
19
20
|
start(workspace?: string): Promise<void>;
|
|
20
21
|
prompt(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
|
|
21
|
-
respondPending(
|
|
22
|
+
respondPending(response: AgentdPendingResponse | string, attachments?: AgentdInputAttachment[]): Promise<void>;
|
|
22
23
|
cancel(): Promise<void>;
|
|
23
24
|
dispose(): Promise<void>;
|
|
24
25
|
}
|
package/dist/session.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AgentDriver } from './drivers/index.js';
|
|
|
2
2
|
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
|
-
import type { AgentdAgentView, AgentdArtifact, AgentdConfig, AgentdEvent, AgentdEventType, AgentdInputAttachmentView, AgentdPendingRequest, AgentdProtocol, AgentdSessionState, AgentdSessionView } from './types.js';
|
|
5
|
+
import type { AgentdAgentView, AgentdArtifact, AgentdConfig, AgentdEvent, AgentdEventType, AgentdInputAttachmentView, AgentdPendingRequest, AgentdPendingResponse, AgentdProtocol, AgentdSessionState, AgentdSessionView, AgentdTurnCompletionProof } from './types.js';
|
|
6
6
|
import type { WorkspacePolicy } from './workspace.js';
|
|
7
7
|
export declare class ManagedSession implements AgentSessionSink, AcpSessionSink {
|
|
8
8
|
readonly agentId: string;
|
|
@@ -12,6 +12,7 @@ export declare class ManagedSession implements AgentSessionSink, AcpSessionSink
|
|
|
12
12
|
private readonly maxOutputChars;
|
|
13
13
|
private readonly agentName;
|
|
14
14
|
private readonly runStore?;
|
|
15
|
+
readonly instanceId: string;
|
|
15
16
|
readonly id: `${string}-${string}-${string}-${string}-${string}`;
|
|
16
17
|
readonly createdAt: number;
|
|
17
18
|
readonly events: SessionEventLog;
|
|
@@ -22,21 +23,25 @@ export declare class ManagedSession implements AgentSessionSink, AcpSessionSink
|
|
|
22
23
|
output: string;
|
|
23
24
|
error?: string;
|
|
24
25
|
pendingRequest?: AgentdPendingRequest;
|
|
26
|
+
private completion?;
|
|
25
27
|
private artifacts;
|
|
26
28
|
private inputAttachments;
|
|
27
29
|
private runtime?;
|
|
28
|
-
constructor(agentId: string, protocol: AgentdProtocol, workspace: string | undefined, ownerKeyId: string, maxEvents: number, maxOutputChars: number, agentName: string, runStore?: RunStore | undefined);
|
|
30
|
+
constructor(agentId: string, protocol: AgentdProtocol, workspace: string | undefined, ownerKeyId: string, maxEvents: number, maxOutputChars: number, agentName: string, runStore?: RunStore | undefined, instanceId?: string);
|
|
29
31
|
attach(runtime: AgentSessionRuntime): void;
|
|
30
32
|
setProtocolSessionId(id: string): void;
|
|
31
33
|
setAcpSessionId(id: string): void;
|
|
32
34
|
setState(state: AgentdSessionState, error?: string): void;
|
|
35
|
+
completeTurn(proof: AgentdTurnCompletionProof): boolean;
|
|
33
36
|
appendOutput(text: string): void;
|
|
34
37
|
addArtifact(artifact: AgentdArtifact): void;
|
|
38
|
+
publishFile(filePath: string): Promise<AgentdArtifact>;
|
|
35
39
|
setPending(request: AgentdPendingRequest): void;
|
|
36
40
|
clearPending(): void;
|
|
37
41
|
emit(type: AgentdEventType, data?: unknown): void;
|
|
38
42
|
addInputAttachment(name: string, mediaType: string | undefined, bytes: Buffer): AgentdInputAttachmentView;
|
|
39
43
|
message(message: string, attachmentIds?: string[]): Promise<void>;
|
|
44
|
+
resolvePending(response: AgentdPendingResponse, attachmentIds?: string[]): Promise<void>;
|
|
40
45
|
cancel(): Promise<void>;
|
|
41
46
|
dispose(): Promise<void>;
|
|
42
47
|
snapshot(): AgentdSessionView;
|
|
@@ -50,6 +55,7 @@ export declare class SessionManager {
|
|
|
50
55
|
private workspacePolicy;
|
|
51
56
|
private drivers;
|
|
52
57
|
private readonly runStore?;
|
|
58
|
+
readonly instanceId: `${string}-${string}-${string}-${string}-${string}`;
|
|
53
59
|
private sessions;
|
|
54
60
|
private cleanupTimer?;
|
|
55
61
|
private readinessCache?;
|
|
@@ -67,8 +73,11 @@ export declare class SessionManager {
|
|
|
67
73
|
};
|
|
68
74
|
getRun(id: string): import("./types.js").AgentdRunDetail;
|
|
69
75
|
message(id: string, message: string, attachmentIds?: string[]): Promise<AgentdSessionView>;
|
|
76
|
+
resolvePending(id: string, response: AgentdPendingResponse, attachmentIds?: string[]): Promise<AgentdSessionView>;
|
|
70
77
|
addInputAttachment(id: string, name: string, mediaType: string | undefined, bytes: Buffer): AgentdInputAttachmentView;
|
|
78
|
+
publishFile(id: string, path: string): Promise<AgentdSessionView>;
|
|
71
79
|
cancel(id: string): Promise<AgentdSessionView>;
|
|
80
|
+
close(id: string): Promise<AgentdSessionView>;
|
|
72
81
|
eventsAfter(id: string, after?: string): AgentdEvent[];
|
|
73
82
|
subscribe(id: string, listener: (event: AgentdEvent) => void): () => boolean;
|
|
74
83
|
shutdown(): Promise<void>;
|