nexus-agentd 0.2.5 → 0.2.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.
@@ -44,6 +44,8 @@ export class A2AClientRuntime {
44
44
  taskId = '';
45
45
  contextId = '';
46
46
  prompting = false;
47
+ promptFinished;
48
+ queuedResponse = false;
47
49
  disposed = false;
48
50
  activeController;
49
51
  artifactCache = new Map();
@@ -57,6 +59,8 @@ export class A2AClientRuntime {
57
59
  this.promptTimeoutMs = promptTimeoutMs;
58
60
  }
59
61
  async start() {
62
+ if (this.disposed)
63
+ throw new Error('A2A runtime is disposed');
60
64
  const resolver = createResolver(this.config);
61
65
  this.card = await resolveAgentCard(resolver, this.config);
62
66
  const fetchImpl = authenticatedFetch(this.config);
@@ -70,6 +74,8 @@ export class A2AClientRuntime {
70
74
  ]
71
75
  });
72
76
  this.client = await factory.createFromAgentCard(this.card);
77
+ if (this.disposed)
78
+ throw new Error('A2A runtime is disposed');
73
79
  this.sink.setState('created');
74
80
  }
75
81
  async prompt(message, attachments = []) {
@@ -80,6 +86,8 @@ export class A2AClientRuntime {
80
86
  if (this.disposed)
81
87
  throw new Error('A2A runtime is disposed');
82
88
  this.prompting = true;
89
+ let finish;
90
+ this.promptFinished = new Promise((resolve) => { finish = resolve; });
83
91
  this.sawTaskStatus = false;
84
92
  this.sawAgentMessage = false;
85
93
  this.completionProof = undefined;
@@ -94,9 +102,15 @@ export class A2AClientRuntime {
94
102
  for await (const response of this.client.sendMessageStream(request, {
95
103
  signal: controller.signal
96
104
  })) {
105
+ if (controller.signal.aborted || this.disposed)
106
+ break;
107
+ // An accepted answer supersedes the old input-required stream.
108
+ // Its trailing status must not reopen the answered request.
109
+ if (this.queuedResponse)
110
+ continue;
97
111
  this.processResponse(response);
98
112
  }
99
- if (this.sink.state === 'running') {
113
+ if (this.sink.state === 'running' && !this.queuedResponse) {
100
114
  if (this.completionProof) {
101
115
  this.sink.completeTurn(this.completionProof);
102
116
  }
@@ -114,7 +128,7 @@ export class A2AClientRuntime {
114
128
  }
115
129
  }
116
130
  catch (error) {
117
- if (this.sink.state !== 'canceled') {
131
+ if (this.sink.state !== 'canceled' && !this.queuedResponse) {
118
132
  this.sink.setState('failed', abortMessage(error, controller.signal));
119
133
  }
120
134
  }
@@ -123,9 +137,13 @@ export class A2AClientRuntime {
123
137
  if (this.activeController === controller)
124
138
  this.activeController = undefined;
125
139
  this.prompting = false;
140
+ finish();
141
+ this.promptFinished = undefined;
126
142
  }
127
143
  }
128
144
  async respondPending(response, attachments = []) {
145
+ if (!this.isAvailable())
146
+ throw new Error('A2A runtime is unavailable');
129
147
  if (this.sink.state !== 'input_required') {
130
148
  throw new Error('A2A session is not waiting for input');
131
149
  }
@@ -138,10 +156,23 @@ export class A2AClientRuntime {
138
156
  : response.message ?? response.optionId ?? '';
139
157
  if (!message.trim())
140
158
  throw new Error('A2A pending response is empty');
159
+ const previous = this.promptFinished || Promise.resolve();
160
+ this.queuedResponse = true;
141
161
  this.sink.clearPending();
142
- await this.prompt(message, attachments);
162
+ this.sink.setState('running');
163
+ void previous.then(async () => {
164
+ this.queuedResponse = false;
165
+ if (this.disposed || this.sink.state === 'canceled')
166
+ return;
167
+ await this.prompt(message, attachments);
168
+ }).catch((error) => {
169
+ if (!this.disposed && this.sink.state !== 'canceled')
170
+ this.sink.setState('failed', errorMessage(error));
171
+ });
143
172
  }
144
173
  async cancel() {
174
+ this.sink.clearPending();
175
+ this.sink.setState('canceled');
145
176
  this.activeController?.abort(new Error('A2A session canceled'));
146
177
  if (this.client && this.taskId) {
147
178
  try {
@@ -162,6 +193,9 @@ export class A2AClientRuntime {
162
193
  this.activeController?.abort(new Error('A2A runtime disposed'));
163
194
  this.activeController = undefined;
164
195
  }
196
+ isAvailable() {
197
+ return !this.disposed && Boolean(this.client && this.card);
198
+ }
165
199
  messageRequest(text, attachments) {
166
200
  return {
167
201
  tenant: '',
@@ -338,12 +372,43 @@ function createResolver(config) {
338
372
  legacyCompat: { enabled: true }
339
373
  });
340
374
  }
341
- function resolveAgentCard(resolver, config) {
342
- if (config.agentCardUrl)
343
- return resolver.resolve(config.agentCardUrl, '');
344
- if (config.agentUrl)
345
- return resolver.resolve(config.agentUrl);
346
- throw new Error('A2A Agent Card URL is required');
375
+ async function resolveAgentCard(resolver, config) {
376
+ const origin = configuredOrigin(config);
377
+ const card = config.agentCardUrl
378
+ ? await resolver.resolve(config.agentCardUrl, '')
379
+ : await resolver.resolve(config.agentUrl);
380
+ // The resolver normalizes v0.3 url/additionalInterfaces into this array.
381
+ // Validate every advertised endpoint before transport selection or probing.
382
+ if (!Array.isArray(card.supportedInterfaces) || !card.supportedInterfaces.length) {
383
+ throw new Error('A2A Agent Card must declare at least one interface URL');
384
+ }
385
+ for (const endpoint of card.supportedInterfaces)
386
+ assertSameOrigin(endpoint?.url, origin);
387
+ return card;
388
+ }
389
+ function a2aUrl(value) {
390
+ let url;
391
+ try {
392
+ url = new URL(value);
393
+ }
394
+ catch {
395
+ throw new Error('A2A URL is invalid');
396
+ }
397
+ if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password || url.hash) {
398
+ throw new Error('A2A URL must use http(s) without credentials or a fragment');
399
+ }
400
+ return url;
401
+ }
402
+ function configuredOrigin(config) {
403
+ const value = config.agentCardUrl || config.agentUrl;
404
+ if (!value)
405
+ throw new Error('A2A Agent Card URL is required');
406
+ return a2aUrl(value).origin;
407
+ }
408
+ function assertSameOrigin(value, origin) {
409
+ if (a2aUrl(value).origin !== origin) {
410
+ throw new Error('A2A URL must have the same origin as the configured Agent Card URL');
411
+ }
347
412
  }
348
413
  function preferredTransports(config) {
349
414
  if (config.preferredTransport === 'jsonrpc')
@@ -353,8 +418,12 @@ function preferredTransports(config) {
353
418
  return undefined;
354
419
  }
355
420
  function authenticatedFetch(config) {
421
+ const origin = configuredOrigin(config);
356
422
  return async (input, init = {}) => {
357
- const headers = new Headers(init.headers);
423
+ // The final guard also covers SDK-derived paths and future card flows.
424
+ // Check before attaching credentials; never let fetch follow redirects.
425
+ assertSameOrigin(input instanceof Request ? input.url : String(input), origin);
426
+ const headers = new Headers(init.headers ?? (input instanceof Request ? input.headers : undefined));
358
427
  if (config.auth?.type === 'bearer' && config.auth.value) {
359
428
  headers.set('Authorization', `Bearer ${config.auth.value}`);
360
429
  }
@@ -366,6 +435,7 @@ function authenticatedFetch(config) {
366
435
  return fetch(input, {
367
436
  ...init,
368
437
  headers,
438
+ redirect: 'error',
369
439
  signal: init.signal || AbortSignal.timeout(config.timeoutMs || 60_000)
370
440
  });
371
441
  };
@@ -4,6 +4,8 @@ import type { AgentdInputAttachment, AgentdPendingResponse } from '../types.js';
4
4
  export declare class AcpProcessRuntime {
5
5
  private readonly driver;
6
6
  private readonly sink;
7
+ private readonly allowedRoots;
8
+ private readonly startupTimeoutMs;
7
9
  private process?;
8
10
  private connection?;
9
11
  private pending?;
@@ -16,12 +18,14 @@ export declare class AcpProcessRuntime {
16
18
  private inputDirectory?;
17
19
  private workspace?;
18
20
  private promptOutput;
19
- constructor(driver: AgentDriver, sink: AcpSessionSink, maxStderrChunkChars?: number, promptTimeoutMs?: number);
21
+ constructor(driver: AgentDriver, sink: AcpSessionSink, maxStderrChunkChars?: number, promptTimeoutMs?: number, allowedRoots?: readonly string[], startupTimeoutMs?: number);
20
22
  start(workspace: string): Promise<void>;
23
+ private connect;
21
24
  prompt(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
22
25
  respondPending(input: AgentdPendingResponse | string, _attachments?: AgentdInputAttachment[]): Promise<void>;
23
26
  cancel(): Promise<void>;
24
27
  dispose(): Promise<void>;
28
+ isAvailable(): boolean;
25
29
  private requestPermission;
26
30
  private finishPermission;
27
31
  private requestInput;
@@ -1,15 +1,21 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { mkdir, readFile, realpath, rm, stat, writeFile } from 'node:fs/promises';
2
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { Readable, Writable } from 'node:stream';
5
5
  import { fileURLToPath, pathToFileURL } from 'node:url';
6
6
  import * as acp from '@agentclientprotocol/sdk';
7
7
  import { terminateProcessTree } from '../process-tree.js';
8
8
  import { TURN_COMPLETION_CONTRACT } from '../completion-contract.js';
9
- const MAX_MEDIA_MARKER_FILE_BYTES = 32 * 1024 * 1024;
9
+ import { readContainedFile } from '../workspace.js';
10
+ // Matches MAX_PUBLISHED_FILE_BYTES in session.ts; larger files are encoded only to be dropped by the base64 budget.
11
+ const MAX_MEDIA_MARKER_FILE_BYTES = 12 * 1024 * 1024;
12
+ // The agent controls how many markers it emits, so bound the reads per turn.
13
+ const MAX_MEDIA_MARKER_ARTIFACTS = 8;
10
14
  export class AcpProcessRuntime {
11
15
  driver;
12
16
  sink;
17
+ allowedRoots;
18
+ startupTimeoutMs;
13
19
  process;
14
20
  connection;
15
21
  pending;
@@ -22,9 +28,11 @@ export class AcpProcessRuntime {
22
28
  inputDirectory;
23
29
  workspace;
24
30
  promptOutput = '';
25
- constructor(driver, sink, maxStderrChunkChars = 16 * 1024, promptTimeoutMs = 30 * 60 * 1000) {
31
+ constructor(driver, sink, maxStderrChunkChars = 16 * 1024, promptTimeoutMs = 30 * 60 * 1000, allowedRoots = [], startupTimeoutMs = 30_000) {
26
32
  this.driver = driver;
27
33
  this.sink = sink;
34
+ this.allowedRoots = allowedRoots;
35
+ this.startupTimeoutMs = startupTimeoutMs;
28
36
  const limit = Number.isFinite(maxStderrChunkChars)
29
37
  ? Math.floor(maxStderrChunkChars)
30
38
  : 16 * 1024;
@@ -32,6 +40,21 @@ export class AcpProcessRuntime {
32
40
  this.promptTimeoutMs = Math.max(10_000, Math.trunc(promptTimeoutMs));
33
41
  }
34
42
  async start(workspace) {
43
+ if (this.disposed)
44
+ throw new Error('ACP runtime is disposed');
45
+ if (this.process)
46
+ throw new Error('ACP runtime is already started');
47
+ try {
48
+ await withTimeout(this.connect(workspace), this.startupTimeoutMs, 'ACP startup timed out');
49
+ }
50
+ catch (error) {
51
+ if (this.sink.state !== 'canceled')
52
+ this.onProcessFailure(error);
53
+ await this.dispose();
54
+ throw error;
55
+ }
56
+ }
57
+ async connect(workspace) {
35
58
  if (this.process)
36
59
  throw new Error('ACP runtime is already started');
37
60
  this.workspace = workspace;
@@ -69,6 +92,8 @@ export class AcpProcessRuntime {
69
92
  version: '0.2.2'
70
93
  }
71
94
  });
95
+ if (this.disposed)
96
+ throw new Error('ACP runtime is disposed');
72
97
  if (initialize.protocolVersion !== acp.PROTOCOL_VERSION) {
73
98
  this.sink.emit('terminal_output', {
74
99
  stream: 'system',
@@ -80,10 +105,14 @@ export class AcpProcessRuntime {
80
105
  cwd: workspace,
81
106
  mcpServers: []
82
107
  });
108
+ if (this.disposed)
109
+ throw new Error('ACP runtime is disposed');
83
110
  this.sink.setAcpSessionId(String(session.sessionId));
84
111
  this.sink.setState('created');
85
112
  }
86
113
  async prompt(message, attachments = []) {
114
+ if (this.disposed)
115
+ throw new Error('ACP runtime is disposed');
87
116
  if (!this.connection)
88
117
  throw new Error('ACP runtime is not connected');
89
118
  if (this.prompting)
@@ -199,11 +228,12 @@ export class AcpProcessRuntime {
199
228
  if (this.pendingInput) {
200
229
  this.finishInput('cancel');
201
230
  }
202
- if (this.connection && this.sink.state !== 'canceled') {
231
+ this.sink.setState('canceled');
232
+ if (this.connection) {
203
233
  try {
204
- await this.connection.agent.notify(acp.methods.agent.session.cancel, {
234
+ await withTimeout(this.connection.agent.notify(acp.methods.agent.session.cancel, {
205
235
  sessionId: this.requireSessionId()
206
- });
236
+ }), 2_000, 'ACP cancellation notification timed out');
207
237
  }
208
238
  catch (error) {
209
239
  this.sink.emit('terminal_output', {
@@ -229,8 +259,16 @@ export class AcpProcessRuntime {
229
259
  this.pendingInput.resolve({ action: 'cancel' });
230
260
  this.pendingInput = undefined;
231
261
  }
232
- this.connection?.close();
262
+ const connection = this.connection;
233
263
  this.connection = undefined;
264
+ try {
265
+ connection?.close();
266
+ }
267
+ catch (error) {
268
+ this.sink.emit('terminal_output', {
269
+ stream: 'system', text: `ACP connection close failed: ${error instanceof Error ? error.message : String(error)}`
270
+ });
271
+ }
234
272
  const child = this.process;
235
273
  this.process = undefined;
236
274
  if (child) {
@@ -243,6 +281,10 @@ export class AcpProcessRuntime {
243
281
  this.inputDirectory = undefined;
244
282
  }
245
283
  }
284
+ isAvailable() {
285
+ return !this.disposed && Boolean(this.connection) &&
286
+ (!this.process || (this.process.exitCode === null && this.process.signalCode === null));
287
+ }
246
288
  requestPermission(params) {
247
289
  if (this.driver.permissionPolicy === 'deny') {
248
290
  const reject = params.options.find((option) => option.kind.startsWith('reject'));
@@ -428,13 +470,23 @@ export class AcpProcessRuntime {
428
470
  if (!markers.length)
429
471
  return;
430
472
  this.sink.replaceOutput(withoutMediaMarkers(this.promptOutput));
431
- const seen = new Set();
432
- for (const marker of markers) {
433
- if (seen.has(marker))
434
- continue;
435
- seen.add(marker);
473
+ const roots = this.allowedRoots.length
474
+ ? this.allowedRoots
475
+ : this.workspace
476
+ ? [this.workspace]
477
+ : [];
478
+ const unique = Array.from(new Set(markers));
479
+ const accepted = unique.slice(0, MAX_MEDIA_MARKER_ARTIFACTS);
480
+ const ignored = unique.length - accepted.length;
481
+ if (ignored > 0) {
482
+ this.sink.emit('terminal_output', {
483
+ stream: 'system',
484
+ text: `Ignored ${ignored} MEDIA marker(s); at most ${MAX_MEDIA_MARKER_ARTIFACTS} artifacts can be attached per turn.`
485
+ });
486
+ }
487
+ for (const marker of accepted) {
436
488
  try {
437
- const artifact = await artifactFromMediaMarker(marker, `media:${randomUUID()}`, this.workspace);
489
+ const artifact = await artifactFromMediaMarker(marker, `media:${randomUUID()}`, roots);
438
490
  this.sink.addArtifact(artifact);
439
491
  this.sink.emit('artifact', artifact);
440
492
  }
@@ -679,7 +731,7 @@ function withoutMediaMarkers(output) {
679
731
  .join('\n')
680
732
  .trim();
681
733
  }
682
- async function artifactFromMediaMarker(marker, id, workspace) {
734
+ async function artifactFromMediaMarker(marker, id, roots) {
683
735
  if (/^https?:\/\//i.test(marker)) {
684
736
  const filename = artifactFilename(marker, undefined, 'resource', undefined);
685
737
  return {
@@ -690,29 +742,19 @@ async function artifactFromMediaMarker(marker, id, workspace) {
690
742
  metadata: { source: 'acp_media_marker' }
691
743
  };
692
744
  }
693
- const localPath = marker.startsWith('file:')
694
- ? fileURLToPath(marker)
695
- : marker;
696
- const resolved = await realpath(path.isAbsolute(localPath)
697
- ? localPath
698
- : path.resolve(workspace || process.cwd(), localPath));
699
- const details = await stat(resolved);
700
- if (!details.isFile()) {
701
- throw new Error('declared path is not a regular file');
702
- }
703
- if (details.size > MAX_MEDIA_MARKER_FILE_BYTES) {
704
- throw new Error(`declared file exceeds the ${MAX_MEDIA_MARKER_FILE_BYTES} byte limit`);
705
- }
706
- const filename = path.basename(resolved);
745
+ const localPath = marker.startsWith('file:') ? fileURLToPath(marker) : marker;
746
+ const file = await readContainedFile(roots, localPath, {
747
+ maxBytes: MAX_MEDIA_MARKER_FILE_BYTES
748
+ });
707
749
  return {
708
750
  id,
709
- name: filename,
710
- filename,
711
- mediaType: mediaTypeForPath(filename),
712
- bytesBase64: (await readFile(resolved)).toString('base64'),
751
+ name: file.filename,
752
+ filename: file.filename,
753
+ mediaType: mediaTypeForPath(file.filename),
754
+ bytesBase64: file.bytes.toString('base64'),
713
755
  metadata: {
714
756
  source: 'acp_media_marker',
715
- size: details.size
757
+ size: file.size
716
758
  }
717
759
  };
718
760
  }
@@ -4,6 +4,6 @@ export const TURN_COMPLETION_CONTRACT = [
4
4
  'If you need user input or permission, use the protocol input/permission request and wait for the response.',
5
5
  'Before ending the turn, verify the requested work and provide a non-empty final response.',
6
6
  'The final response must summarize the outcome, verification performed, and workspace-relative paths for generated files.',
7
- 'Attach generated resources or artifacts through the protocol when supported. If the runtime only reports a local deliverable path, put each requested file on its own final line as MEDIA:<path>; Agent Nexus will snapshot that explicitly declared file as an artifact before the turn completes.',
7
+ 'Attach generated resources or artifacts through the protocol when supported. If the runtime only reports a local deliverable path, put each requested file on its own final line as MEDIA:<path>, where <path> stays inside the configured workspace roots; Agent Nexus snapshots that declared file as an artifact before the turn completes, refuses paths outside those roots, and attaches at most 8 files per turn.',
8
8
  '</agent_nexus_completion_contract>'
9
9
  ].join('\n');
@@ -188,7 +188,17 @@ export class AgentdControlPlane {
188
188
  if (!workspaceRoots.length) {
189
189
  throw new ControlPlaneError(400, 'workspaceRoots must contain at least one path');
190
190
  }
191
- await WorkspacePolicy.create(workspaceRoots);
191
+ const policy = await WorkspacePolicy.create(workspaceRoots);
192
+ for (const [id, agent] of Object.entries(this.config.agents)) {
193
+ if (agent.protocol === 'a2a' || !agent.workspace)
194
+ continue;
195
+ try {
196
+ await policy.resolve(agent.workspace);
197
+ }
198
+ catch {
199
+ throw new ControlPlaneError(409, `Workspace change would exclude agent: ${id}`);
200
+ }
201
+ }
192
202
  const raw = await this.readRawConfig();
193
203
  raw.workspaceRoots = workspaceRoots;
194
204
  await this.persist(raw);
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@ export declare function startAgentd(configPath: string): Promise<{
8
8
  sessions: SessionManager;
9
9
  runStore: RunStore;
10
10
  controlPlane: AgentdControlPlane;
11
- close(): Promise<void>;
11
+ close: () => Promise<void>;
12
12
  }>;
13
13
  export declare function closeServer(server: Server, graceMs?: number): Promise<void>;
14
14
  export * from './config.js';
package/dist/index.js CHANGED
@@ -16,17 +16,35 @@ export async function startAgentd(configPath) {
16
16
  sessions.startCleanup();
17
17
  const controlPlane = new AgentdControlPlane(absoluteConfigPath, config, sessions);
18
18
  const server = createAgentdServer(config, sessions, controlPlane);
19
- await listen(server, config.listen.port, config.listen.host);
19
+ let closing;
20
+ const close = () => closing ??= (async () => {
21
+ const results = await Promise.allSettled([closeServer(server), sessions.shutdown()]);
22
+ await runStore.flush();
23
+ const failed = results.find((result) => result.status === 'rejected');
24
+ if (failed)
25
+ throw failed.reason;
26
+ })();
27
+ try {
28
+ await listen(server, config.listen.port, config.listen.host);
29
+ }
30
+ catch (error) {
31
+ await sessions.shutdown();
32
+ await runStore.flush();
33
+ throw error;
34
+ }
35
+ server.on('error', (error) => {
36
+ console.error(JSON.stringify({ level: 'error', event: 'server_failed', message: error.message }));
37
+ void close().catch((closeError) => {
38
+ console.error(JSON.stringify({ level: 'error', event: 'server_shutdown_failed', message: String(closeError) }));
39
+ });
40
+ });
20
41
  return {
21
42
  config,
22
43
  server,
23
44
  sessions,
24
45
  runStore,
25
46
  controlPlane,
26
- async close() {
27
- await Promise.all([closeServer(server), sessions.shutdown()]);
28
- await runStore.flush();
29
- }
47
+ close
30
48
  };
31
49
  }
32
50
  function listen(server, port, host) {
@@ -37,6 +37,11 @@ export declare class RunStore {
37
37
  list(query?: RunListQuery): {
38
38
  runs: AgentdRunView[];
39
39
  total: number;
40
+ stats: {
41
+ active: number;
42
+ completed: number;
43
+ failed: number;
44
+ };
40
45
  };
41
46
  flush(): Promise<void>;
42
47
  private load;
package/dist/run-store.js CHANGED
@@ -144,8 +144,17 @@ export class RunStore {
144
144
  run.agentName.toLowerCase().includes(needle) ||
145
145
  run.id.toLowerCase().includes(needle));
146
146
  const total = matched.length;
147
+ const stats = { active: 0, completed: 0, failed: 0 };
148
+ for (const run of matched) {
149
+ if (isActive(run.state))
150
+ stats.active++;
151
+ if (run.state === 'completed')
152
+ stats.completed++;
153
+ if (run.state === 'failed')
154
+ stats.failed++;
155
+ }
147
156
  const runs = matched.slice(0, limit).map(publicView);
148
- return { runs, total };
157
+ return { runs, total, stats };
149
158
  }
150
159
  async flush() {
151
160
  this.assertInitialized();
@@ -18,6 +18,7 @@ export interface AcpSessionSink extends AgentSessionSink {
18
18
  setAcpSessionId(id: string): void;
19
19
  }
20
20
  export interface AgentSessionRuntime {
21
+ isAvailable?(): boolean;
21
22
  start(workspace?: string): Promise<void>;
22
23
  prompt(message: string, attachments?: AgentdInputAttachment[]): Promise<void>;
23
24
  respondPending(response: AgentdPendingResponse | string, attachments?: AgentdInputAttachment[]): Promise<void>;
package/dist/session.d.ts CHANGED
@@ -3,7 +3,7 @@ import { SessionEventLog } from './events.js';
3
3
  import { RunStore, type RunListQuery } from './run-store.js';
4
4
  import type { AcpSessionSink, AgentSessionRuntime, AgentSessionSink } from './session-contract.js';
5
5
  import type { AgentdAgentView, AgentdArtifact, AgentdConfig, AgentdEvent, AgentdEventType, AgentdInputAttachmentView, AgentdPendingRequest, AgentdPendingResponse, AgentdProtocol, AgentdSessionState, AgentdSessionView, AgentdTurnCompletionProof } from './types.js';
6
- import type { WorkspacePolicy } from './workspace.js';
6
+ import { type WorkspacePolicy } from './workspace.js';
7
7
  export declare class ManagedSession implements AgentSessionSink, AcpSessionSink {
8
8
  readonly agentId: string;
9
9
  readonly protocol: AgentdProtocol;
@@ -74,6 +74,11 @@ export declare class SessionManager {
74
74
  listRuns(query?: RunListQuery): {
75
75
  runs: import("./types.js").AgentdRunView[];
76
76
  total: number;
77
+ stats: {
78
+ active: number;
79
+ completed: number;
80
+ failed: number;
81
+ };
77
82
  };
78
83
  getRun(id: string): import("./types.js").AgentdRunDetail;
79
84
  message(id: string, message: string, attachmentIds?: string[]): Promise<AgentdSessionView>;