nexus-agentd 0.1.1 → 0.1.7
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 +376 -185
- package/dist/a2a/runtime.d.ts +32 -0
- package/dist/a2a/runtime.js +382 -0
- package/dist/acp/runtime.d.ts +14 -3
- package/dist/acp/runtime.js +256 -17
- package/dist/agent-instructions.d.ts +11 -0
- package/dist/agent-instructions.js +44 -0
- 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 +284 -53
- package/dist/control-plane.d.ts +92 -0
- package/dist/control-plane.js +646 -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 +913 -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 +204 -6
- package/dist/types.js +2 -1
- package/dist/webui/app.d.ts +1 -0
- package/dist/webui/app.js +1074 -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 +31 -27
- package/package.json +6 -6
package/dist/session.js
CHANGED
|
@@ -1,35 +1,61 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
-
import { SessionEventLog } from './events.js';
|
|
3
2
|
import { AcpProcessRuntime } from './acp/runtime.js';
|
|
4
|
-
|
|
3
|
+
import { A2AClientRuntime, probeA2AAgent } from './a2a/runtime.js';
|
|
4
|
+
import { SessionEventLog } from './events.js';
|
|
5
|
+
const MAX_SESSION_ARTIFACTS = 64;
|
|
6
|
+
const MAX_ARTIFACT_BASE64_CHARS = 16 * 1024 * 1024;
|
|
7
|
+
const MAX_SESSION_BASE64_CHARS = 32 * 1024 * 1024;
|
|
8
|
+
const READINESS_CACHE_MS = 20_000;
|
|
9
|
+
const MAX_INPUT_ATTACHMENTS = 16;
|
|
10
|
+
const MAX_INPUT_ATTACHMENT_BYTES = 16 * 1024 * 1024;
|
|
11
|
+
const MAX_SESSION_INPUT_BYTES = 32 * 1024 * 1024;
|
|
12
|
+
export class ManagedSession {
|
|
5
13
|
agentId;
|
|
14
|
+
protocol;
|
|
6
15
|
workspace;
|
|
16
|
+
ownerKeyId;
|
|
7
17
|
maxOutputChars;
|
|
18
|
+
agentName;
|
|
19
|
+
runStore;
|
|
8
20
|
id = randomUUID();
|
|
9
21
|
createdAt = Date.now();
|
|
10
22
|
events;
|
|
11
23
|
state = 'created';
|
|
12
24
|
updatedAt = this.createdAt;
|
|
25
|
+
protocolSessionId;
|
|
13
26
|
acpSessionId;
|
|
14
27
|
output = '';
|
|
15
28
|
error;
|
|
16
29
|
pendingRequest;
|
|
30
|
+
artifacts = [];
|
|
31
|
+
inputAttachments = new Map();
|
|
17
32
|
runtime;
|
|
18
|
-
constructor(agentId, workspace, maxEvents, maxOutputChars) {
|
|
33
|
+
constructor(agentId, protocol, workspace, ownerKeyId, maxEvents, maxOutputChars, agentName, runStore) {
|
|
19
34
|
this.agentId = agentId;
|
|
35
|
+
this.protocol = protocol;
|
|
20
36
|
this.workspace = workspace;
|
|
37
|
+
this.ownerKeyId = ownerKeyId;
|
|
21
38
|
this.maxOutputChars = maxOutputChars;
|
|
39
|
+
this.agentName = agentName;
|
|
40
|
+
this.runStore = runStore;
|
|
22
41
|
this.events = new SessionEventLog(this.id, maxEvents);
|
|
23
|
-
this.events.append('session_state', { state: this.state });
|
|
42
|
+
this.events.append('session_state', { state: this.state, protocol });
|
|
24
43
|
}
|
|
25
44
|
attach(runtime) {
|
|
26
45
|
this.runtime = runtime;
|
|
27
46
|
}
|
|
47
|
+
setProtocolSessionId(id) {
|
|
48
|
+
this.protocolSessionId = id;
|
|
49
|
+
this.updatedAt = Date.now();
|
|
50
|
+
this.syncRun({ protocolSessionId: id });
|
|
51
|
+
}
|
|
28
52
|
setAcpSessionId(id) {
|
|
29
53
|
this.acpSessionId = id;
|
|
30
|
-
this.
|
|
54
|
+
this.setProtocolSessionId(id);
|
|
31
55
|
}
|
|
32
56
|
setState(state, error) {
|
|
57
|
+
if (this.state === state && this.error === error)
|
|
58
|
+
return;
|
|
33
59
|
this.state = state;
|
|
34
60
|
this.error = error;
|
|
35
61
|
this.updatedAt = Date.now();
|
|
@@ -41,23 +67,67 @@ class ManagedSession {
|
|
|
41
67
|
? 'canceled'
|
|
42
68
|
: 'session_state';
|
|
43
69
|
this.events.append(type, { state, ...(error ? { error } : {}) });
|
|
70
|
+
const endedAt = isTerminal(state) ? this.updatedAt : undefined;
|
|
71
|
+
this.syncRun({
|
|
72
|
+
state,
|
|
73
|
+
error,
|
|
74
|
+
progress: progressForState(state, error),
|
|
75
|
+
...(endedAt ? { endedAt } : {})
|
|
76
|
+
});
|
|
44
77
|
}
|
|
45
78
|
appendOutput(text) {
|
|
46
79
|
if (!text)
|
|
47
80
|
return;
|
|
48
81
|
this.output = `${this.output}${text}`;
|
|
49
82
|
if (this.output.length > this.maxOutputChars) {
|
|
50
|
-
this.output = `…[truncated by
|
|
83
|
+
this.output = `…[truncated by Agent Nexus]\n${this.output.slice(-this.maxOutputChars)}`;
|
|
51
84
|
}
|
|
52
85
|
this.updatedAt = Date.now();
|
|
86
|
+
this.syncRun({ output: this.output });
|
|
87
|
+
}
|
|
88
|
+
addArtifact(artifact) {
|
|
89
|
+
const next = structuredClone(artifact);
|
|
90
|
+
const existingIndex = next.id
|
|
91
|
+
? this.artifacts.findIndex((item) => item.id === next.id)
|
|
92
|
+
: -1;
|
|
93
|
+
const retainedBase64Chars = this.artifacts.reduce((total, item, index) => index === existingIndex
|
|
94
|
+
? total
|
|
95
|
+
: total + (item.bytesBase64?.length || 0), 0);
|
|
96
|
+
const binaryChars = next.bytesBase64?.length || 0;
|
|
97
|
+
if (binaryChars > MAX_ARTIFACT_BASE64_CHARS ||
|
|
98
|
+
retainedBase64Chars + binaryChars > MAX_SESSION_BASE64_CHARS) {
|
|
99
|
+
next.bytesBase64 = undefined;
|
|
100
|
+
next.description = [
|
|
101
|
+
next.description,
|
|
102
|
+
`[binary payload omitted: ${binaryChars} base64 characters exceed the session limit]`
|
|
103
|
+
]
|
|
104
|
+
.filter(Boolean)
|
|
105
|
+
.join('\n');
|
|
106
|
+
}
|
|
107
|
+
if (existingIndex >= 0) {
|
|
108
|
+
this.artifacts[existingIndex] = next;
|
|
109
|
+
}
|
|
110
|
+
else if (this.artifacts.length < MAX_SESSION_ARTIFACTS) {
|
|
111
|
+
this.artifacts.push(next);
|
|
112
|
+
}
|
|
113
|
+
this.updatedAt = Date.now();
|
|
114
|
+
this.syncRun({
|
|
115
|
+
artifacts: this.artifacts.map(runArtifact),
|
|
116
|
+
artifactCount: this.artifacts.length
|
|
117
|
+
});
|
|
53
118
|
}
|
|
54
119
|
setPending(request) {
|
|
55
120
|
this.pendingRequest = structuredClone(request);
|
|
56
121
|
this.state = request.kind === 'permission' ? 'permission_required' : 'input_required';
|
|
57
122
|
this.updatedAt = Date.now();
|
|
58
|
-
this.events.append(request.kind === 'permission'
|
|
59
|
-
|
|
60
|
-
:
|
|
123
|
+
this.events.append(request.kind === 'permission' ? 'permission_required' : 'input_required', request);
|
|
124
|
+
this.syncRun({
|
|
125
|
+
state: this.state,
|
|
126
|
+
progress: {
|
|
127
|
+
phase: request.kind === 'permission' ? '等待授权' : '等待输入',
|
|
128
|
+
message: clipProgress(request.prompt)
|
|
129
|
+
}
|
|
130
|
+
});
|
|
61
131
|
}
|
|
62
132
|
clearPending() {
|
|
63
133
|
this.pendingRequest = undefined;
|
|
@@ -66,21 +136,62 @@ class ManagedSession {
|
|
|
66
136
|
emit(type, data) {
|
|
67
137
|
this.updatedAt = Date.now();
|
|
68
138
|
this.events.append(type, data);
|
|
139
|
+
const progress = progressForEvent(type, data);
|
|
140
|
+
if (progress)
|
|
141
|
+
this.syncRun({ progress });
|
|
142
|
+
}
|
|
143
|
+
addInputAttachment(name, mediaType, bytes) {
|
|
144
|
+
if (bytes.length > MAX_INPUT_ATTACHMENT_BYTES) {
|
|
145
|
+
throw new SessionRequestError(413, 'Input attachment is too large');
|
|
146
|
+
}
|
|
147
|
+
const retained = Array.from(this.inputAttachments.values()).reduce((total, attachment) => total + attachment.bytes.length, 0);
|
|
148
|
+
if (this.inputAttachments.size >= MAX_INPUT_ATTACHMENTS) {
|
|
149
|
+
throw new SessionRequestError(413, 'Too many input attachments');
|
|
150
|
+
}
|
|
151
|
+
if (retained + bytes.length > MAX_SESSION_INPUT_BYTES) {
|
|
152
|
+
throw new SessionRequestError(413, 'Session input attachments are too large');
|
|
153
|
+
}
|
|
154
|
+
const id = randomUUID();
|
|
155
|
+
const attachment = {
|
|
156
|
+
id,
|
|
157
|
+
name: safeAttachmentName(name) || `attachment-${id.slice(0, 8)}`,
|
|
158
|
+
mediaType: mediaType || undefined,
|
|
159
|
+
bytes: Buffer.from(bytes)
|
|
160
|
+
};
|
|
161
|
+
this.inputAttachments.set(id, attachment);
|
|
162
|
+
this.updatedAt = Date.now();
|
|
163
|
+
return attachmentView(attachment);
|
|
69
164
|
}
|
|
70
|
-
async message(message) {
|
|
165
|
+
async message(message, attachmentIds = []) {
|
|
71
166
|
if (!this.runtime)
|
|
72
|
-
throw new
|
|
167
|
+
throw new SessionRequestError(409, 'Agent runtime is unavailable');
|
|
168
|
+
const attachments = this.inputAttachmentsFor(attachmentIds);
|
|
73
169
|
if (this.pendingRequest) {
|
|
74
|
-
|
|
170
|
+
this.syncRun({
|
|
171
|
+
progress: { phase: '继续执行', message: '已提交补充信息' }
|
|
172
|
+
});
|
|
173
|
+
await this.runtime.respondPending(message, attachments);
|
|
75
174
|
return;
|
|
76
175
|
}
|
|
77
176
|
if (this.state === 'running') {
|
|
78
|
-
throw new
|
|
177
|
+
throw new SessionRequestError(409, 'Session is already processing a message');
|
|
79
178
|
}
|
|
80
179
|
this.output = '';
|
|
180
|
+
this.artifacts = [];
|
|
81
181
|
this.error = undefined;
|
|
82
182
|
this.updatedAt = Date.now();
|
|
83
|
-
|
|
183
|
+
this.currentRunId = this.runStore?.create({
|
|
184
|
+
sessionId: this.id,
|
|
185
|
+
agentId: this.agentId,
|
|
186
|
+
agentName: this.agentName,
|
|
187
|
+
protocol: this.protocol,
|
|
188
|
+
workspace: this.workspace,
|
|
189
|
+
protocolSessionId: this.protocolSessionId,
|
|
190
|
+
ownerKeyId: this.ownerKeyId,
|
|
191
|
+
task: message,
|
|
192
|
+
inputAttachmentCount: attachments.length
|
|
193
|
+
}).id;
|
|
194
|
+
void this.runtime.prompt(message, attachments).catch((error) => {
|
|
84
195
|
if (this.state !== 'canceled')
|
|
85
196
|
this.setState('failed', errorMessage(error));
|
|
86
197
|
});
|
|
@@ -90,17 +201,22 @@ class ManagedSession {
|
|
|
90
201
|
}
|
|
91
202
|
async dispose() {
|
|
92
203
|
await this.runtime?.dispose();
|
|
204
|
+
this.inputAttachments.clear();
|
|
93
205
|
}
|
|
94
206
|
snapshot() {
|
|
95
207
|
return {
|
|
96
208
|
id: this.id,
|
|
97
|
-
|
|
209
|
+
runId: this.currentRunId,
|
|
210
|
+
protocol: this.protocol,
|
|
211
|
+
protocolSessionId: this.protocolSessionId,
|
|
212
|
+
acpSessionId: this.protocol === 'acp' ? this.acpSessionId : undefined,
|
|
98
213
|
agentId: this.agentId,
|
|
99
214
|
workspace: this.workspace,
|
|
100
215
|
state: this.state,
|
|
101
216
|
output: this.output || undefined,
|
|
102
217
|
error: this.error,
|
|
103
|
-
artifacts:
|
|
218
|
+
artifacts: structuredClone(this.artifacts),
|
|
219
|
+
inputAttachments: Array.from(this.inputAttachments.values()).map(attachmentView),
|
|
104
220
|
pendingRequest: this.pendingRequest
|
|
105
221
|
? structuredClone(this.pendingRequest)
|
|
106
222
|
: undefined,
|
|
@@ -109,35 +225,157 @@ class ManagedSession {
|
|
|
109
225
|
updatedAt: this.updatedAt
|
|
110
226
|
};
|
|
111
227
|
}
|
|
228
|
+
interruptForShutdown() {
|
|
229
|
+
if (isActive(this.state)) {
|
|
230
|
+
this.setState('failed', 'Gateway stopped before the task finished');
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
currentRunId;
|
|
234
|
+
syncRun(patch) {
|
|
235
|
+
if (!this.runStore || !this.currentRunId)
|
|
236
|
+
return;
|
|
237
|
+
this.runStore.update(this.currentRunId, {
|
|
238
|
+
protocolSessionId: this.protocolSessionId,
|
|
239
|
+
output: this.output || undefined,
|
|
240
|
+
artifacts: this.artifacts.map(runArtifact),
|
|
241
|
+
artifactCount: this.artifacts.length,
|
|
242
|
+
updatedAt: this.updatedAt,
|
|
243
|
+
...patch
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
inputAttachmentsFor(ids) {
|
|
247
|
+
const unique = Array.from(new Set(ids));
|
|
248
|
+
const attachments = unique.map((id) => this.inputAttachments.get(id));
|
|
249
|
+
if (attachments.some((attachment) => !attachment)) {
|
|
250
|
+
throw new SessionRequestError(400, 'Unknown input attachment');
|
|
251
|
+
}
|
|
252
|
+
return attachments;
|
|
253
|
+
}
|
|
112
254
|
}
|
|
113
255
|
export class SessionManager {
|
|
114
256
|
config;
|
|
115
257
|
workspacePolicy;
|
|
116
258
|
drivers;
|
|
259
|
+
runStore;
|
|
117
260
|
sessions = new Map();
|
|
118
261
|
cleanupTimer;
|
|
119
|
-
|
|
262
|
+
readinessCache;
|
|
263
|
+
constructor(config, workspacePolicy, drivers, runStore) {
|
|
120
264
|
this.config = config;
|
|
121
265
|
this.workspacePolicy = workspacePolicy;
|
|
122
266
|
this.drivers = drivers;
|
|
267
|
+
this.runStore = runStore;
|
|
268
|
+
}
|
|
269
|
+
reconfigure(config, workspacePolicy, drivers) {
|
|
270
|
+
this.config = config;
|
|
271
|
+
this.workspacePolicy = workspacePolicy;
|
|
272
|
+
this.drivers = drivers;
|
|
273
|
+
this.readinessCache = undefined;
|
|
274
|
+
if (this.cleanupTimer)
|
|
275
|
+
this.restartCleanup();
|
|
123
276
|
}
|
|
124
277
|
startCleanup() {
|
|
125
278
|
if (this.cleanupTimer)
|
|
126
279
|
return;
|
|
127
|
-
this.
|
|
128
|
-
this.cleanupTimer.unref?.();
|
|
280
|
+
this.restartCleanup();
|
|
129
281
|
}
|
|
130
|
-
async listAgents() {
|
|
131
|
-
|
|
282
|
+
async listAgents(agentIds, force = false) {
|
|
283
|
+
const now = Date.now();
|
|
284
|
+
if (!force && this.readinessCache && this.readinessCache.expiresAt > now) {
|
|
285
|
+
return filterAgents(this.readinessCache.agents, agentIds);
|
|
286
|
+
}
|
|
287
|
+
const agents = await Promise.all(Object.entries(this.config.agents).map(async ([id, config]) => {
|
|
288
|
+
if (config.protocol === 'a2a')
|
|
289
|
+
return probeA2AAgent(id, config);
|
|
290
|
+
if (config.enabled === false) {
|
|
291
|
+
return {
|
|
292
|
+
id,
|
|
293
|
+
name: config.name || id,
|
|
294
|
+
description: config.description,
|
|
295
|
+
protocol: 'acp',
|
|
296
|
+
driver: config.driver,
|
|
297
|
+
ready: false,
|
|
298
|
+
enabled: false,
|
|
299
|
+
workspace: this.defaultWorkspace(id),
|
|
300
|
+
error: 'Agent is disabled',
|
|
301
|
+
checkedAt: Date.now()
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const driver = this.drivers.get(id);
|
|
305
|
+
if (!driver) {
|
|
306
|
+
return {
|
|
307
|
+
id,
|
|
308
|
+
name: config.name || id,
|
|
309
|
+
description: config.description,
|
|
310
|
+
protocol: 'acp',
|
|
311
|
+
driver: config.driver,
|
|
312
|
+
ready: false,
|
|
313
|
+
enabled: true,
|
|
314
|
+
workspace: this.defaultWorkspace(id),
|
|
315
|
+
error: 'ACP driver is unavailable',
|
|
316
|
+
checkedAt: Date.now()
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const startedAt = Date.now();
|
|
320
|
+
try {
|
|
321
|
+
const probe = await driver.probe();
|
|
322
|
+
return {
|
|
323
|
+
...probe,
|
|
324
|
+
protocol: 'acp',
|
|
325
|
+
driver: config.driver,
|
|
326
|
+
enabled: true,
|
|
327
|
+
workspace: this.defaultWorkspace(id),
|
|
328
|
+
checkedAt: Date.now(),
|
|
329
|
+
responseMs: Date.now() - startedAt
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
catch (error) {
|
|
333
|
+
return {
|
|
334
|
+
id,
|
|
335
|
+
name: config.name || id,
|
|
336
|
+
description: config.description,
|
|
337
|
+
protocol: 'acp',
|
|
338
|
+
driver: config.driver,
|
|
339
|
+
ready: false,
|
|
340
|
+
enabled: true,
|
|
341
|
+
workspace: this.defaultWorkspace(id),
|
|
342
|
+
error: errorMessage(error),
|
|
343
|
+
checkedAt: Date.now(),
|
|
344
|
+
responseMs: Date.now() - startedAt
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}));
|
|
348
|
+
agents.sort((left, right) => left.name.localeCompare(right.name));
|
|
349
|
+
this.readinessCache = {
|
|
350
|
+
expiresAt: Date.now() + READINESS_CACHE_MS,
|
|
351
|
+
agents: structuredClone(agents)
|
|
352
|
+
};
|
|
353
|
+
return filterAgents(agents, agentIds);
|
|
132
354
|
}
|
|
133
|
-
async create(agentId, workspaceInput) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
355
|
+
async create(agentId, workspaceInput, ownerKeyId) {
|
|
356
|
+
await this.ensureCapacity();
|
|
357
|
+
const config = this.config.agents[agentId];
|
|
358
|
+
if (!config || config.enabled === false) {
|
|
359
|
+
throw new SessionRequestError(404, `Configured agent not found: ${agentId}`);
|
|
360
|
+
}
|
|
361
|
+
let workspace;
|
|
362
|
+
let driver;
|
|
363
|
+
if (config.protocol !== 'a2a') {
|
|
364
|
+
driver = this.drivers.get(agentId);
|
|
365
|
+
if (!driver) {
|
|
366
|
+
throw new SessionRequestError(404, `Configured ACP agent not found: ${agentId}`);
|
|
367
|
+
}
|
|
368
|
+
const requestedWorkspace = String(workspaceInput || '').trim() || this.defaultWorkspace(agentId);
|
|
369
|
+
if (!requestedWorkspace) {
|
|
370
|
+
throw new SessionRequestError(400, `Configured ACP agent has no workspace: ${agentId}`);
|
|
371
|
+
}
|
|
372
|
+
workspace = await this.workspacePolicy.resolve(requestedWorkspace);
|
|
373
|
+
}
|
|
374
|
+
const session = new ManagedSession(agentId, config.protocol === 'a2a' ? 'a2a' : 'acp', workspace, ownerKeyId, this.config.maxEventsPerSession, this.config.maxOutputChars, config.name || agentId, this.runStore);
|
|
375
|
+
const runtime = config.protocol === 'a2a'
|
|
376
|
+
? new A2AClientRuntime(config, session, this.config.promptTimeoutMs || 30 * 60_000)
|
|
377
|
+
: new AcpProcessRuntime(driver, session, this.config.maxOutputChars, this.config.promptTimeoutMs || 30 * 60_000, config.instructions);
|
|
139
378
|
this.sessions.set(session.id, session);
|
|
140
|
-
const runtime = new AcpProcessRuntime(driver, session);
|
|
141
379
|
session.attach(runtime);
|
|
142
380
|
try {
|
|
143
381
|
await runtime.start(workspace);
|
|
@@ -146,20 +384,40 @@ export class SessionManager {
|
|
|
146
384
|
catch (error) {
|
|
147
385
|
session.setState('failed', errorMessage(error));
|
|
148
386
|
await session.dispose();
|
|
149
|
-
|
|
387
|
+
this.sessions.delete(session.id);
|
|
388
|
+
throw new SessionRequestError(502, `Agent failed to start: ${errorMessage(error)}`);
|
|
150
389
|
}
|
|
151
390
|
}
|
|
152
391
|
get(id) {
|
|
153
392
|
return this.require(id).snapshot();
|
|
154
393
|
}
|
|
155
|
-
|
|
394
|
+
owns(id, keyId) {
|
|
395
|
+
return this.require(id).ownerKeyId === keyId;
|
|
396
|
+
}
|
|
397
|
+
count() {
|
|
398
|
+
return this.sessions.size;
|
|
399
|
+
}
|
|
400
|
+
listRuns(query = {}) {
|
|
401
|
+
return this.runStore?.list(query) || { runs: [], total: 0 };
|
|
402
|
+
}
|
|
403
|
+
getRun(id) {
|
|
404
|
+
const run = this.runStore?.get(id);
|
|
405
|
+
if (!run)
|
|
406
|
+
throw new RunNotFoundError(id);
|
|
407
|
+
return run;
|
|
408
|
+
}
|
|
409
|
+
async message(id, message, attachmentIds = []) {
|
|
156
410
|
const text = String(message || '');
|
|
157
411
|
if (!text.trim())
|
|
158
|
-
throw new
|
|
412
|
+
throw new SessionRequestError(400, 'message is required');
|
|
159
413
|
const session = this.require(id);
|
|
160
|
-
await session.message(text);
|
|
414
|
+
await session.message(text, attachmentIds);
|
|
161
415
|
return session.snapshot();
|
|
162
416
|
}
|
|
417
|
+
addInputAttachment(id, name, mediaType, bytes) {
|
|
418
|
+
const session = this.require(id);
|
|
419
|
+
return session.addInputAttachment(name, mediaType, bytes);
|
|
420
|
+
}
|
|
163
421
|
async cancel(id) {
|
|
164
422
|
const session = this.require(id);
|
|
165
423
|
await session.cancel();
|
|
@@ -175,7 +433,10 @@ export class SessionManager {
|
|
|
175
433
|
if (this.cleanupTimer)
|
|
176
434
|
clearInterval(this.cleanupTimer);
|
|
177
435
|
this.cleanupTimer = undefined;
|
|
178
|
-
await Promise.allSettled(Array.from(this.sessions.values()).map((session) =>
|
|
436
|
+
await Promise.allSettled(Array.from(this.sessions.values()).map(async (session) => {
|
|
437
|
+
session.interruptForShutdown();
|
|
438
|
+
await session.dispose();
|
|
439
|
+
}));
|
|
179
440
|
this.sessions.clear();
|
|
180
441
|
}
|
|
181
442
|
require(id) {
|
|
@@ -184,27 +445,161 @@ export class SessionManager {
|
|
|
184
445
|
throw new SessionNotFoundError(id);
|
|
185
446
|
return session;
|
|
186
447
|
}
|
|
448
|
+
async ensureCapacity() {
|
|
449
|
+
const limit = this.config.maxSessions || 64;
|
|
450
|
+
if (this.sessions.size < limit)
|
|
451
|
+
return;
|
|
452
|
+
const terminal = Array.from(this.sessions.values())
|
|
453
|
+
.filter((session) => isTerminal(session.state))
|
|
454
|
+
.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
455
|
+
while (this.sessions.size >= limit && terminal.length) {
|
|
456
|
+
const session = terminal.shift();
|
|
457
|
+
await session.dispose();
|
|
458
|
+
this.sessions.delete(session.id);
|
|
459
|
+
}
|
|
460
|
+
if (this.sessions.size >= limit) {
|
|
461
|
+
throw new SessionRequestError(429, 'Session capacity has been reached');
|
|
462
|
+
}
|
|
463
|
+
}
|
|
187
464
|
async cleanup() {
|
|
188
465
|
const cutoff = Date.now() - this.config.sessionTtlMs;
|
|
189
466
|
for (const [id, session] of this.sessions) {
|
|
190
|
-
if (session.updatedAt > cutoff
|
|
191
|
-
session.state === 'running' ||
|
|
192
|
-
session.state === 'permission_required' ||
|
|
193
|
-
session.state === 'input_required') {
|
|
467
|
+
if (session.updatedAt > cutoff)
|
|
194
468
|
continue;
|
|
469
|
+
if (!isTerminal(session.state)) {
|
|
470
|
+
session.setState('failed', 'Session expired after exceeding its lifetime');
|
|
195
471
|
}
|
|
196
472
|
await session.dispose();
|
|
197
473
|
this.sessions.delete(id);
|
|
198
474
|
}
|
|
199
475
|
}
|
|
476
|
+
restartCleanup() {
|
|
477
|
+
if (this.cleanupTimer)
|
|
478
|
+
clearInterval(this.cleanupTimer);
|
|
479
|
+
this.cleanupTimer = setInterval(() => void this.cleanup(), this.config.cleanupIntervalMs || 60_000);
|
|
480
|
+
this.cleanupTimer.unref?.();
|
|
481
|
+
}
|
|
482
|
+
defaultWorkspace(agentId) {
|
|
483
|
+
const config = this.config.agents[agentId];
|
|
484
|
+
if (config?.protocol === 'a2a')
|
|
485
|
+
return '';
|
|
486
|
+
return config?.workspace || this.config.workspaceRoots[0] || '';
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function progressForState(state, error) {
|
|
490
|
+
const phases = {
|
|
491
|
+
created: '已创建',
|
|
492
|
+
running: '运行中',
|
|
493
|
+
input_required: '等待输入',
|
|
494
|
+
permission_required: '等待授权',
|
|
495
|
+
completed: '已完成',
|
|
496
|
+
failed: '失败',
|
|
497
|
+
canceled: '已取消'
|
|
498
|
+
};
|
|
499
|
+
return { phase: phases[state], message: error ? clipProgress(error) : undefined };
|
|
500
|
+
}
|
|
501
|
+
function progressForEvent(type, data) {
|
|
502
|
+
const record = isRecord(data) ? data : undefined;
|
|
503
|
+
const message = clipProgress(firstString(record?.title, record?.name, record?.status, record?.toolCallId));
|
|
504
|
+
const percent = finitePercent(record?.percent);
|
|
505
|
+
switch (type) {
|
|
506
|
+
case 'assistant_chunk':
|
|
507
|
+
return { phase: '生成回复' };
|
|
508
|
+
case 'thought_chunk':
|
|
509
|
+
return { phase: '分析任务' };
|
|
510
|
+
case 'plan':
|
|
511
|
+
return { phase: '制定计划', message, percent };
|
|
512
|
+
case 'tool_call':
|
|
513
|
+
return { phase: '调用工具', message, percent };
|
|
514
|
+
case 'tool_update':
|
|
515
|
+
return { phase: '执行工具', message, percent };
|
|
516
|
+
case 'terminal_output':
|
|
517
|
+
return { phase: '执行命令', message };
|
|
518
|
+
case 'file_activity':
|
|
519
|
+
return { phase: '处理文件', message };
|
|
520
|
+
case 'artifact':
|
|
521
|
+
return { phase: '生成产物', message };
|
|
522
|
+
default:
|
|
523
|
+
return undefined;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
function runArtifact(artifact) {
|
|
527
|
+
return {
|
|
528
|
+
id: artifact.id,
|
|
529
|
+
name: artifact.name,
|
|
530
|
+
description: artifact.description,
|
|
531
|
+
url: artifact.url,
|
|
532
|
+
filename: artifact.filename,
|
|
533
|
+
mediaType: artifact.mediaType,
|
|
534
|
+
metadata: artifact.metadata ? structuredClone(artifact.metadata) : undefined
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
function firstString(...values) {
|
|
538
|
+
for (const value of values) {
|
|
539
|
+
if (typeof value === 'string' && value.trim())
|
|
540
|
+
return value.trim();
|
|
541
|
+
}
|
|
542
|
+
return undefined;
|
|
543
|
+
}
|
|
544
|
+
function clipProgress(value) {
|
|
545
|
+
if (!value)
|
|
546
|
+
return undefined;
|
|
547
|
+
return value.length > 500 ? `${value.slice(0, 500)}…` : value;
|
|
548
|
+
}
|
|
549
|
+
function finitePercent(value) {
|
|
550
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0 && value <= 100
|
|
551
|
+
? value
|
|
552
|
+
: undefined;
|
|
553
|
+
}
|
|
554
|
+
function isRecord(value) {
|
|
555
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
200
556
|
}
|
|
201
557
|
export class SessionNotFoundError extends Error {
|
|
202
558
|
sessionId;
|
|
203
559
|
constructor(sessionId) {
|
|
204
|
-
super(`Nexus
|
|
560
|
+
super(`Agent Nexus session not found: ${sessionId}`);
|
|
205
561
|
this.sessionId = sessionId;
|
|
206
562
|
}
|
|
207
563
|
}
|
|
564
|
+
export class SessionRequestError extends Error {
|
|
565
|
+
status;
|
|
566
|
+
constructor(status, message) {
|
|
567
|
+
super(message);
|
|
568
|
+
this.status = status;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
export class RunNotFoundError extends Error {
|
|
572
|
+
runId;
|
|
573
|
+
constructor(runId) {
|
|
574
|
+
super(`Agent Nexus run not found: ${runId}`);
|
|
575
|
+
this.runId = runId;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function filterAgents(agents, ids) {
|
|
579
|
+
return agents
|
|
580
|
+
.filter((agent) => !ids || ids.has(agent.id))
|
|
581
|
+
.map((agent) => structuredClone(agent));
|
|
582
|
+
}
|
|
583
|
+
function isTerminal(state) {
|
|
584
|
+
return state === 'completed' || state === 'failed' || state === 'canceled';
|
|
585
|
+
}
|
|
586
|
+
function isActive(state) {
|
|
587
|
+
return state === 'running' || state === 'input_required' || state === 'permission_required';
|
|
588
|
+
}
|
|
208
589
|
function errorMessage(error) {
|
|
209
590
|
return error instanceof Error ? error.message : String(error);
|
|
210
591
|
}
|
|
592
|
+
function attachmentView(attachment) {
|
|
593
|
+
return {
|
|
594
|
+
id: attachment.id,
|
|
595
|
+
name: attachment.name,
|
|
596
|
+
mediaType: attachment.mediaType,
|
|
597
|
+
size: attachment.bytes.length
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
function safeAttachmentName(value) {
|
|
601
|
+
return String(value || '')
|
|
602
|
+
.replace(/[<>:"/\\|?*\u0000-\u001f]/g, '_')
|
|
603
|
+
.trim()
|
|
604
|
+
.slice(0, 180);
|
|
605
|
+
}
|