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.
Files changed (46) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +376 -185
  3. package/dist/a2a/runtime.d.ts +32 -0
  4. package/dist/a2a/runtime.js +382 -0
  5. package/dist/acp/runtime.d.ts +14 -3
  6. package/dist/acp/runtime.js +256 -17
  7. package/dist/agent-instructions.d.ts +11 -0
  8. package/dist/agent-instructions.js +44 -0
  9. package/dist/artifact-store.d.ts +41 -0
  10. package/dist/artifact-store.js +340 -0
  11. package/dist/auth.d.ts +13 -0
  12. package/dist/auth.js +77 -0
  13. package/dist/cli.js +58 -1
  14. package/dist/config.d.ts +10 -0
  15. package/dist/config.js +284 -53
  16. package/dist/control-plane.d.ts +92 -0
  17. package/dist/control-plane.js +646 -0
  18. package/dist/drivers/hermes.d.ts +2 -0
  19. package/dist/drivers/hermes.js +10 -0
  20. package/dist/drivers/index.js +6 -0
  21. package/dist/events.js +13 -2
  22. package/dist/index.d.ts +6 -0
  23. package/dist/index.js +15 -3
  24. package/dist/run-store.d.ts +49 -0
  25. package/dist/run-store.js +291 -0
  26. package/dist/server.d.ts +3 -2
  27. package/dist/server.js +913 -41
  28. package/dist/session-contract.d.ts +16 -4
  29. package/dist/session.d.ts +74 -8
  30. package/dist/session.js +433 -38
  31. package/dist/types.d.ts +204 -6
  32. package/dist/types.js +2 -1
  33. package/dist/webui/app.d.ts +1 -0
  34. package/dist/webui/app.js +1074 -0
  35. package/dist/webui/icons.d.ts +9 -0
  36. package/dist/webui/icons.js +10 -0
  37. package/dist/webui/index.d.ts +3 -0
  38. package/dist/webui/index.js +32 -0
  39. package/dist/webui/markup.d.ts +1 -0
  40. package/dist/webui/markup.js +68 -0
  41. package/dist/webui/styles.d.ts +1 -0
  42. package/dist/webui/styles.js +269 -0
  43. package/dist/workspace-files.d.ts +48 -0
  44. package/dist/workspace-files.js +269 -0
  45. package/nexus-agentd.example.json +31 -27
  46. package/package.json +6 -6
package/dist/events.js CHANGED
@@ -20,8 +20,19 @@ export class SessionEventLog {
20
20
  if (this.events.length > this.maxEvents) {
21
21
  this.events.splice(0, this.events.length - this.maxEvents);
22
22
  }
23
- for (const listener of this.listeners)
24
- listener(structuredClone(event));
23
+ for (const listener of this.listeners) {
24
+ try {
25
+ listener(structuredClone(event));
26
+ }
27
+ catch (error) {
28
+ console.error(JSON.stringify({
29
+ level: 'error',
30
+ event: 'session_event_listener_failed',
31
+ sessionId: this.sessionId,
32
+ message: error instanceof Error ? error.message : String(error)
33
+ }));
34
+ }
35
+ }
25
36
  return event;
26
37
  }
27
38
  after(id) {
package/dist/index.d.ts CHANGED
@@ -1,13 +1,19 @@
1
1
  import type { Server } from 'node:http';
2
+ import { AgentdControlPlane } from './control-plane.js';
3
+ import { RunStore } from './run-store.js';
2
4
  import { SessionManager } from './session.js';
3
5
  export declare function startAgentd(configPath: string): Promise<{
4
6
  config: import("./types.js").AgentdConfig;
5
7
  server: Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
6
8
  sessions: SessionManager;
9
+ runStore: RunStore;
10
+ controlPlane: AgentdControlPlane;
7
11
  close(): Promise<void>;
8
12
  }>;
9
13
  export * from './config.js';
14
+ export * from './control-plane.js';
10
15
  export * from './server.js';
16
+ export * from './run-store.js';
11
17
  export * from './session.js';
12
18
  export * from './types.js';
13
19
  export * from './workspace.js';
package/dist/index.js CHANGED
@@ -1,22 +1,32 @@
1
+ import path from 'node:path';
1
2
  import { loadAgentdConfig } from './config.js';
3
+ import { AgentdControlPlane } from './control-plane.js';
2
4
  import { createDriverRegistry } from './drivers/index.js';
3
5
  import { createAgentdServer } from './server.js';
6
+ import { RunStore, runStorePathForConfig } from './run-store.js';
4
7
  import { SessionManager } from './session.js';
5
8
  import { WorkspacePolicy } from './workspace.js';
6
9
  export async function startAgentd(configPath) {
7
- const config = await loadAgentdConfig(configPath);
10
+ const absoluteConfigPath = path.resolve(configPath);
11
+ const config = await loadAgentdConfig(absoluteConfigPath);
12
+ const runStore = new RunStore(runStorePathForConfig(absoluteConfigPath), 1000, config.maxRequestBytes);
13
+ await runStore.init();
8
14
  const workspacePolicy = await WorkspacePolicy.create(config.workspaceRoots);
9
- const sessions = new SessionManager(config, workspacePolicy, createDriverRegistry(config));
15
+ const sessions = new SessionManager(config, workspacePolicy, createDriverRegistry(config), runStore);
10
16
  sessions.startCleanup();
11
- const server = createAgentdServer(config, sessions);
17
+ const controlPlane = new AgentdControlPlane(absoluteConfigPath, config, sessions);
18
+ const server = createAgentdServer(config, sessions, controlPlane);
12
19
  await listen(server, config.listen.port, config.listen.host);
13
20
  return {
14
21
  config,
15
22
  server,
16
23
  sessions,
24
+ runStore,
25
+ controlPlane,
17
26
  async close() {
18
27
  await closeServer(server);
19
28
  await sessions.shutdown();
29
+ await runStore.flush();
20
30
  }
21
31
  };
22
32
  }
@@ -42,7 +52,9 @@ function closeServer(server) {
42
52
  });
43
53
  }
44
54
  export * from './config.js';
55
+ export * from './control-plane.js';
45
56
  export * from './server.js';
57
+ export * from './run-store.js';
46
58
  export * from './session.js';
47
59
  export * from './types.js';
48
60
  export * from './workspace.js';
@@ -0,0 +1,49 @@
1
+ import type { AgentdProtocol, AgentdRunDetail, AgentdRunView, AgentdSessionState } from './types.js';
2
+ interface StoredRun extends AgentdRunDetail {
3
+ ownerKeyId: string;
4
+ }
5
+ export interface CreateRunInput {
6
+ sessionId: string;
7
+ agentId: string;
8
+ agentName: string;
9
+ protocol: AgentdProtocol;
10
+ workspace?: string;
11
+ protocolSessionId?: string;
12
+ ownerKeyId: string;
13
+ task: string;
14
+ inputAttachmentCount?: number;
15
+ }
16
+ export interface RunListQuery {
17
+ agentId?: string;
18
+ sessionId?: string;
19
+ state?: AgentdSessionState;
20
+ query?: string;
21
+ limit?: number;
22
+ }
23
+ export declare class RunStore {
24
+ readonly filePath: string;
25
+ private readonly maxRuns;
26
+ private readonly maxTaskChars;
27
+ private readonly runs;
28
+ private initialized;
29
+ private dirty;
30
+ private persistTimer?;
31
+ private writeQueue;
32
+ constructor(filePath: string, maxRuns?: number, maxTaskChars?: number);
33
+ init(): Promise<void>;
34
+ create(input: CreateRunInput): AgentdRunDetail;
35
+ update(id: string, patch: Partial<Pick<StoredRun, 'protocolSessionId' | 'state' | 'progress' | 'output' | 'error' | 'artifacts' | 'artifactCount' | 'updatedAt' | 'endedAt' | 'durationMs' | 'inputAttachmentCount'>>): AgentdRunDetail | undefined;
36
+ get(id: string): AgentdRunDetail | undefined;
37
+ list(query?: RunListQuery): {
38
+ runs: AgentdRunView[];
39
+ total: number;
40
+ };
41
+ flush(): Promise<void>;
42
+ private load;
43
+ private changed;
44
+ private prune;
45
+ private persist;
46
+ private assertInitialized;
47
+ }
48
+ export declare function runStorePathForConfig(configPath: string): string;
49
+ export {};
@@ -0,0 +1,291 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ const STATES = new Set([
5
+ 'created',
6
+ 'running',
7
+ 'input_required',
8
+ 'permission_required',
9
+ 'completed',
10
+ 'failed',
11
+ 'canceled'
12
+ ]);
13
+ const MAX_OUTPUT_CHARS = 256 * 1024;
14
+ const MAX_SUMMARY_CHARS = 600;
15
+ export class RunStore {
16
+ filePath;
17
+ maxRuns;
18
+ maxTaskChars;
19
+ runs = new Map();
20
+ initialized = false;
21
+ dirty = false;
22
+ persistTimer;
23
+ writeQueue = Promise.resolve();
24
+ constructor(filePath, maxRuns = 1000, maxTaskChars = 1024 * 1024) {
25
+ this.filePath = filePath;
26
+ this.maxRuns = maxRuns;
27
+ this.maxTaskChars = maxTaskChars;
28
+ }
29
+ async init() {
30
+ if (this.initialized)
31
+ return;
32
+ await mkdir(path.dirname(this.filePath), { recursive: true });
33
+ try {
34
+ const raw = await readFile(this.filePath, 'utf8');
35
+ this.load(raw);
36
+ }
37
+ catch (error) {
38
+ if (error.code !== 'ENOENT') {
39
+ if (error instanceof SyntaxError || error instanceof InvalidRunFileError) {
40
+ const backup = `${this.filePath}.corrupt-${Date.now()}`;
41
+ await rename(this.filePath, backup);
42
+ console.error(JSON.stringify({
43
+ level: 'error',
44
+ event: 'run_history_recovered',
45
+ file: this.filePath,
46
+ backup,
47
+ message: error.message
48
+ }));
49
+ }
50
+ else {
51
+ throw error;
52
+ }
53
+ }
54
+ }
55
+ this.initialized = true;
56
+ const now = Date.now();
57
+ for (const [id, run] of this.runs) {
58
+ if (!isActive(run.state))
59
+ continue;
60
+ this.runs.set(id, {
61
+ ...run,
62
+ state: 'failed',
63
+ error: 'Gateway restarted before the task finished.',
64
+ progress: { phase: '已中断', message: '网关重启前任务尚未结束' },
65
+ updatedAt: now,
66
+ endedAt: now,
67
+ durationMs: Math.max(0, now - run.startedAt)
68
+ });
69
+ this.dirty = true;
70
+ }
71
+ this.prune();
72
+ if (!(await fileExists(this.filePath)))
73
+ this.dirty = true;
74
+ if (this.dirty)
75
+ await this.flush();
76
+ }
77
+ create(input) {
78
+ this.assertInitialized();
79
+ const now = Date.now();
80
+ const taskTruncated = input.task.length > this.maxTaskChars;
81
+ const run = {
82
+ id: randomUUID(),
83
+ sessionId: input.sessionId,
84
+ agentId: input.agentId,
85
+ agentName: input.agentName,
86
+ protocol: input.protocol,
87
+ workspace: input.workspace,
88
+ protocolSessionId: input.protocolSessionId,
89
+ ownerKeyId: input.ownerKeyId,
90
+ task: taskTruncated ? input.task.slice(0, this.maxTaskChars) : input.task,
91
+ taskTruncated: taskTruncated || undefined,
92
+ inputAttachmentCount: input.inputAttachmentCount || undefined,
93
+ state: 'running',
94
+ progress: { phase: '已接收', message: '任务已交给智能体' },
95
+ artifacts: [],
96
+ artifactCount: 0,
97
+ startedAt: now,
98
+ updatedAt: now
99
+ };
100
+ this.runs.set(run.id, run);
101
+ this.changed();
102
+ return publicDetail(run);
103
+ }
104
+ update(id, patch) {
105
+ this.assertInitialized();
106
+ const current = this.runs.get(id);
107
+ if (!current)
108
+ return undefined;
109
+ const output = patch.output === undefined
110
+ ? current.output
111
+ : clipTail(patch.output, MAX_OUTPUT_CHARS);
112
+ const updatedAt = patch.updatedAt ?? Date.now();
113
+ const next = {
114
+ ...current,
115
+ ...structuredClone(patch),
116
+ output,
117
+ resultSummary: output ? summarize(output) : current.resultSummary,
118
+ updatedAt
119
+ };
120
+ if (patch.endedAt !== undefined) {
121
+ next.durationMs = Math.max(0, patch.endedAt - next.startedAt);
122
+ }
123
+ this.runs.delete(id);
124
+ this.runs.set(id, next);
125
+ this.changed();
126
+ return publicDetail(next);
127
+ }
128
+ get(id) {
129
+ this.assertInitialized();
130
+ const run = this.runs.get(id);
131
+ return run ? publicDetail(run) : undefined;
132
+ }
133
+ list(query = {}) {
134
+ this.assertInitialized();
135
+ const needle = String(query.query || '').trim().toLowerCase();
136
+ const limit = Math.min(200, Math.max(1, Number(query.limit) || 50));
137
+ const matched = Array.from(this.runs.values())
138
+ .reverse()
139
+ .filter((run) => !query.agentId || run.agentId === query.agentId)
140
+ .filter((run) => !query.sessionId || run.sessionId === query.sessionId)
141
+ .filter((run) => !query.state || run.state === query.state)
142
+ .filter((run) => !needle ||
143
+ run.task.toLowerCase().includes(needle) ||
144
+ run.agentName.toLowerCase().includes(needle) ||
145
+ run.id.toLowerCase().includes(needle));
146
+ const total = matched.length;
147
+ const runs = matched.slice(0, limit).map(publicView);
148
+ return { runs, total };
149
+ }
150
+ async flush() {
151
+ this.assertInitialized();
152
+ if (this.persistTimer)
153
+ clearTimeout(this.persistTimer);
154
+ this.persistTimer = undefined;
155
+ if (this.dirty)
156
+ await this.persist();
157
+ await this.writeQueue;
158
+ }
159
+ load(raw) {
160
+ const parsed = JSON.parse(raw);
161
+ if (!parsed ||
162
+ typeof parsed !== 'object' ||
163
+ Array.isArray(parsed) ||
164
+ parsed.schemaVersion !== 1 ||
165
+ !Array.isArray(parsed.runs)) {
166
+ throw new InvalidRunFileError('Invalid Agent Nexus run history file');
167
+ }
168
+ for (const value of parsed.runs) {
169
+ const run = normalizeRun(value);
170
+ if (run)
171
+ this.runs.set(run.id, run);
172
+ }
173
+ }
174
+ changed() {
175
+ this.dirty = true;
176
+ this.prune();
177
+ if (this.persistTimer)
178
+ return;
179
+ this.persistTimer = setTimeout(() => {
180
+ this.persistTimer = undefined;
181
+ void this.persist().catch((error) => {
182
+ console.error(JSON.stringify({
183
+ level: 'error',
184
+ event: 'run_history_write_failed',
185
+ message: error instanceof Error ? error.message : String(error)
186
+ }));
187
+ });
188
+ }, 250);
189
+ this.persistTimer.unref?.();
190
+ }
191
+ prune() {
192
+ while (this.runs.size > this.maxRuns) {
193
+ const terminal = Array.from(this.runs.entries()).find(([, run]) => !isActive(run.state));
194
+ if (!terminal)
195
+ break;
196
+ this.runs.delete(terminal[0]);
197
+ this.dirty = true;
198
+ }
199
+ }
200
+ persist() {
201
+ if (!this.dirty)
202
+ return this.writeQueue;
203
+ this.dirty = false;
204
+ const payload = {
205
+ schemaVersion: 1,
206
+ runs: Array.from(this.runs.values()).map((run) => structuredClone(run))
207
+ };
208
+ const serialized = `${JSON.stringify(payload, null, 2)}\n`;
209
+ const write = async () => {
210
+ const temp = `${this.filePath}.${process.pid}.${randomUUID()}.tmp`;
211
+ await writeFile(temp, serialized, { encoding: 'utf8', mode: 0o600 });
212
+ await rename(temp, this.filePath);
213
+ await chmod(this.filePath, 0o600).catch(() => undefined);
214
+ };
215
+ const next = this.writeQueue.then(write, write).catch((error) => {
216
+ this.dirty = true;
217
+ throw error;
218
+ });
219
+ this.writeQueue = next.catch(() => undefined);
220
+ return next;
221
+ }
222
+ assertInitialized() {
223
+ if (!this.initialized)
224
+ throw new Error('RunStore is not initialized');
225
+ }
226
+ }
227
+ export function runStorePathForConfig(configPath) {
228
+ const absolute = path.resolve(configPath);
229
+ const extension = path.extname(absolute);
230
+ const basename = path.basename(absolute, extension);
231
+ return path.join(path.dirname(absolute), `${basename}-runs.json`);
232
+ }
233
+ function normalizeRun(value) {
234
+ if (!value || typeof value !== 'object' || Array.isArray(value))
235
+ return undefined;
236
+ const run = value;
237
+ if (typeof run.id !== 'string' ||
238
+ typeof run.sessionId !== 'string' ||
239
+ typeof run.agentId !== 'string' ||
240
+ typeof run.agentName !== 'string' ||
241
+ (run.protocol !== 'acp' && run.protocol !== 'a2a') ||
242
+ typeof run.ownerKeyId !== 'string' ||
243
+ typeof run.task !== 'string' ||
244
+ !STATES.has(run.state) ||
245
+ !run.progress ||
246
+ typeof run.progress.phase !== 'string' ||
247
+ !Number.isFinite(run.startedAt) ||
248
+ !Number.isFinite(run.updatedAt)) {
249
+ return undefined;
250
+ }
251
+ return {
252
+ ...structuredClone(run),
253
+ artifacts: Array.isArray(run.artifacts) ? structuredClone(run.artifacts) : [],
254
+ artifactCount: Number.isFinite(run.artifactCount) ? run.artifactCount : 0
255
+ };
256
+ }
257
+ function publicView(run) {
258
+ const { ownerKeyId: _ownerKeyId, output: _output, artifacts: _artifacts, ...view } = run;
259
+ return structuredClone(view);
260
+ }
261
+ function publicDetail(run) {
262
+ const { ownerKeyId: _ownerKeyId, ...detail } = run;
263
+ return structuredClone(detail);
264
+ }
265
+ function summarize(value) {
266
+ const compact = value.replace(/\s+/g, ' ').trim();
267
+ return compact.length > MAX_SUMMARY_CHARS
268
+ ? `${compact.slice(0, MAX_SUMMARY_CHARS)}…`
269
+ : compact;
270
+ }
271
+ function clipTail(value, max) {
272
+ return value.length <= max
273
+ ? value
274
+ : `…[truncated by Agent Nexus]\n${value.slice(-max)}`;
275
+ }
276
+ function isActive(state) {
277
+ return state === 'running' || state === 'input_required' || state === 'permission_required';
278
+ }
279
+ async function fileExists(filePath) {
280
+ try {
281
+ await readFile(filePath);
282
+ return true;
283
+ }
284
+ catch (error) {
285
+ if (error.code === 'ENOENT')
286
+ return false;
287
+ throw error;
288
+ }
289
+ }
290
+ class InvalidRunFileError extends Error {
291
+ }
package/dist/server.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import http from 'node:http';
2
- import type { AgentdConfig } from './types.js';
2
+ import { type AgentdControlPlane } from './control-plane.js';
3
3
  import { SessionManager } from './session.js';
4
- export declare function createAgentdServer(config: AgentdConfig, sessions: SessionManager): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;
4
+ import type { AgentdConfig } from './types.js';
5
+ export declare function createAgentdServer(config: AgentdConfig, sessions: SessionManager, controlPlane?: AgentdControlPlane): http.Server<typeof http.IncomingMessage, typeof http.ServerResponse>;