nexus-agentd 0.3.0 → 0.3.1

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 CHANGED
@@ -1,22 +1,22 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 lumia1998
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
22
-
1
+ MIT License
2
+
3
+ Copyright (c) 2026 lumia1998
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
@@ -126,6 +126,12 @@ export declare class ArtifactStore {
126
126
  readArtifact(runId: string, artifactId: string): Promise<ArtifactReadResult | undefined>;
127
127
  /** Queue removal of one run's own directory. Active runs are protected. */
128
128
  removeRun(runId: string): void;
129
+ /**
130
+ * Delete a single artifact: drop its payload file and record entry. The
131
+ * run record itself stays; its artifacts list shrinks via the views
132
+ * callback. Refused while the payload write is still in flight.
133
+ */
134
+ removeArtifact(runId: string, artifactId: string): Promise<boolean>;
129
135
  flush(): Promise<void>;
130
136
  get queuedByteCount(): number;
131
137
  get storedByteCount(): number;
@@ -359,6 +359,36 @@ export class ArtifactStore {
359
359
  this.pendingDeletes.add(runId);
360
360
  this.scheduleDrain();
361
361
  }
362
+ /**
363
+ * Delete a single artifact: drop its payload file and record entry. The
364
+ * run record itself stays; its artifacts list shrinks via the views
365
+ * callback. Refused while the payload write is still in flight.
366
+ */
367
+ async removeArtifact(runId, artifactId) {
368
+ this.assertInitialized();
369
+ const key = entryKey(runId, artifactId);
370
+ const entry = this.entries.get(key);
371
+ if (!entry)
372
+ return false;
373
+ if (this.inFlight.has(key))
374
+ return false;
375
+ const batch = this.pending.get(runId);
376
+ const queued = batch?.get(artifactId);
377
+ if (queued) {
378
+ this.queuedBytes -= queued.bytes.length;
379
+ batch.delete(artifactId);
380
+ if (!batch.size)
381
+ this.pending.delete(runId);
382
+ }
383
+ if (entry.filePath) {
384
+ await this.safeUnlink(entry.filePath);
385
+ this.totalBytes -= entry.bytes || 0;
386
+ }
387
+ this.entries.delete(key);
388
+ await this.writeManifest(runId);
389
+ this.emit(runId, this.viewsForRun(runId));
390
+ return true;
391
+ }
362
392
  async flush() {
363
393
  this.assertInitialized();
364
394
  if (this.activeDrain)
@@ -35,6 +35,32 @@ export interface RunListQuery {
35
35
  limit?: number;
36
36
  offset?: number;
37
37
  }
38
+ export interface RunArtifactQuery {
39
+ agentId?: string;
40
+ /** 'all' keeps every kind; otherwise one of artifactKind() values. */
41
+ kind?: string;
42
+ query?: string;
43
+ limit?: number;
44
+ offset?: number;
45
+ }
46
+ export interface RunArtifactRow {
47
+ id: string;
48
+ runId: string;
49
+ agentId: string;
50
+ agentName: string;
51
+ runState: AgentdSessionState;
52
+ runStartedAt: number;
53
+ name?: string;
54
+ filename?: string;
55
+ mediaType?: string;
56
+ size?: number;
57
+ kind: string;
58
+ downloadable: boolean;
59
+ storageStatus?: string;
60
+ createdAt?: number;
61
+ }
62
+ /** Coarse bucket used by the console filter and list grouping. */
63
+ export declare function artifactKind(mediaType: string | undefined): "image" | "audio" | "video" | "file";
38
64
  export declare class RunStore {
39
65
  readonly filePath: string;
40
66
  private readonly runs;
@@ -60,6 +86,13 @@ export declare class RunStore {
60
86
  recordArtifacts(runId: string, artifacts: AgentdArtifact[]): StoredArtifactView[] | undefined;
61
87
  artifactViews(runId: string): StoredArtifactView[] | undefined;
62
88
  readArtifact(runId: string, artifactId: string): Promise<ArtifactReadResult | undefined>;
89
+ /** Flat, newest-first artifact index across all retained runs. */
90
+ listArtifacts(query?: RunArtifactQuery): {
91
+ artifacts: RunArtifactRow[];
92
+ total: number;
93
+ };
94
+ /** Delete one artifact's payload and record entry; the run record stays. */
95
+ removeArtifact(runId: string, artifactId: string): Promise<boolean>;
63
96
  getOwnerKeyId(runId: string): string | undefined;
64
97
  metrics(): {
65
98
  historyBytes: number;
@@ -78,6 +111,11 @@ export declare class RunStore {
78
111
  artifactStoreStats: ArtifactStoreMetrics;
79
112
  };
80
113
  setRetryOfRunId(runId: string, retryOfRunId?: string): AgentdRunDetail | undefined;
114
+ /**
115
+ * Remove a terminal run record and its stored artifacts. Active runs are
116
+ * refused so a live turn cannot lose its progress tracking.
117
+ */
118
+ deleteRun(id: string): AgentdRunDetail | undefined;
81
119
  get(id: string): AgentdRunDetail | undefined;
82
120
  list(query?: RunListQuery): {
83
121
  runs: AgentdRunView[];
package/dist/run-store.js CHANGED
@@ -3,6 +3,17 @@ import { chmod, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promi
3
3
  import path from 'node:path';
4
4
  import { ArtifactStore, artifactStorePathForRunStore } from './artifact-store.js';
5
5
  export { ArtifactStore, artifactStorePathForRunStore } from './artifact-store.js';
6
+ /** Coarse bucket used by the console filter and list grouping. */
7
+ export function artifactKind(mediaType) {
8
+ const type = String(mediaType || '').toLowerCase();
9
+ if (type.startsWith('image/'))
10
+ return 'image';
11
+ if (type.startsWith('video/'))
12
+ return 'video';
13
+ if (type.startsWith('audio/'))
14
+ return 'audio';
15
+ return 'file';
16
+ }
6
17
  const STATES = new Set([
7
18
  'created',
8
19
  'running',
@@ -187,6 +198,55 @@ export class RunStore {
187
198
  return undefined;
188
199
  return this.artifactStore.readArtifact(runId, artifactId);
189
200
  }
201
+ /** Flat, newest-first artifact index across all retained runs. */
202
+ listArtifacts(query = {}) {
203
+ this.assertInitialized();
204
+ const needle = String(query.query || '').trim().toLowerCase();
205
+ const limit = Math.min(200, Math.max(1, Number(query.limit) || 50));
206
+ const offset = Math.max(0, Math.floor(Number(query.offset) || 0));
207
+ const rows = [];
208
+ for (const run of this.runs.values()) {
209
+ for (const view of run.artifacts || []) {
210
+ if (!view.id)
211
+ continue;
212
+ if (query.agentId && run.agentId !== query.agentId)
213
+ continue;
214
+ const kind = artifactKind(view.mediaType);
215
+ if (query.kind && query.kind !== 'all' && kind !== query.kind)
216
+ continue;
217
+ if (needle) {
218
+ const haystack = `${view.name || ''} ${view.filename || ''}`.toLowerCase();
219
+ if (!haystack.includes(needle))
220
+ continue;
221
+ }
222
+ rows.push({
223
+ id: view.id,
224
+ runId: run.id,
225
+ agentId: run.agentId,
226
+ agentName: run.agentName,
227
+ runState: run.state,
228
+ runStartedAt: run.startedAt,
229
+ name: view.name,
230
+ filename: view.filename,
231
+ mediaType: view.mediaType,
232
+ size: view.size,
233
+ kind,
234
+ downloadable: view.downloadable === true,
235
+ storageStatus: view.storageStatus,
236
+ createdAt: view.createdAt
237
+ });
238
+ }
239
+ }
240
+ rows.sort((left, right) => (right.createdAt || 0) - (left.createdAt || 0));
241
+ return { artifacts: rows.slice(offset, offset + limit), total: rows.length };
242
+ }
243
+ /** Delete one artifact's payload and record entry; the run record stays. */
244
+ async removeArtifact(runId, artifactId) {
245
+ this.assertInitialized();
246
+ if (!this.runs.has(runId))
247
+ return false;
248
+ return this.artifactStore.removeArtifact(runId, artifactId);
249
+ }
190
250
  getOwnerKeyId(runId) {
191
251
  this.assertInitialized();
192
252
  return this.runs.get(runId)?.ownerKeyId;
@@ -211,6 +271,22 @@ export class RunStore {
211
271
  setRetryOfRunId(runId, retryOfRunId) {
212
272
  return this.update(runId, { retryOfRunId });
213
273
  }
274
+ /**
275
+ * Remove a terminal run record and its stored artifacts. Active runs are
276
+ * refused so a live turn cannot lose its progress tracking.
277
+ */
278
+ deleteRun(id) {
279
+ this.assertInitialized();
280
+ const run = this.runs.get(id);
281
+ if (!run)
282
+ return undefined;
283
+ if (isActive(run.state))
284
+ return undefined;
285
+ this.runs.delete(id);
286
+ this.artifactStore.removeRun(id);
287
+ this.changed();
288
+ return publicDetail(run);
289
+ }
214
290
  get(id) {
215
291
  this.assertInitialized();
216
292
  const run = this.runs.get(id);
package/dist/server.js CHANGED
@@ -505,15 +505,68 @@ async function handleAdminRoute(context, url) {
505
505
  await handleAdminRunAction(context, decodeURIComponent(runActionMatch[1]), runActionMatch[2]);
506
506
  return;
507
507
  }
508
+ if (url.pathname === '/v1/admin/artifacts' && request.method === 'GET') {
509
+ writeJson(response, 200, sessions.listArtifacts({
510
+ agentId: cleanQuery(url.searchParams.get('agentId')),
511
+ kind: cleanQuery(url.searchParams.get('kind')),
512
+ query: cleanQuery(url.searchParams.get('q')),
513
+ offset: boundedOffset(url.searchParams.get('offset')),
514
+ limit: boundedLimit(url.searchParams.get('limit'))
515
+ }));
516
+ return;
517
+ }
508
518
  const adminArtifactMatch = url.pathname.match(/^\/v1\/admin\/runs\/([^/]+)\/artifacts\/([^/]+)$/);
509
519
  if (adminArtifactMatch && request.method === 'GET') {
510
520
  const run = sessions.getRun(decodeURIComponent(adminArtifactMatch[1]));
511
521
  await writeRunArtifact(response, sessions, run, decodeURIComponent(adminArtifactMatch[2]));
512
522
  return;
513
523
  }
524
+ if (adminArtifactMatch && request.method === 'DELETE') {
525
+ await assertEmptyJsonBody(request, config.maxRequestBytes);
526
+ const runId = decodeURIComponent(adminArtifactMatch[1]);
527
+ const artifactId = decodeURIComponent(adminArtifactMatch[2]);
528
+ const run = sessions.getRun(runId);
529
+ if (!isTerminalState(run.state)) {
530
+ throw new RequestError(409, 'Artifacts of active runs cannot be deleted; cancel the task first');
531
+ }
532
+ const deleted = await sessions.removeRunArtifact(runId, artifactId);
533
+ if (!deleted)
534
+ throw new RequestError(404, 'Agent Nexus artifact not found');
535
+ console.info(JSON.stringify({
536
+ level: 'info',
537
+ event: 'admin_artifact_deleted',
538
+ runId,
539
+ artifactId,
540
+ agentId: run.agentId,
541
+ requestId: String(response.getHeader('X-Request-Id') || '')
542
+ }));
543
+ writeJson(response, 200, { deleted: true, runId, artifactId });
544
+ return;
545
+ }
514
546
  const runMatch = url.pathname.match(/^\/v1\/admin\/runs\/([^/]+)$/);
515
- if (runMatch && request.method === 'GET') {
547
+ if (runMatch && (request.method === 'GET' || request.method === 'DELETE')) {
516
548
  const runId = decodeURIComponent(runMatch[1]);
549
+ if (request.method === 'DELETE') {
550
+ await assertEmptyJsonBody(request, config.maxRequestBytes);
551
+ const detail = sessions.getRun(runId);
552
+ if (!isTerminalState(detail.state)) {
553
+ throw new RequestError(409, 'Active runs cannot be deleted; cancel the task first');
554
+ }
555
+ const deleted = sessions.deleteRun(runId);
556
+ if (!deleted)
557
+ throw new RequestError(404, 'Agent Nexus run not found');
558
+ console.info(JSON.stringify({
559
+ level: 'info',
560
+ event: 'admin_run_record_deleted',
561
+ runId,
562
+ sessionId: detail.sessionId,
563
+ agentId: detail.agentId,
564
+ state: detail.state,
565
+ requestId: String(response.getHeader('X-Request-Id') || '')
566
+ }));
567
+ writeJson(response, 200, { deleted: true, id: runId });
568
+ return;
569
+ }
517
570
  const detail = sessions.getRun(runId);
518
571
  writeJson(response, 200, {
519
572
  ...detail,
@@ -778,6 +831,7 @@ function buildRunControls(sessions, detail) {
778
831
  return {
779
832
  canCancel,
780
833
  canRetry,
834
+ canDelete: !active,
781
835
  ...(pendingRequest ? { pendingRequest: structuredClone(pendingRequest) } : {}),
782
836
  ...(unavailableReason ? { unavailableReason } : {})
783
837
  };
@@ -73,6 +73,7 @@ export class AdminRunManager {
73
73
  return {
74
74
  canCancel: current && active && Boolean(session?.runtimeAvailable()),
75
75
  canRetry,
76
+ canDelete: isTerminal(run.state),
76
77
  ...(pendingRequest ? { pendingRequest } : {}),
77
78
  ...(unavailableReason ? { unavailableReason } : {})
78
79
  };
package/dist/session.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { AgentDriver } from './drivers/index.js';
2
2
  import { SessionEventLog } from './events.js';
3
- import { RunStore, type RunListQuery } from './run-store.js';
3
+ import { RunStore, type RunListQuery, type RunArtifactQuery } from './run-store.js';
4
4
  import { type AdminRetryReservationFactory, type AdminRunOperationResult, type AdminRunControls } from './session/admin-runs.js';
5
5
  import type { AcpSessionSink, AgentSessionRuntime, AgentSessionSink } from './session-contract.js';
6
6
  import type { AgentdAgentView, AgentdArtifact, AgentdConfig, AgentdEvent, AgentdEventType, AgentdInputAttachmentView, AgentdPendingRequest, AgentdPendingResponse, AgentdProtocol, AgentdSessionState, AgentdSessionView, AgentdTurnCompletionProof } from './types.js';
@@ -114,8 +114,14 @@ export declare class SessionManager {
114
114
  };
115
115
  getRun(id: string): import("./types.js").AgentdRunDetail;
116
116
  getRunOwnerKeyId(id: string): string | undefined;
117
+ deleteRun(id: string): import("./types.js").AgentdRunDetail | undefined;
117
118
  ownsRun(id: string, keyId: string): boolean;
118
119
  getRunArtifacts(id: string): import("./types.js").AgentdRunArtifactView[];
120
+ listArtifacts(query?: RunArtifactQuery): {
121
+ artifacts: import("./run-store.js").RunArtifactRow[];
122
+ total: number;
123
+ };
124
+ removeRunArtifact(runId: string, artifactId: string): Promise<boolean | undefined>;
119
125
  getStorageMetrics(): ReturnType<RunStore['metrics']> | undefined;
120
126
  readRunArtifact(runId: string, artifactId: string): Promise<import("./artifact-store.js").ArtifactReadResult | undefined>;
121
127
  getRunArtifact(runId: string, artifactId: string): Promise<import("./artifact-store.js").ArtifactReadResult | undefined>;
package/dist/session.js CHANGED
@@ -654,12 +654,24 @@ export class SessionManager {
654
654
  const store = this.runStore;
655
655
  return store?.getOwnerKeyId?.(id);
656
656
  }
657
+ deleteRun(id) {
658
+ const store = this.runStore;
659
+ return store?.deleteRun?.(id);
660
+ }
657
661
  ownsRun(id, keyId) {
658
662
  return this.getRunOwnerKeyId(id) === keyId;
659
663
  }
660
664
  getRunArtifacts(id) {
661
665
  return this.getRun(id).artifacts;
662
666
  }
667
+ listArtifacts(query = {}) {
668
+ const store = this.runStore;
669
+ return store?.listArtifacts?.(query) || { artifacts: [], total: 0 };
670
+ }
671
+ async removeRunArtifact(runId, artifactId) {
672
+ const store = this.runStore;
673
+ return store?.removeArtifact?.(runId, artifactId);
674
+ }
663
675
  getStorageMetrics() {
664
676
  return this.runStore?.metrics();
665
677
  }
package/dist/types.d.ts CHANGED
@@ -268,6 +268,7 @@ export interface AgentdRunArtifactView {
268
268
  size?: number;
269
269
  downloadable?: boolean;
270
270
  storageStatus?: 'pending' | 'available' | 'metadata_only' | 'expired' | 'evicted' | 'failed';
271
+ createdAt?: number;
271
272
  }
272
273
  export interface AgentdRunView {
273
274
  id: string;
@@ -301,6 +302,7 @@ export interface AgentdRunDetail extends AgentdRunView {
301
302
  export interface AgentdRunControls {
302
303
  canCancel: boolean;
303
304
  canRetry: boolean;
305
+ canDelete?: boolean;
304
306
  pendingRequest?: AgentdPendingRequest;
305
307
  unavailableReason?: string;
306
308
  }