macro-agent 0.0.7 → 0.0.9

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 (36) hide show
  1. package/dist/cli/acp.d.ts.map +1 -1
  2. package/dist/cli/acp.js +101 -3
  3. package/dist/cli/acp.js.map +1 -1
  4. package/dist/map/adapter/acp-over-map.d.ts +77 -0
  5. package/dist/map/adapter/acp-over-map.d.ts.map +1 -0
  6. package/dist/map/adapter/acp-over-map.js +372 -0
  7. package/dist/map/adapter/acp-over-map.js.map +1 -0
  8. package/dist/map/adapter/index.d.ts +1 -0
  9. package/dist/map/adapter/index.d.ts.map +1 -1
  10. package/dist/map/adapter/index.js +2 -0
  11. package/dist/map/adapter/index.js.map +1 -1
  12. package/dist/map/adapter/interface.d.ts +22 -0
  13. package/dist/map/adapter/interface.d.ts.map +1 -1
  14. package/dist/map/adapter/map-adapter.d.ts +25 -1
  15. package/dist/map/adapter/map-adapter.d.ts.map +1 -1
  16. package/dist/map/adapter/map-adapter.js +147 -21
  17. package/dist/map/adapter/map-adapter.js.map +1 -1
  18. package/dist/map/adapter/rpc-handler.d.ts.map +1 -1
  19. package/dist/map/adapter/rpc-handler.js +4 -0
  20. package/dist/map/adapter/rpc-handler.js.map +1 -1
  21. package/dist/map/adapter/subscription-manager.js.map +1 -1
  22. package/dist/server/combined-server.d.ts.map +1 -1
  23. package/dist/server/combined-server.js +8 -1
  24. package/dist/server/combined-server.js.map +1 -1
  25. package/dist/store/event-store.js +7 -1
  26. package/dist/store/event-store.js.map +1 -1
  27. package/package.json +2 -2
  28. package/src/cli/acp.ts +107 -3
  29. package/src/map/adapter/acp-over-map.ts +516 -0
  30. package/src/map/adapter/index.ts +8 -0
  31. package/src/map/adapter/interface.ts +30 -6
  32. package/src/map/adapter/map-adapter.ts +325 -76
  33. package/src/map/adapter/rpc-handler.ts +4 -0
  34. package/src/map/adapter/subscription-manager.ts +10 -10
  35. package/src/server/combined-server.ts +8 -1
  36. package/src/store/event-store.ts +8 -2
@@ -0,0 +1,516 @@
1
+ /**
2
+ * ACP-over-MAP Handler
3
+ *
4
+ * Handles ACP protocol messages that arrive via MAP instead of direct WebSocket.
5
+ * Provides a bridge between MAP messaging and the MacroAgent ACP implementation.
6
+ *
7
+ * This allows external clients to communicate with macro-agent agents using
8
+ * ACP protocol semantics over the MAP transport layer.
9
+ */
10
+
11
+ import type { AgentManager } from "../../agent/agent-manager.js";
12
+ import type { EventStore } from "../../store/event-store.js";
13
+ import type { TaskManager } from "../../task/task-manager.js";
14
+ import type { AgentId } from "../../store/types/index.js";
15
+ import { SessionMapper } from "../../acp/session-mapper.js";
16
+ import type { ACPSessionId } from "../../acp/types.js";
17
+
18
+ // ─────────────────────────────────────────────────────────────────
19
+ // Types
20
+ // ─────────────────────────────────────────────────────────────────
21
+
22
+ export interface ACPEnvelope {
23
+ acp: {
24
+ jsonrpc: string;
25
+ id?: string | number;
26
+ method?: string;
27
+ params?: unknown;
28
+ result?: unknown;
29
+ error?: { code: number; message: string; data?: unknown };
30
+ };
31
+ acpContext: {
32
+ streamId: string;
33
+ sessionId?: string;
34
+ direction: string;
35
+ };
36
+ }
37
+
38
+ export interface ACPOverMAPConfig {
39
+ agentManager: AgentManager;
40
+ eventStore: EventStore;
41
+ taskManager: TaskManager;
42
+ defaultCwd?: string;
43
+ }
44
+
45
+ /**
46
+ * Callback for emitting ACP notifications during request processing.
47
+ * Used to stream session updates back to the client.
48
+ */
49
+ export type ACPNotificationEmitter = (notification: ACPEnvelope) => void;
50
+
51
+ interface StreamState {
52
+ initialized: boolean;
53
+ streamId: string;
54
+ sessionId?: string;
55
+ agentId?: AgentId;
56
+ abortController: AbortController;
57
+ }
58
+
59
+ // ─────────────────────────────────────────────────────────────────
60
+ // ACP-over-MAP Handler
61
+ // ─────────────────────────────────────────────────────────────────
62
+
63
+ export class ACPOverMAPHandler {
64
+ private agentManager: AgentManager;
65
+ private eventStore: EventStore;
66
+ private taskManager: TaskManager;
67
+ private defaultCwd: string;
68
+
69
+ /** Stream states by streamId */
70
+ private streams: Map<string, StreamState> = new Map();
71
+
72
+ /** Session mapper for ACP session -> Agent mapping */
73
+ private sessionMapper: SessionMapper = new SessionMapper();
74
+
75
+ constructor(config: ACPOverMAPConfig) {
76
+ this.agentManager = config.agentManager;
77
+ this.eventStore = config.eventStore;
78
+ this.taskManager = config.taskManager;
79
+ this.defaultCwd = config.defaultCwd ?? process.cwd();
80
+ }
81
+
82
+ /**
83
+ * Process an ACP request and return the response.
84
+ * @param targetAgentId - Target agent for the request
85
+ * @param envelope - ACP request envelope
86
+ * @param emitNotification - Optional callback to emit notifications (for streaming updates)
87
+ */
88
+ async processRequest(
89
+ targetAgentId: AgentId,
90
+ envelope: ACPEnvelope,
91
+ emitNotification?: ACPNotificationEmitter,
92
+ ): Promise<ACPEnvelope> {
93
+ const { acp, acpContext } = envelope;
94
+ const { streamId, sessionId } = acpContext;
95
+ const method = acp.method;
96
+
97
+ console.error(`[ACP-over-MAP] Processing - streamId=${streamId} method=${method}`);
98
+
99
+ // Get or create stream state
100
+ let streamState = this.streams.get(streamId);
101
+ if (!streamState) {
102
+ streamState = {
103
+ initialized: false,
104
+ streamId,
105
+ abortController: new AbortController(),
106
+ };
107
+ this.streams.set(streamId, streamState);
108
+ }
109
+
110
+ let result: unknown;
111
+ let error: { code: number; message: string; data?: unknown } | undefined;
112
+
113
+ try {
114
+ switch (method) {
115
+ case "initialize":
116
+ result = await this.handleInitialize(streamState, acp.params);
117
+ break;
118
+
119
+ case "session/new":
120
+ result = await this.handleNewSession(streamState, acp.params);
121
+ break;
122
+
123
+ case "session/load":
124
+ result = await this.handleLoadSession(streamState, acp.params);
125
+ break;
126
+
127
+ case "authenticate":
128
+ result = await this.handleAuthenticate(acp.params);
129
+ break;
130
+
131
+ case "session/prompt":
132
+ result = await this.handlePrompt(streamState, acp.params, sessionId, emitNotification);
133
+ break;
134
+
135
+ case "session/cancel":
136
+ result = await this.handleCancel(streamState, acp.params, sessionId);
137
+ break;
138
+
139
+ default:
140
+ // Check for extension methods
141
+ if (method?.startsWith("_")) {
142
+ result = await this.handleExtension(streamState, method, acp.params);
143
+ } else {
144
+ throw new Error(`Unknown ACP method: ${method}`);
145
+ }
146
+ }
147
+ } catch (e) {
148
+ console.error(`[ACP-over-MAP] Error processing ${method}:`, e);
149
+ error = {
150
+ code: -32603,
151
+ message: e instanceof Error ? e.message : String(e),
152
+ };
153
+ }
154
+
155
+ // Build response envelope
156
+ return {
157
+ acp: {
158
+ jsonrpc: "2.0",
159
+ id: acp.id,
160
+ ...(error ? { error } : { result }),
161
+ },
162
+ acpContext: {
163
+ streamId,
164
+ sessionId: streamState.sessionId,
165
+ direction: "agent-to-client",
166
+ },
167
+ };
168
+ }
169
+
170
+ // ─────────────────────────────────────────────────────────────────
171
+ // Core ACP Methods
172
+ // ─────────────────────────────────────────────────────────────────
173
+
174
+ private async handleInitialize(
175
+ streamState: StreamState,
176
+ params: unknown,
177
+ ): Promise<unknown> {
178
+ if (streamState.initialized) {
179
+ throw new Error("Stream already initialized");
180
+ }
181
+
182
+ streamState.initialized = true;
183
+
184
+ return {
185
+ protocolVersion: 1,
186
+ agentCapabilities: {
187
+ loadSession: true,
188
+ _meta: {
189
+ extensions: [
190
+ "_macro/spawnAgent",
191
+ "_macro/getHierarchy",
192
+ "_macro/getTask",
193
+ ],
194
+ agentType: "macro-agent",
195
+ transport: "acp-over-map",
196
+ },
197
+ },
198
+ };
199
+ }
200
+
201
+ private async handleNewSession(
202
+ streamState: StreamState,
203
+ params: unknown,
204
+ ): Promise<unknown> {
205
+ if (!streamState.initialized) {
206
+ throw new Error("Must call initialize before newSession");
207
+ }
208
+
209
+ const { cwd, mcpServers } = (params as { cwd?: string; mcpServers?: unknown[] }) ?? {};
210
+ const workingDir = cwd ?? this.defaultCwd;
211
+
212
+ // Spawn a new head manager for this session
213
+ const spawned = await this.agentManager.getOrCreateHeadManager({
214
+ cwd: workingDir,
215
+ forceNew: true,
216
+ });
217
+
218
+ const sessionId = spawned.session_id;
219
+ streamState.sessionId = sessionId;
220
+ streamState.agentId = spawned.id;
221
+
222
+ // Create session mapping
223
+ this.sessionMapper.createMapping(sessionId as ACPSessionId, spawned.id);
224
+
225
+ console.error(`[ACP-over-MAP] Created session ${sessionId} -> agent ${spawned.id}`);
226
+
227
+ return { sessionId };
228
+ }
229
+
230
+ private async handleLoadSession(
231
+ streamState: StreamState,
232
+ params: unknown,
233
+ ): Promise<unknown> {
234
+ if (!streamState.initialized) {
235
+ throw new Error("Must call initialize before loadSession");
236
+ }
237
+
238
+ const { sessionId, cwd } = (params as { sessionId: string; cwd?: string }) ?? {};
239
+ if (!sessionId) {
240
+ throw new Error("sessionId required");
241
+ }
242
+
243
+ const workingDir = cwd ?? this.defaultCwd;
244
+
245
+ // Try to find an existing head manager with this session ID
246
+ const headManagers = this.agentManager.listHeadManagers();
247
+ const existing = headManagers.find((hm) => hm.session_id === sessionId);
248
+
249
+ if (existing) {
250
+ // Check if the agent already has an active session
251
+ if (this.agentManager.hasActiveSession(existing.id)) {
252
+ console.error(`[ACP-over-MAP] loadSession: Reusing existing session for agent ${existing.id}`);
253
+ streamState.sessionId = sessionId;
254
+ streamState.agentId = existing.id;
255
+ this.sessionMapper.createMapping(sessionId as ACPSessionId, existing.id);
256
+ return {};
257
+ }
258
+
259
+ // Agent exists but no active session - resume it
260
+ console.error(`[ACP-over-MAP] loadSession: Resuming stopped agent ${existing.id}`);
261
+ const spawned = await this.agentManager.resume(existing.id);
262
+ streamState.sessionId = sessionId;
263
+ streamState.agentId = spawned.id;
264
+ this.sessionMapper.createMapping(sessionId as ACPSessionId, spawned.id);
265
+ return {};
266
+ }
267
+
268
+ // No existing agent found - create new with the specified session ID
269
+ console.error(`[ACP-over-MAP] loadSession: Creating new agent for session ${sessionId}`);
270
+ const spawned = await this.agentManager.getOrCreateHeadManager({
271
+ cwd: workingDir,
272
+ sessionId,
273
+ });
274
+
275
+ streamState.sessionId = sessionId;
276
+ streamState.agentId = spawned.id;
277
+ this.sessionMapper.createMapping(sessionId as ACPSessionId, spawned.id);
278
+
279
+ return {};
280
+ }
281
+
282
+ private async handleAuthenticate(_params: unknown): Promise<unknown> {
283
+ // No authentication required
284
+ return {};
285
+ }
286
+
287
+ private async handlePrompt(
288
+ streamState: StreamState,
289
+ params: unknown,
290
+ sessionIdFromContext?: string,
291
+ emitNotification?: ACPNotificationEmitter,
292
+ ): Promise<unknown> {
293
+ const { prompt, sessionId: paramSessionId } = (params as {
294
+ prompt?: Array<{ type: string; text?: string }>;
295
+ sessionId?: string;
296
+ messages?: Array<{ role: string; content: string }>;
297
+ }) ?? {};
298
+
299
+ const sessionId = paramSessionId ?? sessionIdFromContext ?? streamState.sessionId;
300
+ if (!sessionId) {
301
+ throw new Error("No session - call newSession or loadSession first");
302
+ }
303
+
304
+ // Get the mapped agent
305
+ const agentId = this.sessionMapper.getAgentId(sessionId as ACPSessionId);
306
+ if (!agentId) {
307
+ throw new Error(`No agent mapped for session ${sessionId}`);
308
+ }
309
+
310
+ // Reset abort controller if needed
311
+ if (streamState.abortController.signal.aborted) {
312
+ streamState.abortController = new AbortController();
313
+ }
314
+
315
+ // Extract message content
316
+ let messageContent: string;
317
+ if (prompt && Array.isArray(prompt)) {
318
+ messageContent = prompt
319
+ .filter((block) => block.type === "text" && block.text)
320
+ .map((block) => block.text)
321
+ .join("\n");
322
+ } else if ((params as { messages?: Array<{ content: string }> })?.messages) {
323
+ // Handle messages format (role/content array)
324
+ const messages = (params as { messages: Array<{ role: string; content: string }> }).messages;
325
+ messageContent = messages.map((m) => m.content).join("\n");
326
+ } else {
327
+ messageContent = JSON.stringify(params);
328
+ }
329
+
330
+ console.error(`[ACP-over-MAP] Prompting agent ${agentId} with: ${messageContent.slice(0, 100)}...`);
331
+
332
+ // Mark session as processing
333
+ this.sessionMapper.setProcessing(sessionId as ACPSessionId, true);
334
+
335
+ // Helper to emit session update notifications
336
+ const emitSessionUpdate = (update: unknown) => {
337
+ if (!emitNotification) return;
338
+
339
+ const notification: ACPEnvelope = {
340
+ acp: {
341
+ jsonrpc: "2.0",
342
+ method: "session/update",
343
+ params: {
344
+ sessionId: sessionId,
345
+ update: update,
346
+ },
347
+ },
348
+ acpContext: {
349
+ streamId: streamState.streamId,
350
+ sessionId: sessionId,
351
+ direction: "agent-to-client",
352
+ },
353
+ };
354
+ emitNotification(notification);
355
+ };
356
+
357
+ try {
358
+ // Stream responses from the agent
359
+ let updateCount = 0;
360
+ for await (const update of this.agentManager.prompt(agentId, messageContent)) {
361
+ // Check for cancellation
362
+ if (streamState.abortController.signal.aborted) {
363
+ return { stopReason: "cancelled" };
364
+ }
365
+
366
+ // Stream the update back to the client
367
+ emitSessionUpdate(update);
368
+ updateCount++;
369
+ }
370
+
371
+ console.error(`[ACP-over-MAP] Prompt completed for agent ${agentId}, ${updateCount} updates`);
372
+
373
+ return { stopReason: "end_turn" };
374
+ } catch (error) {
375
+ console.error(`[ACP-over-MAP] Prompt error:`, error);
376
+ return {
377
+ stopReason: "end_turn",
378
+ error: error instanceof Error ? error.message : String(error),
379
+ };
380
+ } finally {
381
+ this.sessionMapper.setProcessing(sessionId as ACPSessionId, false);
382
+ }
383
+ }
384
+
385
+ private async handleCancel(
386
+ streamState: StreamState,
387
+ params: unknown,
388
+ sessionIdFromContext?: string,
389
+ ): Promise<unknown> {
390
+ const { sessionId: paramSessionId } = (params as { sessionId?: string }) ?? {};
391
+ const sessionId = paramSessionId ?? sessionIdFromContext ?? streamState.sessionId;
392
+
393
+ // Signal cancellation
394
+ streamState.abortController.abort();
395
+
396
+ if (!sessionId) {
397
+ return { cancelled: true };
398
+ }
399
+
400
+ // Get the mapped agent
401
+ const agentId = this.sessionMapper.getAgentId(sessionId as ACPSessionId);
402
+ if (!agentId) {
403
+ return { cancelled: true };
404
+ }
405
+
406
+ // Terminate the agent
407
+ try {
408
+ await this.agentManager.terminate(agentId, "cancelled");
409
+ } catch (error) {
410
+ console.warn(`[ACP-over-MAP] Error terminating agent ${agentId}:`, error);
411
+ }
412
+
413
+ // Clean up
414
+ this.sessionMapper.removeMapping(sessionId as ACPSessionId);
415
+
416
+ return { cancelled: true };
417
+ }
418
+
419
+ // ─────────────────────────────────────────────────────────────────
420
+ // Extension Methods
421
+ // ─────────────────────────────────────────────────────────────────
422
+
423
+ private async handleExtension(
424
+ streamState: StreamState,
425
+ method: string,
426
+ params: unknown,
427
+ ): Promise<unknown> {
428
+ const methodParams = params as Record<string, unknown> ?? {};
429
+
430
+ switch (method) {
431
+ case "_macro/spawnAgent": {
432
+ const { task, cwd, topics, config, parentId } = methodParams as {
433
+ task: string;
434
+ cwd?: string;
435
+ topics?: string[];
436
+ config?: Record<string, unknown>;
437
+ parentId?: string;
438
+ };
439
+
440
+ // Use the stream's agent as parent, or find head manager
441
+ let parent = streamState.agentId;
442
+ if (parentId) {
443
+ parent = parentId as AgentId;
444
+ } else if (!parent) {
445
+ const headManagers = this.agentManager.listHeadManagers();
446
+ if (headManagers.length > 0) {
447
+ parent = headManagers[0].id;
448
+ }
449
+ }
450
+
451
+ if (!parent) {
452
+ throw new Error("No parent agent available for spawning");
453
+ }
454
+
455
+ const spawned = await this.agentManager.spawn({
456
+ parent,
457
+ task,
458
+ cwd: cwd ?? this.defaultCwd,
459
+ role: "worker",
460
+ });
461
+
462
+ return {
463
+ agentId: spawned.id,
464
+ sessionId: spawned.session_id,
465
+ };
466
+ }
467
+
468
+ case "_macro/getHierarchy": {
469
+ const agents = this.agentManager.list();
470
+ return {
471
+ agents: agents.map((a) => ({
472
+ id: a.id,
473
+ role: a.role,
474
+ state: a.state,
475
+ parent: a.parent,
476
+ createdAt: a.created_at,
477
+ })),
478
+ };
479
+ }
480
+
481
+ case "_macro/getTask": {
482
+ const { taskId } = methodParams as { taskId: string };
483
+ const task = await this.taskManager.get(taskId);
484
+ return { task };
485
+ }
486
+
487
+ default:
488
+ throw new Error(`Unknown extension method: ${method}`);
489
+ }
490
+ }
491
+
492
+ // ─────────────────────────────────────────────────────────────────
493
+ // Cleanup
494
+ // ─────────────────────────────────────────────────────────────────
495
+
496
+ /**
497
+ * Clean up a stream when it's closed.
498
+ */
499
+ closeStream(streamId: string): void {
500
+ const state = this.streams.get(streamId);
501
+ if (state) {
502
+ state.abortController.abort();
503
+ this.streams.delete(streamId);
504
+ }
505
+ }
506
+
507
+ /**
508
+ * Clean up all streams.
509
+ */
510
+ closeAll(): void {
511
+ for (const [streamId, state] of this.streams) {
512
+ state.abortController.abort();
513
+ }
514
+ this.streams.clear();
515
+ }
516
+ }
@@ -76,6 +76,14 @@ export {
76
76
  type MAPAdapterServices,
77
77
  } from "./map-adapter.js";
78
78
 
79
+ // ACP-over-MAP support
80
+ export {
81
+ ACPOverMAPHandler,
82
+ type ACPEnvelope,
83
+ type ACPOverMAPConfig,
84
+ type ACPNotificationEmitter,
85
+ } from "./acp-over-map.js";
86
+
79
87
  // Event translation
80
88
  export {
81
89
  translateEvent,
@@ -75,7 +75,7 @@ export interface ExtensionContext {
75
75
  */
76
76
  export type ExtensionHandler = (
77
77
  context: ExtensionContext,
78
- params: unknown
78
+ params: unknown,
79
79
  ) => Promise<unknown>;
80
80
 
81
81
  // =============================================================================
@@ -154,7 +154,7 @@ export interface AdapterLimits {
154
154
  */
155
155
  export type AuthenticateHandler = (
156
156
  participantType: ParticipantType,
157
- credentials: AuthCredentials
157
+ credentials: AuthCredentials,
158
158
  ) => Promise<AuthResult>;
159
159
 
160
160
  /**
@@ -232,6 +232,28 @@ export interface AgentFilter {
232
232
  parent?: AgentId;
233
233
  }
234
234
 
235
+ /**
236
+ * ACP capability advertisement (matches MAP SDK's ACPCapability).
237
+ */
238
+ export interface ACPCapability {
239
+ /** ACP protocol version supported (e.g., '2024-10-07') */
240
+ version?: string;
241
+ /** ACP features supported by this agent */
242
+ features?: string[];
243
+ }
244
+
245
+ /**
246
+ * Agent capabilities advertised via MAP (matches MAP SDK's ParticipantCapabilities).
247
+ */
248
+ export interface AgentCapabilities {
249
+ /** Protocols supported by this agent (e.g., ["acp"]) */
250
+ protocols?: string[];
251
+ /** ACP capability details (present if 'acp' is in protocols array) */
252
+ acp?: ACPCapability;
253
+ /** Additional capability flags */
254
+ [key: string]: unknown;
255
+ }
256
+
235
257
  /**
236
258
  * Agent info returned by queries.
237
259
  */
@@ -244,6 +266,8 @@ export interface AgentInfo {
244
266
  scopes: ScopeId[];
245
267
  metadata?: Record<string, unknown>;
246
268
  createdAt: number;
269
+ /** Agent capabilities (protocols, features) */
270
+ capabilities?: AgentCapabilities;
247
271
  }
248
272
 
249
273
  /**
@@ -368,7 +392,7 @@ export interface MAPAdapter {
368
392
  participantId: ParticipantId,
369
393
  to: Address,
370
394
  payload: MessagePayload,
371
- options?: SendOptions
395
+ options?: SendOptions,
372
396
  ): Promise<SendResult>;
373
397
 
374
398
  // ===========================================================================
@@ -388,7 +412,7 @@ export interface MAPAdapter {
388
412
  */
389
413
  createSubscription(
390
414
  participantId: ParticipantId,
391
- filter?: SubscriptionFilter
415
+ filter?: SubscriptionFilter,
392
416
  ): Promise<SubscriptionId>;
393
417
 
394
418
  /**
@@ -440,7 +464,7 @@ export interface MAPAdapter {
440
464
  */
441
465
  getAgent(
442
466
  participantId: ParticipantId,
443
- agentId: AgentId
467
+ agentId: AgentId,
444
468
  ): AgentInfo | undefined;
445
469
 
446
470
  /**
@@ -460,7 +484,7 @@ export interface MAPAdapter {
460
484
  */
461
485
  getScope(
462
486
  participantId: ParticipantId,
463
- scopeId: ScopeId
487
+ scopeId: ScopeId,
464
488
  ): ScopeInfo | undefined;
465
489
 
466
490
  // ===========================================================================