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.
Files changed (44) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +351 -179
  3. package/dist/a2a/runtime.d.ts +30 -0
  4. package/dist/a2a/runtime.js +374 -0
  5. package/dist/acp/runtime.d.ts +12 -3
  6. package/dist/acp/runtime.js +248 -17
  7. package/dist/artifact-store.d.ts +41 -0
  8. package/dist/artifact-store.js +340 -0
  9. package/dist/auth.d.ts +13 -0
  10. package/dist/auth.js +77 -0
  11. package/dist/cli.js +58 -1
  12. package/dist/config.d.ts +10 -0
  13. package/dist/config.js +281 -53
  14. package/dist/control-plane.d.ts +91 -0
  15. package/dist/control-plane.js +629 -0
  16. package/dist/drivers/hermes.d.ts +2 -0
  17. package/dist/drivers/hermes.js +10 -0
  18. package/dist/drivers/index.js +6 -0
  19. package/dist/events.js +13 -2
  20. package/dist/index.d.ts +6 -0
  21. package/dist/index.js +15 -3
  22. package/dist/run-store.d.ts +49 -0
  23. package/dist/run-store.js +291 -0
  24. package/dist/server.d.ts +3 -2
  25. package/dist/server.js +911 -41
  26. package/dist/session-contract.d.ts +16 -4
  27. package/dist/session.d.ts +74 -8
  28. package/dist/session.js +433 -38
  29. package/dist/types.d.ts +193 -6
  30. package/dist/types.js +2 -1
  31. package/dist/webui/app.d.ts +1 -0
  32. package/dist/webui/app.js +1072 -0
  33. package/dist/webui/icons.d.ts +9 -0
  34. package/dist/webui/icons.js +10 -0
  35. package/dist/webui/index.d.ts +3 -0
  36. package/dist/webui/index.js +32 -0
  37. package/dist/webui/markup.d.ts +1 -0
  38. package/dist/webui/markup.js +68 -0
  39. package/dist/webui/styles.d.ts +1 -0
  40. package/dist/webui/styles.js +269 -0
  41. package/dist/workspace-files.d.ts +48 -0
  42. package/dist/workspace-files.js +269 -0
  43. package/nexus-agentd.example.json +30 -27
  44. 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
- class ManagedSession {
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.updatedAt = Date.now();
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 nexus-agentd]\n${this.output.slice(-this.maxOutputChars)}`;
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
- ? 'permission_required'
60
- : 'input_required', request);
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 Error('ACP runtime is unavailable');
167
+ throw new SessionRequestError(409, 'Agent runtime is unavailable');
168
+ const attachments = this.inputAttachmentsFor(attachmentIds);
73
169
  if (this.pendingRequest) {
74
- await this.runtime.respondPending(message);
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 Error('ACP session is already processing a prompt');
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
- void this.runtime.prompt(message).catch((error) => {
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
- acpSessionId: this.acpSessionId,
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
- constructor(config, workspacePolicy, drivers) {
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.cleanupTimer = setInterval(() => void this.cleanup(), 60_000);
128
- this.cleanupTimer.unref?.();
280
+ this.restartCleanup();
129
281
  }
130
- async listAgents() {
131
- return Promise.all(Array.from(this.drivers.values()).map((driver) => driver.probe()));
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
- const driver = this.drivers.get(agentId);
135
- if (!driver)
136
- throw new Error(`Configured ACP agent not found: ${agentId}`);
137
- const workspace = await this.workspacePolicy.resolve(workspaceInput);
138
- const session = new ManagedSession(agentId, workspace, this.config.maxEventsPerSession, this.config.maxOutputChars);
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);
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
- throw error;
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
- async message(id, message) {
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 Error('message is required');
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) => session.dispose()));
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 Gateway session not found: ${sessionId}`);
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
+ }