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/dist/session.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { AcpProcessRuntime, InvalidPermissionAnswerError } from './acp/runtime.js';
2
+ import { open, realpath } from 'node:fs/promises';
3
+ import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
4
+ import { AcpProcessRuntime } from './acp/runtime.js';
3
5
  import { A2AClientRuntime, probeA2AAgent } from './a2a/runtime.js';
4
6
  import { SessionEventLog } from './events.js';
5
7
  const MAX_SESSION_ARTIFACTS = 64;
@@ -9,6 +11,7 @@ const READINESS_CACHE_MS = 20_000;
9
11
  const MAX_INPUT_ATTACHMENTS = 16;
10
12
  const MAX_INPUT_ATTACHMENT_BYTES = 16 * 1024 * 1024;
11
13
  const MAX_SESSION_INPUT_BYTES = 32 * 1024 * 1024;
14
+ const MAX_PUBLISHED_FILE_BYTES = 12 * 1024 * 1024;
12
15
  export class ManagedSession {
13
16
  agentId;
14
17
  protocol;
@@ -17,6 +20,7 @@ export class ManagedSession {
17
20
  maxOutputChars;
18
21
  agentName;
19
22
  runStore;
23
+ instanceId;
20
24
  id = randomUUID();
21
25
  createdAt = Date.now();
22
26
  events;
@@ -27,10 +31,11 @@ export class ManagedSession {
27
31
  output = '';
28
32
  error;
29
33
  pendingRequest;
34
+ completion;
30
35
  artifacts = [];
31
36
  inputAttachments = new Map();
32
37
  runtime;
33
- constructor(agentId, protocol, workspace, ownerKeyId, maxEvents, maxOutputChars, agentName, runStore) {
38
+ constructor(agentId, protocol, workspace, ownerKeyId, maxEvents, maxOutputChars, agentName, runStore, instanceId = randomUUID()) {
34
39
  this.agentId = agentId;
35
40
  this.protocol = protocol;
36
41
  this.workspace = workspace;
@@ -38,6 +43,7 @@ export class ManagedSession {
38
43
  this.maxOutputChars = maxOutputChars;
39
44
  this.agentName = agentName;
40
45
  this.runStore = runStore;
46
+ this.instanceId = instanceId;
41
47
  this.events = new SessionEventLog(this.id, maxEvents);
42
48
  this.events.append('session_state', { state: this.state, protocol });
43
49
  }
@@ -56,6 +62,8 @@ export class ManagedSession {
56
62
  setState(state, error) {
57
63
  if (this.state === state && this.error === error)
58
64
  return;
65
+ if (state !== 'completed')
66
+ this.completion = undefined;
59
67
  this.state = state;
60
68
  this.error = error;
61
69
  this.updatedAt = Date.now();
@@ -66,15 +74,52 @@ export class ManagedSession {
66
74
  : state === 'canceled'
67
75
  ? 'canceled'
68
76
  : 'session_state';
69
- this.events.append(type, { state, ...(error ? { error } : {}) });
77
+ this.events.append(type, {
78
+ state,
79
+ ...(error ? { error } : {}),
80
+ ...(state === 'completed' && this.completion
81
+ ? { completion: structuredClone(this.completion) }
82
+ : {})
83
+ });
70
84
  const endedAt = isTerminal(state) ? this.updatedAt : undefined;
71
85
  this.syncRun({
72
86
  state,
73
87
  error,
74
88
  progress: progressForState(state, error),
89
+ completion: state === 'completed' && this.completion
90
+ ? structuredClone(this.completion)
91
+ : undefined,
75
92
  ...(endedAt ? { endedAt } : {})
76
93
  });
77
94
  }
95
+ completeTurn(proof) {
96
+ if (this.pendingRequest || isInteractive(this.state)) {
97
+ this.emit('terminal_output', {
98
+ stream: 'system',
99
+ text: `Ignored ${proof.source} completion while the session is waiting for ${this.pendingRequest?.kind || 'input'}.`
100
+ });
101
+ return false;
102
+ }
103
+ if (this.state !== 'running')
104
+ return false;
105
+ const outputPresent = this.output.trim().length > 0;
106
+ const artifactCount = this.artifacts.length;
107
+ if (!outputPresent && artifactCount < 1) {
108
+ this.setState('failed', `Agent ended the turn (${proof.stopReason}) without a final response or artifact.`);
109
+ return false;
110
+ }
111
+ this.completion = {
112
+ ...structuredClone(proof),
113
+ runId: this.currentRunId,
114
+ protocol: this.protocol,
115
+ verified: true,
116
+ outputPresent,
117
+ artifactCount,
118
+ completedAt: Date.now()
119
+ };
120
+ this.setState('completed');
121
+ return true;
122
+ }
78
123
  appendOutput(text) {
79
124
  if (!text)
80
125
  return;
@@ -116,7 +161,72 @@ export class ManagedSession {
116
161
  artifactCount: this.artifacts.length
117
162
  });
118
163
  }
164
+ async publishFile(filePath) {
165
+ const requestedPath = String(filePath || '').trim();
166
+ if (!requestedPath) {
167
+ throw new SessionRequestError(400, 'path is required');
168
+ }
169
+ if (!this.workspace) {
170
+ throw new SessionRequestError(400, 'This session does not have a local workspace');
171
+ }
172
+ let handle;
173
+ try {
174
+ const workspaceRoot = await realpath(this.workspace);
175
+ const unresolved = resolve(workspaceRoot, requestedPath);
176
+ const target = await realpath(unresolved);
177
+ assertWorkspacePath(workspaceRoot, target);
178
+ handle = await open(target, 'r');
179
+ // Resolve again after opening so a swapped symlink cannot silently
180
+ // redirect a publish request outside the configured workspace.
181
+ const verifiedTarget = await realpath(unresolved);
182
+ assertWorkspacePath(workspaceRoot, verifiedTarget);
183
+ if (verifiedTarget !== target) {
184
+ throw new SessionRequestError(409, 'Published file changed while it was being opened');
185
+ }
186
+ const stats = await handle.stat();
187
+ if (!stats.isFile()) {
188
+ throw new SessionRequestError(400, 'Only regular files can be published');
189
+ }
190
+ if (stats.size > MAX_PUBLISHED_FILE_BYTES) {
191
+ throw new SessionRequestError(413, `Published file exceeds the ${MAX_PUBLISHED_FILE_BYTES} byte limit`);
192
+ }
193
+ const bytes = await handle.readFile();
194
+ const filename = basename(verifiedTarget);
195
+ const workspacePath = relative(workspaceRoot, verifiedTarget).split(sep).join('/');
196
+ const artifact = {
197
+ id: `published:${randomUUID()}`,
198
+ name: filename,
199
+ filename,
200
+ mediaType: mediaTypeForPath(filename),
201
+ bytesBase64: bytes.toString('base64'),
202
+ metadata: {
203
+ source: 'workspace_publish',
204
+ path: workspacePath,
205
+ size: bytes.length
206
+ }
207
+ };
208
+ this.addArtifact(artifact);
209
+ this.emit('artifact', runArtifact(artifact));
210
+ return artifact;
211
+ }
212
+ catch (error) {
213
+ if (error instanceof SessionRequestError)
214
+ throw error;
215
+ const code = error?.code;
216
+ if (code === 'ENOENT') {
217
+ throw new SessionRequestError(404, 'Published file was not found');
218
+ }
219
+ if (code === 'EACCES' || code === 'EPERM') {
220
+ throw new SessionRequestError(403, 'Published file cannot be read');
221
+ }
222
+ throw error;
223
+ }
224
+ finally {
225
+ await handle?.close();
226
+ }
227
+ }
119
228
  setPending(request) {
229
+ this.completion = undefined;
120
230
  this.pendingRequest = structuredClone(request);
121
231
  this.state = request.kind === 'permission' ? 'permission_required' : 'input_required';
122
232
  this.updatedAt = Date.now();
@@ -167,18 +277,10 @@ export class ManagedSession {
167
277
  throw new SessionRequestError(409, 'Agent runtime is unavailable');
168
278
  const attachments = this.inputAttachmentsFor(attachmentIds);
169
279
  if (this.pendingRequest) {
170
- this.syncRun({
171
- progress: { phase: '继续执行', message: '已提交补充信息' }
172
- });
173
- try {
174
- await this.runtime.respondPending(message, attachments);
175
- }
176
- catch (error) {
177
- if (error instanceof InvalidPermissionAnswerError) {
178
- throw new SessionRequestError(400, error.message);
179
- }
180
- throw error;
181
- }
280
+ await this.resolvePending({
281
+ requestId: this.pendingRequest.id,
282
+ message
283
+ }, attachmentIds);
182
284
  return;
183
285
  }
184
286
  if (this.state === 'running') {
@@ -186,6 +288,7 @@ export class ManagedSession {
186
288
  }
187
289
  this.output = '';
188
290
  this.artifacts = [];
291
+ this.completion = undefined;
189
292
  this.error = undefined;
190
293
  this.updatedAt = Date.now();
191
294
  this.currentRunId = this.runStore?.create({
@@ -204,6 +307,23 @@ export class ManagedSession {
204
307
  this.setState('failed', errorMessage(error));
205
308
  });
206
309
  }
310
+ async resolvePending(response, attachmentIds = []) {
311
+ if (!this.runtime) {
312
+ throw new SessionRequestError(409, 'Agent runtime is unavailable');
313
+ }
314
+ const pending = this.pendingRequest;
315
+ if (!pending) {
316
+ throw new SessionRequestError(409, 'Session is not waiting for input');
317
+ }
318
+ if (pending.id !== response.requestId) {
319
+ throw new SessionRequestError(409, `Pending request no longer matches: ${response.requestId}`);
320
+ }
321
+ const attachments = this.inputAttachmentsFor(attachmentIds);
322
+ this.syncRun({
323
+ progress: { phase: '继续执行', message: '已提交补充信息' }
324
+ });
325
+ await this.runtime.respondPending(response, attachments);
326
+ }
207
327
  async cancel() {
208
328
  await this.runtime?.cancel();
209
329
  }
@@ -214,6 +334,7 @@ export class ManagedSession {
214
334
  snapshot() {
215
335
  return {
216
336
  id: this.id,
337
+ instanceId: this.instanceId,
217
338
  runId: this.currentRunId,
218
339
  protocol: this.protocol,
219
340
  protocolSessionId: this.protocolSessionId,
@@ -228,6 +349,9 @@ export class ManagedSession {
228
349
  pendingRequest: this.pendingRequest
229
350
  ? structuredClone(this.pendingRequest)
230
351
  : undefined,
352
+ completion: this.completion
353
+ ? structuredClone(this.completion)
354
+ : undefined,
231
355
  lastEventId: this.events.lastId,
232
356
  createdAt: this.createdAt,
233
357
  updatedAt: this.updatedAt
@@ -247,6 +371,9 @@ export class ManagedSession {
247
371
  output: this.output || undefined,
248
372
  artifacts: this.artifacts.map(runArtifact),
249
373
  artifactCount: this.artifacts.length,
374
+ completion: this.completion
375
+ ? structuredClone(this.completion)
376
+ : undefined,
250
377
  updatedAt: this.updatedAt,
251
378
  ...patch
252
379
  });
@@ -265,6 +392,7 @@ export class SessionManager {
265
392
  workspacePolicy;
266
393
  drivers;
267
394
  runStore;
395
+ instanceId = randomUUID();
268
396
  sessions = new Map();
269
397
  cleanupTimer;
270
398
  readinessCache;
@@ -379,10 +507,10 @@ export class SessionManager {
379
507
  }
380
508
  workspace = await this.workspacePolicy.resolve(requestedWorkspace);
381
509
  }
382
- const session = new ManagedSession(agentId, config.protocol === 'a2a' ? 'a2a' : 'acp', workspace, ownerKeyId, this.config.maxEventsPerSession, this.config.maxOutputChars, config.name || agentId, this.runStore);
510
+ const session = new ManagedSession(agentId, config.protocol === 'a2a' ? 'a2a' : 'acp', workspace, ownerKeyId, this.config.maxEventsPerSession, this.config.maxOutputChars, config.name || agentId, this.runStore, this.instanceId);
383
511
  const runtime = config.protocol === 'a2a'
384
512
  ? new A2AClientRuntime(config, session, this.config.promptTimeoutMs || 30 * 60_000)
385
- : new AcpProcessRuntime(driver, session, this.config.maxOutputChars, this.config.promptTimeoutMs || 30 * 60_000, config.instructions);
513
+ : new AcpProcessRuntime(driver, session, this.config.maxOutputChars, this.config.promptTimeoutMs || 30 * 60_000);
386
514
  this.sessions.set(session.id, session);
387
515
  session.attach(runtime);
388
516
  try {
@@ -422,15 +550,34 @@ export class SessionManager {
422
550
  await session.message(text, attachmentIds);
423
551
  return session.snapshot();
424
552
  }
553
+ async resolvePending(id, response, attachmentIds = []) {
554
+ const session = this.require(id);
555
+ await session.resolvePending(response, attachmentIds);
556
+ return session.snapshot();
557
+ }
425
558
  addInputAttachment(id, name, mediaType, bytes) {
426
559
  const session = this.require(id);
427
560
  return session.addInputAttachment(name, mediaType, bytes);
428
561
  }
562
+ async publishFile(id, path) {
563
+ const session = this.require(id);
564
+ await session.publishFile(path);
565
+ return session.snapshot();
566
+ }
429
567
  async cancel(id) {
430
568
  const session = this.require(id);
431
569
  await session.cancel();
432
570
  return session.snapshot();
433
571
  }
572
+ async close(id) {
573
+ const session = this.require(id);
574
+ if (isActive(session.state))
575
+ await session.cancel();
576
+ const snapshot = session.snapshot();
577
+ await session.dispose();
578
+ this.sessions.delete(id);
579
+ return snapshot;
580
+ }
434
581
  eventsAfter(id, after) {
435
582
  return this.require(id).events.after(after);
436
583
  }
@@ -542,6 +689,41 @@ function runArtifact(artifact) {
542
689
  metadata: artifact.metadata ? structuredClone(artifact.metadata) : undefined
543
690
  };
544
691
  }
692
+ function assertWorkspacePath(workspaceRoot, target) {
693
+ const child = relative(workspaceRoot, target);
694
+ if (child === '..' || child.startsWith(`..${sep}`) || isAbsolute(child)) {
695
+ throw new SessionRequestError(403, 'Published files must stay inside the session workspace');
696
+ }
697
+ }
698
+ function mediaTypeForPath(path) {
699
+ switch (extname(path).toLowerCase()) {
700
+ case '.txt':
701
+ case '.log':
702
+ case '.md':
703
+ return 'text/plain';
704
+ case '.json':
705
+ return 'application/json';
706
+ case '.pdf':
707
+ return 'application/pdf';
708
+ case '.png':
709
+ return 'image/png';
710
+ case '.jpg':
711
+ case '.jpeg':
712
+ return 'image/jpeg';
713
+ case '.gif':
714
+ return 'image/gif';
715
+ case '.webp':
716
+ return 'image/webp';
717
+ case '.svg':
718
+ return 'image/svg+xml';
719
+ case '.csv':
720
+ return 'text/csv';
721
+ case '.zip':
722
+ return 'application/zip';
723
+ default:
724
+ return 'application/octet-stream';
725
+ }
726
+ }
545
727
  function firstString(...values) {
546
728
  for (const value of values) {
547
729
  if (typeof value === 'string' && value.trim())
@@ -594,6 +776,9 @@ function isTerminal(state) {
594
776
  function isActive(state) {
595
777
  return state === 'running' || state === 'input_required' || state === 'permission_required';
596
778
  }
779
+ function isInteractive(state) {
780
+ return state === 'input_required' || state === 'permission_required';
781
+ }
597
782
  function errorMessage(error) {
598
783
  return error instanceof Error ? error.message : String(error);
599
784
  }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,19 @@
1
1
  export type AgentdSessionState = 'created' | 'running' | 'input_required' | 'permission_required' | 'completed' | 'failed' | 'canceled';
2
2
  export type AgentdProtocol = 'acp' | 'a2a';
3
- export type PermissionPolicy = 'ask' | 'allow' | 'deny';
3
+ export type PermissionPolicy = 'ask' | 'deny';
4
+ export type AgentdTurnCompletionSource = 'acp_prompt_response' | 'a2a_task_status' | 'a2a_message_stream';
5
+ export interface AgentdTurnCompletionProof {
6
+ source: AgentdTurnCompletionSource;
7
+ stopReason: string;
8
+ }
9
+ export interface AgentdTurnCompletion extends AgentdTurnCompletionProof {
10
+ runId?: string;
11
+ protocol: AgentdProtocol;
12
+ verified: true;
13
+ outputPresent: boolean;
14
+ artifactCount: number;
15
+ completedAt: number;
16
+ }
4
17
  export declare const agentdDriverKinds: readonly ["opencode", "claude", "codex", "pi", "openclaw", "hermes"];
5
18
  export type AgentdDriverKind = (typeof agentdDriverKinds)[number];
6
19
  export interface AgentdDriverConfig {
@@ -14,8 +27,6 @@ export interface AgentdDriverConfig {
14
27
  args?: string[];
15
28
  inheritEnv?: string[];
16
29
  env?: Record<string, string>;
17
- /** Optional extra instructions appended to the built-in Agent Nexus guidance. */
18
- instructions?: string;
19
30
  permissionPolicy?: PermissionPolicy;
20
31
  permissionTimeoutMs?: number;
21
32
  }
@@ -25,8 +36,6 @@ export interface AgentdA2AConfig {
25
36
  protocol: 'a2a';
26
37
  name?: string;
27
38
  description?: string;
28
- /** Optional extra instructions sent with the first A2A user message. */
29
- instructions?: string;
30
39
  enabled?: boolean;
31
40
  /** Full Agent Card JSON URL. Preferred for new configurations. */
32
41
  agentCardUrl?: string;
@@ -67,18 +76,6 @@ export interface AgentdConfig {
67
76
  workspaceRoots: string[];
68
77
  maxRequestBytes: number;
69
78
  maxAttachmentBytes?: number;
70
- /** Gateway-owned directory for temporary, anonymously downloadable artifacts. */
71
- artifactStoragePath?: string;
72
- /** Maximum size of one published artifact. */
73
- maxArtifactBytes?: number;
74
- /** Maximum combined size of all retained published artifacts. */
75
- maxArtifactStorageBytes?: number;
76
- /** Maximum number of retained published artifacts. */
77
- maxPublishedArtifacts?: number;
78
- /** Maximum number of simultaneous artifact copies. */
79
- maxConcurrentArtifactPublishes?: number;
80
- /** Lifetime of a published artifact URL. */
81
- artifactTtlMs?: number;
82
79
  maxEventsPerSession: number;
83
80
  maxOutputChars: number;
84
81
  sessionTtlMs: number;
@@ -92,27 +89,6 @@ export interface AgentdConfig {
92
89
  secureAdminCookies?: boolean;
93
90
  agents: Record<string, AgentdAgentConfig>;
94
91
  }
95
- export interface AgentdFileRootView {
96
- id: string;
97
- name: string;
98
- path: string;
99
- }
100
- export interface AgentdFileEntryView {
101
- name: string;
102
- path: string;
103
- type: 'file' | 'directory' | 'symlink' | 'other';
104
- size?: number;
105
- modifiedAt: number;
106
- }
107
- export interface AgentdPublishedFile {
108
- id: string;
109
- name: string;
110
- url: string;
111
- size: number;
112
- mediaType: string;
113
- sha256: string;
114
- expiresAt: number;
115
- }
116
92
  export interface AgentdAgentView {
117
93
  id: string;
118
94
  name: string;
@@ -132,7 +108,6 @@ export interface AgentdAgentConfigView {
132
108
  protocol: AgentdProtocol;
133
109
  name: string;
134
110
  description?: string;
135
- instructions?: string;
136
111
  enabled: boolean;
137
112
  driver?: AgentdDriverKind;
138
113
  workspace?: string;
@@ -186,17 +161,17 @@ export interface AgentdPendingRequest {
186
161
  id: string;
187
162
  kind: 'permission' | 'input';
188
163
  prompt: string;
189
- /** Optional business-facing step, for example `payment` or `pickup_time`. */
190
- step?: string;
191
- /** Optional hint for clients rendering the pending interaction. */
192
- inputType?: 'text' | 'choice' | 'confirmation' | 'payment' | 'unknown';
193
164
  options?: Array<{
194
165
  id: string;
195
166
  name: string;
196
167
  kind?: string;
197
168
  }>;
198
- /** JSON-safe metadata for the owning integration. Never put secrets here. */
199
- metadata?: Record<string, unknown>;
169
+ }
170
+ export interface AgentdPendingResponse {
171
+ requestId: string;
172
+ message?: string;
173
+ optionId?: string;
174
+ action?: 'accept' | 'decline' | 'cancel';
200
175
  }
201
176
  export interface AgentdArtifact {
202
177
  id?: string;
@@ -212,6 +187,8 @@ export interface AgentdArtifact {
212
187
  }
213
188
  export interface AgentdSessionView {
214
189
  id: string;
190
+ /** Identifies the Gateway process that owns this in-memory session. */
191
+ instanceId?: string;
215
192
  runId?: string;
216
193
  protocol: AgentdProtocol;
217
194
  protocolSessionId?: string;
@@ -225,6 +202,8 @@ export interface AgentdSessionView {
225
202
  artifacts: AgentdArtifact[];
226
203
  inputAttachments?: AgentdInputAttachmentView[];
227
204
  pendingRequest?: AgentdPendingRequest;
205
+ /** Structural proof that the current protocol turn reached a valid terminal boundary. */
206
+ completion?: AgentdTurnCompletion;
228
207
  lastEventId?: string;
229
208
  createdAt: number;
230
209
  updatedAt: number;
@@ -259,6 +238,7 @@ export interface AgentdRunView {
259
238
  error?: string;
260
239
  artifactCount: number;
261
240
  inputAttachmentCount?: number;
241
+ completion?: AgentdTurnCompletion;
262
242
  startedAt: number;
263
243
  updatedAt: number;
264
244
  endedAt?: number;