nexus-agentd 0.2.6 → 0.3.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/LICENSE +22 -22
- package/README.md +103 -14
- package/dist/a2a/runtime.d.ts +4 -1
- package/dist/a2a/runtime.js +213 -10
- package/dist/acp/runtime.js +13 -6
- package/dist/artifact-store.d.ts +162 -0
- package/dist/artifact-store.js +1081 -0
- package/dist/cli.js +13 -14
- package/dist/config.js +45 -5
- package/dist/control-plane-validation.d.ts +54 -0
- package/dist/control-plane-validation.js +152 -0
- package/dist/control-plane.d.ts +9 -32
- package/dist/control-plane.js +41 -157
- package/dist/drivers/index.js +12 -0
- package/dist/drivers/pi.js +1 -1
- package/dist/drivers/stdio.d.ts +4 -1
- package/dist/drivers/stdio.js +135 -19
- package/dist/events.d.ts +9 -0
- package/dist/events.js +17 -4
- package/dist/index.js +10 -1
- package/dist/process-tree.js +15 -6
- package/dist/run-store.d.ts +52 -7
- package/dist/run-store.js +178 -22
- package/dist/server/auth-policy.d.ts +27 -0
- package/dist/server/auth-policy.js +143 -0
- package/dist/server/connection.d.ts +8 -0
- package/dist/server/connection.js +67 -0
- package/dist/server/diagnostics.d.ts +40 -0
- package/dist/server/diagnostics.js +130 -0
- package/dist/server/metrics.d.ts +51 -0
- package/dist/server/metrics.js +56 -0
- package/dist/server/quota.d.ts +59 -0
- package/dist/server/quota.js +176 -0
- package/dist/server/validation.d.ts +29 -0
- package/dist/server/validation.js +221 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.js +682 -418
- package/dist/session/admin-runs.d.ts +73 -0
- package/dist/session/admin-runs.js +297 -0
- package/dist/session/admission.d.ts +32 -0
- package/dist/session/admission.js +62 -0
- package/dist/session/readiness.d.ts +33 -0
- package/dist/session/readiness.js +174 -0
- package/dist/session.d.ts +58 -6
- package/dist/session.js +244 -109
- package/dist/sse-writer.d.ts +7 -0
- package/dist/sse-writer.js +60 -0
- package/dist/types.d.ts +44 -2
- package/dist/types.js +1 -0
- package/dist/webui/app/sources.js +16 -8
- package/dist/webui/index.js +11 -11
- package/dist/webui/markup.js +2 -0
- package/dist/webui/styles.js +46 -1
- package/examples/client.mjs +102 -0
- package/nexus-agentd.example.json +53 -34
- package/package.json +3 -2
package/dist/session.d.ts
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
1
1
|
import type { AgentDriver } from './drivers/index.js';
|
|
2
2
|
import { SessionEventLog } from './events.js';
|
|
3
3
|
import { RunStore, type RunListQuery } from './run-store.js';
|
|
4
|
+
import { type AdminRetryReservationFactory, type AdminRunOperationResult, type AdminRunControls } from './session/admin-runs.js';
|
|
4
5
|
import type { AcpSessionSink, AgentSessionRuntime, AgentSessionSink } from './session-contract.js';
|
|
5
6
|
import type { AgentdAgentView, AgentdArtifact, AgentdConfig, AgentdEvent, AgentdEventType, AgentdInputAttachmentView, AgentdPendingRequest, AgentdPendingResponse, AgentdProtocol, AgentdSessionState, AgentdSessionView, AgentdTurnCompletionProof } from './types.js';
|
|
6
7
|
import { type WorkspacePolicy } from './workspace.js';
|
|
8
|
+
/** Optional durable sink for full artifact snapshots. */
|
|
9
|
+
export interface SessionArtifactStore {
|
|
10
|
+
recordArtifacts(runId: string, artifacts: AgentdArtifact[]): unknown | Promise<unknown>;
|
|
11
|
+
}
|
|
12
|
+
export type SessionLifecycleEvent = {
|
|
13
|
+
type: 'session_created';
|
|
14
|
+
sessionId: string;
|
|
15
|
+
ownerKeyId: string;
|
|
16
|
+
agentId: string;
|
|
17
|
+
} | {
|
|
18
|
+
type: 'run_updated';
|
|
19
|
+
sessionId: string;
|
|
20
|
+
runId: string;
|
|
21
|
+
ownerKeyId: string;
|
|
22
|
+
state: AgentdSessionState;
|
|
23
|
+
} | {
|
|
24
|
+
type: 'session_closed';
|
|
25
|
+
sessionId: string;
|
|
26
|
+
ownerKeyId: string;
|
|
27
|
+
agentId: string;
|
|
28
|
+
};
|
|
29
|
+
export type SessionLifecycleListener = (event: SessionLifecycleEvent) => void;
|
|
7
30
|
export declare class ManagedSession implements AgentSessionSink, AcpSessionSink {
|
|
8
31
|
readonly agentId: string;
|
|
9
32
|
readonly protocol: AgentdProtocol;
|
|
@@ -27,8 +50,11 @@ export declare class ManagedSession implements AgentSessionSink, AcpSessionSink
|
|
|
27
50
|
private artifacts;
|
|
28
51
|
private inputAttachments;
|
|
29
52
|
private runtime?;
|
|
30
|
-
|
|
53
|
+
private readonly artifactStore?;
|
|
54
|
+
private readonly onRunUpdate?;
|
|
55
|
+
constructor(agentId: string, protocol: AgentdProtocol, workspace: string | undefined, ownerKeyId: string, maxEvents: number, maxOutputChars: number, agentName: string, runStore?: RunStore | undefined, instanceId?: string, artifactStore?: SessionArtifactStore, onRunUpdate?: (runId: string, state: AgentdSessionState) => void);
|
|
31
56
|
attach(runtime: AgentSessionRuntime): void;
|
|
57
|
+
runtimeAvailable(): boolean;
|
|
32
58
|
setProtocolSessionId(id: string): void;
|
|
33
59
|
setAcpSessionId(id: string): void;
|
|
34
60
|
setState(state: AgentdSessionState, error?: string): void;
|
|
@@ -43,7 +69,7 @@ export declare class ManagedSession implements AgentSessionSink, AcpSessionSink
|
|
|
43
69
|
clearPending(): void;
|
|
44
70
|
emit(type: AgentdEventType, data?: unknown): void;
|
|
45
71
|
addInputAttachment(name: string, mediaType: string | undefined, bytes: Buffer): AgentdInputAttachmentView;
|
|
46
|
-
message(message: string, attachmentIds?: string[]): Promise<void>;
|
|
72
|
+
message(message: string, attachmentIds?: string[], retryOfRunId?: string): Promise<void>;
|
|
47
73
|
resolvePending(response: AgentdPendingResponse, attachmentIds?: string[]): Promise<void>;
|
|
48
74
|
cancel(): Promise<void>;
|
|
49
75
|
dispose(): Promise<void>;
|
|
@@ -62,15 +88,21 @@ export declare class SessionManager {
|
|
|
62
88
|
private sessions;
|
|
63
89
|
private sessionLocks;
|
|
64
90
|
private cleanupTimer?;
|
|
65
|
-
private
|
|
66
|
-
|
|
91
|
+
private readonly readiness;
|
|
92
|
+
private readonly admission;
|
|
93
|
+
private readonly adminRuns;
|
|
94
|
+
private readonly artifactStore?;
|
|
95
|
+
private readonly lifecycleListeners;
|
|
96
|
+
constructor(config: AgentdConfig, workspacePolicy: WorkspacePolicy, drivers: Map<string, AgentDriver>, runStore?: RunStore | undefined, artifactStore?: SessionArtifactStore);
|
|
67
97
|
reconfigure(config: AgentdConfig, workspacePolicy: WorkspacePolicy, drivers: Map<string, AgentDriver>): void;
|
|
68
98
|
startCleanup(): void;
|
|
69
99
|
listAgents(agentIds?: Set<string>, force?: boolean): Promise<AgentdAgentView[]>;
|
|
70
100
|
create(agentId: string, workspaceInput: string | undefined, ownerKeyId: string): Promise<AgentdSessionView>;
|
|
101
|
+
private createReserved;
|
|
71
102
|
get(id: string): AgentdSessionView;
|
|
72
103
|
owns(id: string, keyId: string): boolean;
|
|
73
104
|
count(): number;
|
|
105
|
+
subscribeLifecycle(listener: SessionLifecycleListener): () => boolean;
|
|
74
106
|
listRuns(query?: RunListQuery): {
|
|
75
107
|
runs: import("./types.js").AgentdRunView[];
|
|
76
108
|
total: number;
|
|
@@ -81,7 +113,18 @@ export declare class SessionManager {
|
|
|
81
113
|
};
|
|
82
114
|
};
|
|
83
115
|
getRun(id: string): import("./types.js").AgentdRunDetail;
|
|
84
|
-
|
|
116
|
+
getRunOwnerKeyId(id: string): string | undefined;
|
|
117
|
+
ownsRun(id: string, keyId: string): boolean;
|
|
118
|
+
getRunArtifacts(id: string): import("./types.js").AgentdRunArtifactView[];
|
|
119
|
+
getStorageMetrics(): ReturnType<RunStore['metrics']> | undefined;
|
|
120
|
+
readRunArtifact(runId: string, artifactId: string): Promise<import("./artifact-store.js").ArtifactReadResult | undefined>;
|
|
121
|
+
getRunArtifact(runId: string, artifactId: string): Promise<import("./artifact-store.js").ArtifactReadResult | undefined>;
|
|
122
|
+
getRunControls(id: string): AdminRunControls;
|
|
123
|
+
cancelRun(id: string): Promise<AdminRunOperationResult>;
|
|
124
|
+
respondRun(id: string, response: AgentdPendingResponse, attachmentIds?: string[]): Promise<AdminRunOperationResult>;
|
|
125
|
+
retryRun(id: string, reserveRetry?: AdminRetryReservationFactory): Promise<AdminRunOperationResult>;
|
|
126
|
+
retryOfRunId(id: string): string | undefined;
|
|
127
|
+
message(id: string, message: string, attachmentIds?: string[], retryOfRunId?: string): Promise<AgentdSessionView>;
|
|
85
128
|
resolvePending(id: string, response: AgentdPendingResponse, attachmentIds?: string[]): Promise<AgentdSessionView>;
|
|
86
129
|
addInputAttachment(id: string, name: string, mediaType: string | undefined, bytes: Buffer): AgentdInputAttachmentView;
|
|
87
130
|
publishFile(id: string, paths: string[]): Promise<{
|
|
@@ -91,14 +134,23 @@ export declare class SessionManager {
|
|
|
91
134
|
cancel(id: string): Promise<AgentdSessionView>;
|
|
92
135
|
close(id: string): Promise<AgentdSessionView>;
|
|
93
136
|
eventsAfter(id: string, after?: string): AgentdEvent[];
|
|
137
|
+
eventReplay(id: string, after?: string): {
|
|
138
|
+
earliestId: string;
|
|
139
|
+
latestId: string;
|
|
140
|
+
reset: {
|
|
141
|
+
reason: string;
|
|
142
|
+
requestedAfter: string | null;
|
|
143
|
+
} | undefined;
|
|
144
|
+
events: AgentdEvent[];
|
|
145
|
+
};
|
|
94
146
|
subscribe(id: string, listener: (event: AgentdEvent) => void): () => boolean;
|
|
95
147
|
shutdown(): Promise<void>;
|
|
96
148
|
private withSessionLock;
|
|
97
149
|
private require;
|
|
98
|
-
private ensureCapacity;
|
|
99
150
|
private cleanup;
|
|
100
151
|
private restartCleanup;
|
|
101
152
|
private defaultWorkspace;
|
|
153
|
+
private notifyLifecycle;
|
|
102
154
|
}
|
|
103
155
|
export declare class SessionNotFoundError extends Error {
|
|
104
156
|
readonly sessionId: string;
|
package/dist/session.js
CHANGED
|
@@ -2,13 +2,15 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { realpath } from 'node:fs/promises';
|
|
3
3
|
import { extname } from 'node:path';
|
|
4
4
|
import { AcpProcessRuntime } from './acp/runtime.js';
|
|
5
|
-
import { A2AClientRuntime
|
|
5
|
+
import { A2AClientRuntime } from './a2a/runtime.js';
|
|
6
6
|
import { SessionEventLog } from './events.js';
|
|
7
|
+
import { AdminRunManager } from './session/admin-runs.js';
|
|
8
|
+
import { SessionAdmission } from './session/admission.js';
|
|
9
|
+
import { SessionReadiness } from './session/readiness.js';
|
|
7
10
|
import { ContainedReadError, readContainedFile } from './workspace.js';
|
|
8
11
|
const MAX_SESSION_ARTIFACTS = 64;
|
|
9
12
|
const MAX_ARTIFACT_BASE64_CHARS = 16 * 1024 * 1024;
|
|
10
13
|
const MAX_SESSION_BASE64_CHARS = 32 * 1024 * 1024;
|
|
11
|
-
const READINESS_CACHE_MS = 20_000;
|
|
12
14
|
const MAX_INPUT_ATTACHMENTS = 16;
|
|
13
15
|
const MAX_INPUT_ATTACHMENT_BYTES = 16 * 1024 * 1024;
|
|
14
16
|
const MAX_SESSION_INPUT_BYTES = 32 * 1024 * 1024;
|
|
@@ -36,7 +38,9 @@ export class ManagedSession {
|
|
|
36
38
|
artifacts = [];
|
|
37
39
|
inputAttachments = new Map();
|
|
38
40
|
runtime;
|
|
39
|
-
|
|
41
|
+
artifactStore;
|
|
42
|
+
onRunUpdate;
|
|
43
|
+
constructor(agentId, protocol, workspace, ownerKeyId, maxEvents, maxOutputChars, agentName, runStore, instanceId = randomUUID(), artifactStore, onRunUpdate) {
|
|
40
44
|
this.agentId = agentId;
|
|
41
45
|
this.protocol = protocol;
|
|
42
46
|
this.workspace = workspace;
|
|
@@ -45,12 +49,17 @@ export class ManagedSession {
|
|
|
45
49
|
this.agentName = agentName;
|
|
46
50
|
this.runStore = runStore;
|
|
47
51
|
this.instanceId = instanceId;
|
|
52
|
+
this.artifactStore = artifactStore || runStore;
|
|
53
|
+
this.onRunUpdate = onRunUpdate;
|
|
48
54
|
this.events = new SessionEventLog(this.id, maxEvents);
|
|
49
55
|
this.events.append('session_state', { state: this.state, protocol });
|
|
50
56
|
}
|
|
51
57
|
attach(runtime) {
|
|
52
58
|
this.runtime = runtime;
|
|
53
59
|
}
|
|
60
|
+
runtimeAvailable() {
|
|
61
|
+
return Boolean(this.runtime && this.runtime.isAvailable?.() !== false);
|
|
62
|
+
}
|
|
54
63
|
setProtocolSessionId(id) {
|
|
55
64
|
this.protocolSessionId = id;
|
|
56
65
|
this.updatedAt = Date.now();
|
|
@@ -277,7 +286,7 @@ export class ManagedSession {
|
|
|
277
286
|
this.updatedAt = Date.now();
|
|
278
287
|
return attachmentView(attachment);
|
|
279
288
|
}
|
|
280
|
-
async message(message, attachmentIds = []) {
|
|
289
|
+
async message(message, attachmentIds = [], retryOfRunId) {
|
|
281
290
|
if (!this.runtime || this.runtime.isAvailable?.() === false)
|
|
282
291
|
throw new SessionRequestError(409, 'Agent runtime is unavailable');
|
|
283
292
|
const attachments = this.inputAttachmentsFor(attachmentIds);
|
|
@@ -305,7 +314,8 @@ export class ManagedSession {
|
|
|
305
314
|
protocolSessionId: this.protocolSessionId,
|
|
306
315
|
ownerKeyId: this.ownerKeyId,
|
|
307
316
|
task: message,
|
|
308
|
-
inputAttachmentCount: attachments.length
|
|
317
|
+
inputAttachmentCount: attachments.length,
|
|
318
|
+
...(retryOfRunId ? { retryOfRunId } : {})
|
|
309
319
|
}).id;
|
|
310
320
|
this.setState('running');
|
|
311
321
|
void this.runtime.prompt(message, attachments).catch((error) => {
|
|
@@ -403,6 +413,34 @@ export class ManagedSession {
|
|
|
403
413
|
updatedAt: this.updatedAt,
|
|
404
414
|
...patch
|
|
405
415
|
});
|
|
416
|
+
if (patch.artifacts && this.artifactStore?.recordArtifacts) {
|
|
417
|
+
const runId = this.currentRunId;
|
|
418
|
+
const artifacts = structuredClone(this.artifacts);
|
|
419
|
+
void Promise.resolve()
|
|
420
|
+
.then(() => this.artifactStore.recordArtifacts(runId, artifacts))
|
|
421
|
+
.catch((error) => {
|
|
422
|
+
console.error(JSON.stringify({
|
|
423
|
+
level: 'error',
|
|
424
|
+
event: 'artifact_snapshot_record_failed',
|
|
425
|
+
runId,
|
|
426
|
+
message: errorMessage(error)
|
|
427
|
+
}));
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
try {
|
|
431
|
+
this.onRunUpdate?.(this.currentRunId, this.state);
|
|
432
|
+
}
|
|
433
|
+
catch (error) {
|
|
434
|
+
// Lifecycle observers (for example quota accounting) must never
|
|
435
|
+
// turn a successful agent event into a failed protocol turn.
|
|
436
|
+
console.error(JSON.stringify({
|
|
437
|
+
level: 'error',
|
|
438
|
+
event: 'session_lifecycle_observer_failed',
|
|
439
|
+
sessionId: this.id,
|
|
440
|
+
runId: this.currentRunId,
|
|
441
|
+
message: errorMessage(error)
|
|
442
|
+
}));
|
|
443
|
+
}
|
|
406
444
|
}
|
|
407
445
|
inputAttachmentsFor(ids) {
|
|
408
446
|
const unique = Array.from(new Set(ids));
|
|
@@ -422,18 +460,83 @@ export class SessionManager {
|
|
|
422
460
|
sessions = new Map();
|
|
423
461
|
sessionLocks = new Map();
|
|
424
462
|
cleanupTimer;
|
|
425
|
-
|
|
426
|
-
|
|
463
|
+
readiness;
|
|
464
|
+
admission;
|
|
465
|
+
adminRuns;
|
|
466
|
+
artifactStore;
|
|
467
|
+
lifecycleListeners = new Set();
|
|
468
|
+
constructor(config, workspacePolicy, drivers, runStore, artifactStore) {
|
|
427
469
|
this.config = config;
|
|
428
470
|
this.workspacePolicy = workspacePolicy;
|
|
429
471
|
this.drivers = drivers;
|
|
430
472
|
this.runStore = runStore;
|
|
473
|
+
this.artifactStore = artifactStore || runStore;
|
|
474
|
+
this.readiness = new SessionReadiness(config, drivers, {
|
|
475
|
+
requestError: (status, message) => new SessionRequestError(status, message)
|
|
476
|
+
});
|
|
477
|
+
this.admission = new SessionAdmission({
|
|
478
|
+
maxSessions: () => this.config.maxSessions || 64,
|
|
479
|
+
sessionCount: () => this.sessions.size,
|
|
480
|
+
sessions: () => this.sessions.values(),
|
|
481
|
+
isCurrent: (session) => this.sessions.get(session.id) === session,
|
|
482
|
+
withSessionLock: (id, operation) => this.withSessionLock(id, operation),
|
|
483
|
+
removeSession: (id) => {
|
|
484
|
+
const session = this.sessions.get(id);
|
|
485
|
+
if (!session || !this.sessions.delete(id))
|
|
486
|
+
return;
|
|
487
|
+
this.notifyLifecycle({
|
|
488
|
+
type: 'session_closed',
|
|
489
|
+
sessionId: session.id,
|
|
490
|
+
ownerKeyId: session.ownerKeyId,
|
|
491
|
+
agentId: session.agentId
|
|
492
|
+
});
|
|
493
|
+
},
|
|
494
|
+
requestError: (status, message) => new SessionRequestError(status, message)
|
|
495
|
+
});
|
|
496
|
+
this.adminRuns = new AdminRunManager({
|
|
497
|
+
getRun: (runId) => this.getRun(runId),
|
|
498
|
+
getSession: (sessionId) => this.sessions.get(sessionId),
|
|
499
|
+
getAgent: (agentId) => this.config.agents[agentId],
|
|
500
|
+
getOwnerKeyId: (runId) => this.getRunOwnerKeyId(runId),
|
|
501
|
+
withSessionLock: (sessionId, operation) => this.withSessionLock(sessionId, operation),
|
|
502
|
+
startRetry: async (run, ownerKeyId, reservation) => {
|
|
503
|
+
const created = await this.create(run.agentId, run.workspace, ownerKeyId);
|
|
504
|
+
try {
|
|
505
|
+
reservation?.bind(created.id);
|
|
506
|
+
await this.message(created.id, run.task, [], run.id);
|
|
507
|
+
const snapshot = this.get(created.id);
|
|
508
|
+
if (!snapshot.runId) {
|
|
509
|
+
throw new SessionRequestError(502, 'Retried task did not create a run record');
|
|
510
|
+
}
|
|
511
|
+
return { runId: snapshot.runId, sessionId: created.id };
|
|
512
|
+
}
|
|
513
|
+
catch (error) {
|
|
514
|
+
try {
|
|
515
|
+
await this.close(created.id);
|
|
516
|
+
}
|
|
517
|
+
catch (cleanupError) {
|
|
518
|
+
console.error(JSON.stringify({
|
|
519
|
+
level: 'error',
|
|
520
|
+
event: 'retry_session_cleanup_failed',
|
|
521
|
+
sessionId: created.id,
|
|
522
|
+
message: errorMessage(cleanupError)
|
|
523
|
+
}));
|
|
524
|
+
}
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
},
|
|
528
|
+
markRetryOf: (newRunId, oldRunId) => {
|
|
529
|
+
const store = this.runStore;
|
|
530
|
+
return store?.setRetryOfRunId?.(newRunId, oldRunId);
|
|
531
|
+
},
|
|
532
|
+
requestError: (status, message) => new SessionRequestError(status, message)
|
|
533
|
+
});
|
|
431
534
|
}
|
|
432
535
|
reconfigure(config, workspacePolicy, drivers) {
|
|
433
536
|
this.config = config;
|
|
434
537
|
this.workspacePolicy = workspacePolicy;
|
|
435
538
|
this.drivers = drivers;
|
|
436
|
-
this.
|
|
539
|
+
this.readiness.reconfigure(config, drivers);
|
|
437
540
|
if (this.cleanupTimer)
|
|
438
541
|
this.restartCleanup();
|
|
439
542
|
}
|
|
@@ -443,81 +546,25 @@ export class SessionManager {
|
|
|
443
546
|
this.restartCleanup();
|
|
444
547
|
}
|
|
445
548
|
async listAgents(agentIds, force = false) {
|
|
446
|
-
|
|
447
|
-
if (!force && this.readinessCache && this.readinessCache.expiresAt > now) {
|
|
448
|
-
return filterAgents(this.readinessCache.agents, agentIds);
|
|
449
|
-
}
|
|
450
|
-
const agents = await Promise.all(Object.entries(this.config.agents).map(async ([id, config]) => {
|
|
451
|
-
if (config.protocol === 'a2a')
|
|
452
|
-
return probeA2AAgent(id, config);
|
|
453
|
-
if (config.enabled === false) {
|
|
454
|
-
return {
|
|
455
|
-
id,
|
|
456
|
-
name: config.name || id,
|
|
457
|
-
description: config.description,
|
|
458
|
-
protocol: 'acp',
|
|
459
|
-
driver: config.driver,
|
|
460
|
-
ready: false,
|
|
461
|
-
enabled: false,
|
|
462
|
-
workspace: this.defaultWorkspace(id),
|
|
463
|
-
error: 'Agent is disabled',
|
|
464
|
-
checkedAt: Date.now()
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
const driver = this.drivers.get(id);
|
|
468
|
-
if (!driver) {
|
|
469
|
-
return {
|
|
470
|
-
id,
|
|
471
|
-
name: config.name || id,
|
|
472
|
-
description: config.description,
|
|
473
|
-
protocol: 'acp',
|
|
474
|
-
driver: config.driver,
|
|
475
|
-
ready: false,
|
|
476
|
-
enabled: true,
|
|
477
|
-
workspace: this.defaultWorkspace(id),
|
|
478
|
-
error: 'ACP driver is unavailable',
|
|
479
|
-
checkedAt: Date.now()
|
|
480
|
-
};
|
|
481
|
-
}
|
|
482
|
-
const startedAt = Date.now();
|
|
483
|
-
try {
|
|
484
|
-
const probe = await driver.probe();
|
|
485
|
-
return {
|
|
486
|
-
...probe,
|
|
487
|
-
protocol: 'acp',
|
|
488
|
-
driver: config.driver,
|
|
489
|
-
enabled: true,
|
|
490
|
-
workspace: this.defaultWorkspace(id),
|
|
491
|
-
checkedAt: Date.now(),
|
|
492
|
-
responseMs: Date.now() - startedAt
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
catch (error) {
|
|
496
|
-
return {
|
|
497
|
-
id,
|
|
498
|
-
name: config.name || id,
|
|
499
|
-
description: config.description,
|
|
500
|
-
protocol: 'acp',
|
|
501
|
-
driver: config.driver,
|
|
502
|
-
ready: false,
|
|
503
|
-
enabled: true,
|
|
504
|
-
workspace: this.defaultWorkspace(id),
|
|
505
|
-
error: errorMessage(error),
|
|
506
|
-
checkedAt: Date.now(),
|
|
507
|
-
responseMs: Date.now() - startedAt
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
}));
|
|
511
|
-
agents.sort((left, right) => left.name.localeCompare(right.name));
|
|
512
|
-
this.readinessCache = {
|
|
513
|
-
expiresAt: Date.now() + READINESS_CACHE_MS,
|
|
514
|
-
agents: structuredClone(agents)
|
|
515
|
-
};
|
|
516
|
-
return filterAgents(agents, agentIds);
|
|
549
|
+
return this.readiness.listAgents(agentIds, force);
|
|
517
550
|
}
|
|
518
551
|
async create(agentId, workspaceInput, ownerKeyId) {
|
|
519
|
-
|
|
520
|
-
|
|
552
|
+
if (!Object.hasOwn(this.config.agents, agentId) || this.config.agents[agentId].enabled === false) {
|
|
553
|
+
throw new SessionRequestError(404, `Configured agent not found: ${agentId}`);
|
|
554
|
+
}
|
|
555
|
+
let release = () => undefined;
|
|
556
|
+
await this.withSessionLock('admission', async () => {
|
|
557
|
+
release = await this.admission.reserve();
|
|
558
|
+
});
|
|
559
|
+
try {
|
|
560
|
+
return await this.createReserved(agentId, workspaceInput, ownerKeyId, release);
|
|
561
|
+
}
|
|
562
|
+
finally {
|
|
563
|
+
release();
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
async createReserved(agentId, workspaceInput, ownerKeyId, release) {
|
|
567
|
+
const config = Object.hasOwn(this.config.agents, agentId) ? this.config.agents[agentId] : undefined;
|
|
521
568
|
if (!config || config.enabled === false) {
|
|
522
569
|
throw new SessionRequestError(404, `Configured agent not found: ${agentId}`);
|
|
523
570
|
}
|
|
@@ -534,11 +581,27 @@ export class SessionManager {
|
|
|
534
581
|
}
|
|
535
582
|
workspace = await this.workspacePolicy.resolve(requestedWorkspace);
|
|
536
583
|
}
|
|
537
|
-
|
|
584
|
+
if (this.admission.isClosing()) {
|
|
585
|
+
throw new SessionRequestError(503, 'Gateway is shutting down');
|
|
586
|
+
}
|
|
587
|
+
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, this.artifactStore, (runId, state) => this.notifyLifecycle({
|
|
588
|
+
type: 'run_updated',
|
|
589
|
+
sessionId: session.id,
|
|
590
|
+
runId,
|
|
591
|
+
ownerKeyId: session.ownerKeyId,
|
|
592
|
+
state
|
|
593
|
+
}));
|
|
538
594
|
const runtime = config.protocol === 'a2a'
|
|
539
595
|
? new A2AClientRuntime(config, session, this.config.promptTimeoutMs || 30 * 60_000)
|
|
540
596
|
: new AcpProcessRuntime(driver, session, this.config.maxOutputChars, this.config.promptTimeoutMs || 30 * 60_000, this.workspacePolicy.listRoots());
|
|
541
597
|
this.sessions.set(session.id, session);
|
|
598
|
+
this.notifyLifecycle({
|
|
599
|
+
type: 'session_created',
|
|
600
|
+
sessionId: session.id,
|
|
601
|
+
ownerKeyId: session.ownerKeyId,
|
|
602
|
+
agentId: session.agentId
|
|
603
|
+
});
|
|
604
|
+
release();
|
|
542
605
|
session.attach(runtime);
|
|
543
606
|
try {
|
|
544
607
|
await runtime.start(workspace);
|
|
@@ -553,7 +616,14 @@ export class SessionManager {
|
|
|
553
616
|
console.error(JSON.stringify({ level: 'error', event: 'session_start_cleanup_failed', sessionId: session.id, message: errorMessage(cleanupError) }));
|
|
554
617
|
}
|
|
555
618
|
finally {
|
|
556
|
-
this.sessions.delete(session.id)
|
|
619
|
+
if (this.sessions.delete(session.id)) {
|
|
620
|
+
this.notifyLifecycle({
|
|
621
|
+
type: 'session_closed',
|
|
622
|
+
sessionId: session.id,
|
|
623
|
+
ownerKeyId: session.ownerKeyId,
|
|
624
|
+
agentId: session.agentId
|
|
625
|
+
});
|
|
626
|
+
}
|
|
557
627
|
}
|
|
558
628
|
throw new SessionRequestError(502, `Agent failed to start: ${errorMessage(error)}`);
|
|
559
629
|
}
|
|
@@ -567,6 +637,10 @@ export class SessionManager {
|
|
|
567
637
|
count() {
|
|
568
638
|
return this.sessions.size;
|
|
569
639
|
}
|
|
640
|
+
subscribeLifecycle(listener) {
|
|
641
|
+
this.lifecycleListeners.add(listener);
|
|
642
|
+
return () => this.lifecycleListeners.delete(listener);
|
|
643
|
+
}
|
|
570
644
|
listRuns(query = {}) {
|
|
571
645
|
return this.runStore?.list(query) || { runs: [], total: 0, stats: { active: 0, completed: 0, failed: 0 } };
|
|
572
646
|
}
|
|
@@ -576,13 +650,51 @@ export class SessionManager {
|
|
|
576
650
|
throw new RunNotFoundError(id);
|
|
577
651
|
return run;
|
|
578
652
|
}
|
|
579
|
-
|
|
653
|
+
getRunOwnerKeyId(id) {
|
|
654
|
+
const store = this.runStore;
|
|
655
|
+
return store?.getOwnerKeyId?.(id);
|
|
656
|
+
}
|
|
657
|
+
ownsRun(id, keyId) {
|
|
658
|
+
return this.getRunOwnerKeyId(id) === keyId;
|
|
659
|
+
}
|
|
660
|
+
getRunArtifacts(id) {
|
|
661
|
+
return this.getRun(id).artifacts;
|
|
662
|
+
}
|
|
663
|
+
getStorageMetrics() {
|
|
664
|
+
return this.runStore?.metrics();
|
|
665
|
+
}
|
|
666
|
+
async readRunArtifact(runId, artifactId) {
|
|
667
|
+
// Read through RunStore so the artifact directory remains the only
|
|
668
|
+
// place that knows how payload filenames are derived and validated.
|
|
669
|
+
this.getRun(runId);
|
|
670
|
+
const store = this.runStore;
|
|
671
|
+
return store?.readArtifact?.(runId, artifactId);
|
|
672
|
+
}
|
|
673
|
+
getRunArtifact(runId, artifactId) {
|
|
674
|
+
return this.readRunArtifact(runId, artifactId);
|
|
675
|
+
}
|
|
676
|
+
getRunControls(id) {
|
|
677
|
+
return this.adminRuns.getRunControls(id);
|
|
678
|
+
}
|
|
679
|
+
async cancelRun(id) {
|
|
680
|
+
return this.adminRuns.cancelRun(id);
|
|
681
|
+
}
|
|
682
|
+
async respondRun(id, response, attachmentIds = []) {
|
|
683
|
+
return this.adminRuns.respondRun(id, response, attachmentIds);
|
|
684
|
+
}
|
|
685
|
+
async retryRun(id, reserveRetry) {
|
|
686
|
+
return this.adminRuns.retryRun(id, reserveRetry);
|
|
687
|
+
}
|
|
688
|
+
retryOfRunId(id) {
|
|
689
|
+
return this.adminRuns.retryOfRunId(id);
|
|
690
|
+
}
|
|
691
|
+
async message(id, message, attachmentIds = [], retryOfRunId) {
|
|
580
692
|
const text = String(message || '');
|
|
581
693
|
if (!text.trim())
|
|
582
694
|
throw new SessionRequestError(400, 'message is required');
|
|
583
695
|
return this.withSessionLock(id, async () => {
|
|
584
696
|
const session = this.require(id);
|
|
585
|
-
await session.message(text, attachmentIds);
|
|
697
|
+
await session.message(text, attachmentIds, retryOfRunId);
|
|
586
698
|
return session.snapshot();
|
|
587
699
|
});
|
|
588
700
|
}
|
|
@@ -618,25 +730,47 @@ export class SessionManager {
|
|
|
618
730
|
await session.cancel();
|
|
619
731
|
const snapshot = session.snapshot();
|
|
620
732
|
await session.dispose();
|
|
621
|
-
this.sessions.delete(id)
|
|
733
|
+
if (this.sessions.delete(id)) {
|
|
734
|
+
this.notifyLifecycle({
|
|
735
|
+
type: 'session_closed',
|
|
736
|
+
sessionId: session.id,
|
|
737
|
+
ownerKeyId: session.ownerKeyId,
|
|
738
|
+
agentId: session.agentId
|
|
739
|
+
});
|
|
740
|
+
}
|
|
622
741
|
return snapshot;
|
|
623
742
|
});
|
|
624
743
|
}
|
|
625
744
|
eventsAfter(id, after) {
|
|
626
745
|
return this.require(id).events.after(after);
|
|
627
746
|
}
|
|
747
|
+
eventReplay(id, after) {
|
|
748
|
+
return this.require(id).events.replay(after);
|
|
749
|
+
}
|
|
628
750
|
subscribe(id, listener) {
|
|
629
751
|
return this.require(id).events.subscribe(listener);
|
|
630
752
|
}
|
|
631
753
|
async shutdown() {
|
|
754
|
+
this.admission.markClosing();
|
|
632
755
|
if (this.cleanupTimer)
|
|
633
756
|
clearInterval(this.cleanupTimer);
|
|
634
757
|
this.cleanupTimer = undefined;
|
|
635
|
-
await Promise.allSettled(Array.from(this.sessions.values()).map(async (session) => {
|
|
758
|
+
const results = await Promise.allSettled(Array.from(this.sessions.values()).map(async (session) => {
|
|
636
759
|
session.interruptForShutdown();
|
|
637
760
|
await session.dispose();
|
|
638
761
|
}));
|
|
762
|
+
for (const session of this.sessions.values()) {
|
|
763
|
+
this.notifyLifecycle({
|
|
764
|
+
type: 'session_closed',
|
|
765
|
+
sessionId: session.id,
|
|
766
|
+
ownerKeyId: session.ownerKeyId,
|
|
767
|
+
agentId: session.agentId
|
|
768
|
+
});
|
|
769
|
+
}
|
|
639
770
|
this.sessions.clear();
|
|
771
|
+
const failures = results.filter((result) => result.status === 'rejected');
|
|
772
|
+
if (failures.length)
|
|
773
|
+
throw new AggregateError(failures.map((result) => result.reason), 'Failed to dispose one or more Agent sessions');
|
|
640
774
|
}
|
|
641
775
|
async withSessionLock(id, operation) {
|
|
642
776
|
// Serialise mutating operations per session so that concurrent
|
|
@@ -666,22 +800,6 @@ export class SessionManager {
|
|
|
666
800
|
throw new SessionNotFoundError(id);
|
|
667
801
|
return session;
|
|
668
802
|
}
|
|
669
|
-
async ensureCapacity() {
|
|
670
|
-
const limit = this.config.maxSessions || 64;
|
|
671
|
-
if (this.sessions.size < limit)
|
|
672
|
-
return;
|
|
673
|
-
const terminal = Array.from(this.sessions.values())
|
|
674
|
-
.filter((session) => isTerminal(session.state))
|
|
675
|
-
.sort((left, right) => left.updatedAt - right.updatedAt);
|
|
676
|
-
while (this.sessions.size >= limit && terminal.length) {
|
|
677
|
-
const session = terminal.shift();
|
|
678
|
-
await session.dispose();
|
|
679
|
-
this.sessions.delete(session.id);
|
|
680
|
-
}
|
|
681
|
-
if (this.sessions.size >= limit) {
|
|
682
|
-
throw new SessionRequestError(429, 'Session capacity has been reached');
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
803
|
async cleanup() {
|
|
686
804
|
const cutoff = Date.now() - this.config.sessionTtlMs;
|
|
687
805
|
for (const [id, session] of this.sessions) {
|
|
@@ -699,7 +817,14 @@ export class SessionManager {
|
|
|
699
817
|
console.error(JSON.stringify({ level: 'error', event: 'session_cleanup_failed', sessionId: id, message: errorMessage(error) }));
|
|
700
818
|
}
|
|
701
819
|
finally {
|
|
702
|
-
this.sessions.delete(id)
|
|
820
|
+
if (this.sessions.delete(id)) {
|
|
821
|
+
this.notifyLifecycle({
|
|
822
|
+
type: 'session_closed',
|
|
823
|
+
sessionId: session.id,
|
|
824
|
+
ownerKeyId: session.ownerKeyId,
|
|
825
|
+
agentId: session.agentId
|
|
826
|
+
});
|
|
827
|
+
}
|
|
703
828
|
}
|
|
704
829
|
});
|
|
705
830
|
}
|
|
@@ -718,6 +843,21 @@ export class SessionManager {
|
|
|
718
843
|
return '';
|
|
719
844
|
return config?.workspace || this.config.workspaceRoots[0] || '';
|
|
720
845
|
}
|
|
846
|
+
notifyLifecycle(event) {
|
|
847
|
+
for (const listener of this.lifecycleListeners) {
|
|
848
|
+
try {
|
|
849
|
+
listener(structuredClone(event));
|
|
850
|
+
}
|
|
851
|
+
catch (error) {
|
|
852
|
+
console.error(JSON.stringify({
|
|
853
|
+
level: 'error',
|
|
854
|
+
event: 'session_lifecycle_listener_failed',
|
|
855
|
+
type: event.type,
|
|
856
|
+
message: errorMessage(error)
|
|
857
|
+
}));
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
}
|
|
721
861
|
}
|
|
722
862
|
function progressForState(state, error) {
|
|
723
863
|
const phases = {
|
|
@@ -849,11 +989,6 @@ export class RunNotFoundError extends Error {
|
|
|
849
989
|
this.runId = runId;
|
|
850
990
|
}
|
|
851
991
|
}
|
|
852
|
-
function filterAgents(agents, ids) {
|
|
853
|
-
return agents
|
|
854
|
-
.filter((agent) => !ids || ids.has(agent.id))
|
|
855
|
-
.map((agent) => structuredClone(agent));
|
|
856
|
-
}
|
|
857
992
|
function isTerminal(state) {
|
|
858
993
|
return state === 'completed' || state === 'failed' || state === 'canceled';
|
|
859
994
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ServerResponse } from 'node:http';
|
|
2
|
+
/** A false write already accepted the chunk. Queue later chunks until drain,
|
|
3
|
+
* with both a byte ceiling and a deadline independent of socket activity. */
|
|
4
|
+
export declare function createSseWriter(response: ServerResponse, close: () => void, maxBytes?: number, drainMs?: number): {
|
|
5
|
+
write(chunk: string): boolean;
|
|
6
|
+
stop: () => void;
|
|
7
|
+
};
|