@a2a-wrapper/core 1.2.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.
Files changed (50) hide show
  1. package/README.md +186 -0
  2. package/dist/cli/scaffold.d.ts +237 -0
  3. package/dist/cli/scaffold.d.ts.map +1 -0
  4. package/dist/cli/scaffold.js +241 -0
  5. package/dist/cli/scaffold.js.map +1 -0
  6. package/dist/config/loader.d.ts +100 -0
  7. package/dist/config/loader.d.ts.map +1 -0
  8. package/dist/config/loader.js +130 -0
  9. package/dist/config/loader.js.map +1 -0
  10. package/dist/config/types.d.ts +317 -0
  11. package/dist/config/types.d.ts.map +1 -0
  12. package/dist/config/types.js +17 -0
  13. package/dist/config/types.js.map +1 -0
  14. package/dist/events/event-publisher.d.ts +205 -0
  15. package/dist/events/event-publisher.d.ts.map +1 -0
  16. package/dist/events/event-publisher.js +317 -0
  17. package/dist/events/event-publisher.js.map +1 -0
  18. package/dist/executor/types.d.ts +164 -0
  19. package/dist/executor/types.d.ts.map +1 -0
  20. package/dist/executor/types.js +30 -0
  21. package/dist/executor/types.js.map +1 -0
  22. package/dist/index.d.ts +37 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +34 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/server/agent-card.d.ts +66 -0
  27. package/dist/server/agent-card.d.ts.map +1 -0
  28. package/dist/server/agent-card.js +114 -0
  29. package/dist/server/agent-card.js.map +1 -0
  30. package/dist/server/factory.d.ts +159 -0
  31. package/dist/server/factory.d.ts.map +1 -0
  32. package/dist/server/factory.js +167 -0
  33. package/dist/server/factory.js.map +1 -0
  34. package/dist/session/base-session-manager.d.ts +218 -0
  35. package/dist/session/base-session-manager.d.ts.map +1 -0
  36. package/dist/session/base-session-manager.js +222 -0
  37. package/dist/session/base-session-manager.js.map +1 -0
  38. package/dist/utils/deep-merge.d.ts +83 -0
  39. package/dist/utils/deep-merge.d.ts.map +1 -0
  40. package/dist/utils/deep-merge.js +108 -0
  41. package/dist/utils/deep-merge.js.map +1 -0
  42. package/dist/utils/deferred.d.ts +97 -0
  43. package/dist/utils/deferred.d.ts.map +1 -0
  44. package/dist/utils/deferred.js +83 -0
  45. package/dist/utils/deferred.js.map +1 -0
  46. package/dist/utils/logger.d.ts +186 -0
  47. package/dist/utils/logger.d.ts.map +1 -0
  48. package/dist/utils/logger.js +244 -0
  49. package/dist/utils/logger.js.map +1 -0
  50. package/package.json +57 -0
@@ -0,0 +1,167 @@
1
+ /**
2
+ * A2A Server Factory
3
+ *
4
+ * Creates, wires, and starts an Express-based A2A HTTP server with all
5
+ * standard protocol routes. This module is the single entry point for
6
+ * server bootstrapping across all wrapper projects, ensuring consistent
7
+ * route registration, middleware ordering, and lifecycle management.
8
+ *
9
+ * Ported from `a2a-copilot/src/server/index.ts` and
10
+ * `a2a-opencode/src/server/index.ts` with the following changes for
11
+ * core-package reuse:
12
+ *
13
+ * 1. Accepts a generic `executorFactory` callback instead of importing a
14
+ * concrete executor class — the core package has zero knowledge of any
15
+ * specific backend.
16
+ * 2. Supports an optional {@link ServerOptions.registerRoutes} hook so that
17
+ * wrapper projects can mount custom routes (e.g. `/context`, `/mcp/status`)
18
+ * before the server starts listening.
19
+ * 3. The `A2A-Version` response header value is configurable via
20
+ * {@link ServerOptions.protocolVersion} (default `"0.3"`).
21
+ * 4. Wrapper-specific routes (context API, MCP status) are **not** included —
22
+ * those belong in each wrapper's `registerRoutes` hook.
23
+ *
24
+ * @module server/factory
25
+ */
26
+ import express from "express";
27
+ import { AGENT_CARD_PATH } from "@a2a-js/sdk";
28
+ import { DefaultRequestHandler, InMemoryTaskStore } from "@a2a-js/sdk/server";
29
+ import { jsonRpcHandler, restHandler, UserBuilder, } from "@a2a-js/sdk/server/express";
30
+ import { buildAgentCard } from "./agent-card.js";
31
+ // ─── createA2AServer ────────────────────────────────────────────────────────
32
+ /**
33
+ * Create, wire, and start an A2A-compliant Express server.
34
+ *
35
+ * This function is the primary entry point for all wrapper projects. It:
36
+ *
37
+ * 1. Instantiates the backend executor via the supplied `executorFactory`.
38
+ * 2. Builds the static agent card from resolved configuration.
39
+ * 3. Creates the A2A SDK request handler with an in-memory task store.
40
+ * 4. Mounts middleware and standard routes:
41
+ * - `A2A-Version` response header on every response.
42
+ * - `GET /health` — health check endpoint.
43
+ * - `GET /.well-known/agent-card.json` — dynamic agent card with URL
44
+ * rewriting for reverse proxy compatibility.
45
+ * - Legacy agent card paths (`.well-known/agent.json`,
46
+ * `.well-known/agent-json`).
47
+ * - `POST /a2a/jsonrpc` — JSON-RPC transport.
48
+ * - `/a2a/rest` — REST transport.
49
+ * 5. Invokes the optional `registerRoutes` hook for wrapper-specific routes.
50
+ * 6. Starts listening on the configured hostname and port.
51
+ * 7. Returns a {@link ServerHandle} for lifecycle management.
52
+ *
53
+ * @typeParam T - The full configuration type, extending {@link BaseAgentConfig}.
54
+ * The generic parameter ensures the `executorFactory` receives the same
55
+ * fully-resolved config type that the wrapper project defined.
56
+ *
57
+ * @param config - Fully resolved configuration with all fields populated.
58
+ * @param executorFactory - Factory function that creates the backend-specific
59
+ * executor from the resolved config. The server factory
60
+ * calls `executor.initialize()` before registering routes.
61
+ * @param options - Optional server customization (protocol version,
62
+ * custom route hooks).
63
+ * @returns A promise resolving to a {@link ServerHandle} once the server is
64
+ * listening and ready to accept requests.
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * import { createA2AServer } from "@a2a-wrapper/core";
69
+ * import { MyExecutor } from "./my-executor.js";
70
+ *
71
+ * const handle = await createA2AServer(
72
+ * resolvedConfig,
73
+ * (cfg) => new MyExecutor(cfg),
74
+ * {
75
+ * protocolVersion: "0.3",
76
+ * registerRoutes: (app, executor) => {
77
+ * app.get("/custom", (_req, res) => res.json({ ok: true }));
78
+ * },
79
+ * },
80
+ * );
81
+ *
82
+ * // Graceful shutdown on SIGTERM
83
+ * process.on("SIGTERM", () => handle.shutdown());
84
+ * ```
85
+ */
86
+ export async function createA2AServer(config, executorFactory, options) {
87
+ const { server: srv } = config;
88
+ const port = srv.port ?? 3000;
89
+ const hostname = srv.hostname ?? "0.0.0.0";
90
+ const advertiseHost = srv.advertiseHost ?? "localhost";
91
+ const advertiseProto = srv.advertiseProtocol ?? "http";
92
+ const protocolVersion = options?.protocolVersion ?? "0.3";
93
+ // ── 1. Executor ─────────────────────────────────────────────────────────
94
+ const executor = executorFactory(config);
95
+ await executor.initialize();
96
+ // ── 2. Agent card (static, used as base for dynamic rewriting) ──────────
97
+ const agentCard = buildAgentCard(config);
98
+ // ── 3. A2A SDK request handler ──────────────────────────────────────────
99
+ const taskStore = new InMemoryTaskStore();
100
+ const requestHandler = new DefaultRequestHandler(agentCard, taskStore, executor);
101
+ // ── 4. Express app ──────────────────────────────────────────────────────
102
+ const app = express();
103
+ // ── A2A-Version response header middleware ──────────────────────────────
104
+ // Every response includes the protocol version this server implements,
105
+ // enabling clients to detect version mismatches and future negotiation.
106
+ app.use((_req, res, next) => {
107
+ res.setHeader("A2A-Version", protocolVersion);
108
+ next();
109
+ });
110
+ // ── Health check ────────────────────────────────────────────────────────
111
+ app.get("/health", (_req, res) => {
112
+ res.json({ status: "healthy", agent: agentCard.name });
113
+ });
114
+ // ── Dynamic agent card handler ──────────────────────────────────────────
115
+ // Rewrites endpoint URLs to match the caller's Host + x-forwarded-proto
116
+ // headers so clients behind Docker / reverse proxies reach the correct
117
+ // address for JSON-RPC / REST endpoints.
118
+ const serveAgentCard = (req, res) => {
119
+ const host = req.headers.host || `${advertiseHost}:${port}`;
120
+ const proto = req.headers["x-forwarded-proto"] || advertiseProto;
121
+ const dynamicBase = `${proto}://${host}`;
122
+ const jsonRpcUrl = `${dynamicBase}/a2a/jsonrpc`;
123
+ const restUrl = `${dynamicBase}/a2a/rest`;
124
+ res.json({
125
+ ...agentCard,
126
+ url: jsonRpcUrl,
127
+ additionalInterfaces: [
128
+ { transport: "JSONRPC", url: jsonRpcUrl },
129
+ { transport: "REST", url: restUrl },
130
+ ],
131
+ });
132
+ };
133
+ // Current A2A spec path
134
+ app.get(`/${AGENT_CARD_PATH}`, serveAgentCard);
135
+ // Legacy agent card paths for older A2A Inspector versions
136
+ for (const p of [".well-known/agent.json", ".well-known/agent-json"]) {
137
+ if (p !== AGENT_CARD_PATH) {
138
+ app.get(`/${p}`, serveAgentCard);
139
+ }
140
+ }
141
+ // ── A2A transports ──────────────────────────────────────────────────────
142
+ app.use("/a2a/jsonrpc", jsonRpcHandler({
143
+ requestHandler,
144
+ userBuilder: UserBuilder.noAuthentication,
145
+ }));
146
+ app.use("/a2a/rest", restHandler({
147
+ requestHandler,
148
+ userBuilder: UserBuilder.noAuthentication,
149
+ }));
150
+ // ── 5. Wrapper-specific custom routes ───────────────────────────────────
151
+ if (options?.registerRoutes) {
152
+ options.registerRoutes(app, executor);
153
+ }
154
+ // ── 6. Start listening ──────────────────────────────────────────────────
155
+ const httpServer = app.listen(port, hostname);
156
+ // ── 7. Return handle ───────────────────────────────────────────────────
157
+ return {
158
+ app,
159
+ server: httpServer,
160
+ executor,
161
+ async shutdown() {
162
+ httpServer.close();
163
+ await executor.shutdown();
164
+ },
165
+ };
166
+ }
167
+ //# sourceMappingURL=factory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"factory.js","sourceRoot":"","sources":["../../src/server/factory.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Base Session Manager
3
+ *
4
+ * Provides the abstract foundation for session lifecycle management across
5
+ * all A2A wrapper projects. This module handles the mapping from A2A
6
+ * `contextId` values to backend-specific session entries, TTL-based
7
+ * expiration with periodic cleanup sweeps, and task-to-session tracking
8
+ * required for cancel support.
9
+ *
10
+ * Wrapper projects extend {@link BaseSessionManager} and implement the
11
+ * abstract {@link BaseSessionManager.getOrCreate | getOrCreate} method to
12
+ * provide backend-specific session creation logic (e.g. creating a Copilot
13
+ * SDK session or an OpenCode session). All shared bookkeeping — context map
14
+ * management, cleanup timers, task tracking — lives here so that each
15
+ * wrapper only contains its backend-specific code.
16
+ *
17
+ * The class is parameterized by `TSession`, the backend session object type,
18
+ * enabling full type safety without runtime coupling to any specific backend.
19
+ *
20
+ * @module session/base-session-manager
21
+ */
22
+ import type { SessionConfig } from "../config/types.js";
23
+ /**
24
+ * Internal session entry stored by {@link BaseSessionManager}.
25
+ *
26
+ * Each entry pairs a backend session object with metadata used for
27
+ * TTL-based expiration and session reuse decisions. The `lastUsed`
28
+ * timestamp is updated on every successful `getOrCreate` hit so that
29
+ * active sessions are not prematurely evicted by the cleanup sweep.
30
+ *
31
+ * @typeParam TSession - The backend-specific session object type
32
+ * (e.g. `CopilotSession`, `string` session ID for OpenCode).
33
+ */
34
+ export interface SessionEntry<TSession> {
35
+ /** Unique session identifier assigned by the backend. */
36
+ sessionId: string;
37
+ /**
38
+ * The backend session object.
39
+ * May be a rich SDK object (CopilotSession) or a simple identifier
40
+ * (string session ID for OpenCode), depending on the wrapper.
41
+ */
42
+ session: TSession;
43
+ /** Timestamp (ms since epoch) when this session was first created. */
44
+ createdAt: number;
45
+ /**
46
+ * Timestamp (ms since epoch) of last activity on this session.
47
+ * Updated on every `getOrCreate` cache hit. Used by the cleanup
48
+ * sweep to determine whether the session has exceeded its TTL.
49
+ */
50
+ lastUsed: number;
51
+ }
52
+ /**
53
+ * Abstract base class for session lifecycle management.
54
+ *
55
+ * Manages the mapping from A2A `contextId` to backend session entries,
56
+ * TTL-based cleanup, and task-to-session tracking for cancel support.
57
+ * Wrapper projects extend this class and implement
58
+ * {@link BaseSessionManager.getOrCreate | getOrCreate} to provide
59
+ * backend-specific session creation logic.
60
+ *
61
+ * Key behaviors:
62
+ * - **TTL-based cleanup**: A periodic timer removes sessions whose
63
+ * `lastUsed` timestamp exceeds the configured TTL.
64
+ * - **Idempotent `startCleanup`**: Calling `startCleanup()` when a timer
65
+ * is already running is a safe no-op.
66
+ * - **Task tracking**: Maps `taskId` → `sessionId` and optionally
67
+ * `taskId` → `contextId` so that cancel operations can locate the
68
+ * correct session and emit the correct contextId.
69
+ * - **Protected helpers**: Subclasses access the context map through
70
+ * {@link getSessionEntry}, {@link setSessionEntry}, and
71
+ * {@link deleteSessionEntry} without direct map access.
72
+ *
73
+ * @typeParam TSession - The backend-specific session object type
74
+ * (e.g. `CopilotSession`, `string` session ID for OpenCode).
75
+ */
76
+ export declare abstract class BaseSessionManager<TSession> {
77
+ /**
78
+ * Resolved session configuration controlling TTL, cleanup interval,
79
+ * context reuse, and session title prefix.
80
+ */
81
+ protected readonly sessionConfig: Required<SessionConfig>;
82
+ /** A2A contextId → session entry. */
83
+ private readonly contextMap;
84
+ /** taskId → sessionId for cancel support. */
85
+ private readonly taskMap;
86
+ /** taskId → contextId for cancel support. */
87
+ private readonly taskContexts;
88
+ /** Handle for the periodic cleanup interval, or `null` when stopped. */
89
+ private cleanupTimer;
90
+ /**
91
+ * Create a new session manager instance.
92
+ *
93
+ * @param sessionConfig - Fully resolved session configuration. All
94
+ * optional fields must already be filled with defaults so that the
95
+ * manager can rely on every value being present.
96
+ */
97
+ constructor(sessionConfig: Required<SessionConfig>);
98
+ /**
99
+ * Start periodic cleanup of expired sessions.
100
+ *
101
+ * Sessions whose `lastUsed` timestamp exceeds the configured
102
+ * {@link SessionConfig.ttl | ttl} are removed from the context map
103
+ * on each sweep. The sweep interval is controlled by
104
+ * {@link SessionConfig.cleanupInterval | cleanupInterval}.
105
+ *
106
+ * This method is idempotent — calling it when a timer is already
107
+ * running is a safe no-op.
108
+ */
109
+ startCleanup(): void;
110
+ /**
111
+ * Stop the periodic cleanup timer.
112
+ *
113
+ * Safe to call even if no timer is running. After calling this method,
114
+ * expired sessions will no longer be automatically evicted until
115
+ * {@link startCleanup} is called again.
116
+ */
117
+ stopCleanup(): void;
118
+ /**
119
+ * Get or create a backend session for the given A2A contextId.
120
+ *
121
+ * Subclasses implement this method to provide backend-specific session
122
+ * creation logic. The implementation should use the protected helpers
123
+ * ({@link getSessionEntry}, {@link setSessionEntry},
124
+ * {@link deleteSessionEntry}) to interact with the context map.
125
+ *
126
+ * When `reuseByContext` is enabled in the session config, the
127
+ * implementation should check for an existing entry and return its
128
+ * session if the entry has not exceeded TTL.
129
+ *
130
+ * @param contextId - The A2A contextId identifying the conversation.
131
+ * @returns The backend session object for this context.
132
+ */
133
+ abstract getOrCreate(contextId: string): Promise<TSession>;
134
+ /**
135
+ * Track a task → session + context mapping for cancel support.
136
+ *
137
+ * Called by the executor when a new task begins execution so that
138
+ * subsequent cancel requests can locate the correct session and
139
+ * emit the correct contextId in status events.
140
+ *
141
+ * @param taskId - The A2A task identifier.
142
+ * @param sessionId - The backend session identifier handling this task.
143
+ * @param contextId - Optional A2A contextId associated with this task.
144
+ */
145
+ trackTask(taskId: string, sessionId: string, contextId?: string): void;
146
+ /**
147
+ * Get the session identifier for a tracked task.
148
+ *
149
+ * Used by cancel handlers to find the backend session that should
150
+ * be interrupted.
151
+ *
152
+ * @param taskId - The A2A task identifier.
153
+ * @returns The session identifier, or `undefined` if the task is not tracked.
154
+ */
155
+ getSessionForTask(taskId: string): string | undefined;
156
+ /**
157
+ * Get the A2A contextId for a tracked task.
158
+ *
159
+ * Used by cancel handlers to emit the correct contextId in status
160
+ * update events when cancelling a task.
161
+ *
162
+ * @param taskId - The A2A task identifier.
163
+ * @returns The contextId, or `undefined` if the task is not tracked
164
+ * or was tracked without a contextId.
165
+ */
166
+ getContextForTask(taskId: string): string | undefined;
167
+ /**
168
+ * Remove task tracking for a completed or cancelled task.
169
+ *
170
+ * Should be called when a task finishes (successfully or otherwise)
171
+ * to prevent unbounded growth of the task tracking maps.
172
+ *
173
+ * @param taskId - The A2A task identifier to stop tracking.
174
+ */
175
+ untrackTask(taskId: string): void;
176
+ /**
177
+ * Shut down the session manager.
178
+ *
179
+ * Stops the cleanup timer and clears all internal maps (context map,
180
+ * task map, task contexts). After calling this method, the session
181
+ * manager is in a clean state and should not be reused.
182
+ *
183
+ * Subclasses that need to perform additional cleanup (e.g. destroying
184
+ * backend sessions) should override this method and call `super.shutdown()`.
185
+ */
186
+ shutdown(): void;
187
+ /**
188
+ * Retrieve a session entry from the context map.
189
+ *
190
+ * Subclasses use this in their {@link getOrCreate} implementation to
191
+ * check for an existing session before creating a new one.
192
+ *
193
+ * @param contextId - The A2A contextId to look up.
194
+ * @returns The session entry, or `undefined` if no session exists
195
+ * for this contextId.
196
+ */
197
+ protected getSessionEntry(contextId: string): SessionEntry<TSession> | undefined;
198
+ /**
199
+ * Store or update a session entry in the context map.
200
+ *
201
+ * Subclasses use this in their {@link getOrCreate} implementation to
202
+ * register a newly created session or update an existing entry.
203
+ *
204
+ * @param contextId - The A2A contextId to associate with the entry.
205
+ * @param entry - The session entry to store.
206
+ */
207
+ protected setSessionEntry(contextId: string, entry: SessionEntry<TSession>): void;
208
+ /**
209
+ * Remove a session entry from the context map.
210
+ *
211
+ * Subclasses use this when a session is destroyed or invalidated
212
+ * (e.g. backend reports the session no longer exists).
213
+ *
214
+ * @param contextId - The A2A contextId whose entry should be removed.
215
+ */
216
+ protected deleteSessionEntry(contextId: string): void;
217
+ }
218
+ //# sourceMappingURL=base-session-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-session-manager.d.ts","sourceRoot":"","sources":["../../src/session/base-session-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAIxD;;;;;;;;;;GAUG;AACH,MAAM,WAAW,YAAY,CAAC,QAAQ;IACpC,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAElB;;;;OAIG;IACH,OAAO,EAAE,QAAQ,CAAC;IAElB,sEAAsE;IACtE,SAAS,EAAE,MAAM,CAAC;IAElB;;;;OAIG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB;AAID;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,8BAAsB,kBAAkB,CAAC,QAAQ;IAC/C;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,aAAa,CAAC,CAAC;IAE1D,qCAAqC;IACrC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA6C;IAExE,6CAA6C;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAErD,6CAA6C;IAC7C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA6B;IAE1D,wEAAwE;IACxE,OAAO,CAAC,YAAY,CAA+C;IAEnE;;;;;;OAMG;gBACS,aAAa,EAAE,QAAQ,CAAC,aAAa,CAAC;IAMlD;;;;;;;;;;OAUG;IACH,YAAY,IAAI,IAAI;IAkBpB;;;;;;OAMG;IACH,WAAW,IAAI,IAAI;IASnB;;;;;;;;;;;;;;OAcG;IACH,QAAQ,CAAC,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC;IAI1D;;;;;;;;;;OAUG;IACH,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI;IAOtE;;;;;;;;OAQG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIrD;;;;;;;;;OASG;IACH,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIrD;;;;;;;OAOG;IACH,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,IAAI;IAOjC;;;;;;;;;OASG;IACH,QAAQ,IAAI,IAAI;IAShB;;;;;;;;;OASG;IACH,SAAS,CAAC,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,YAAY,CAAC,QAAQ,CAAC,GAAG,SAAS;IAIhF;;;;;;;;OAQG;IACH,SAAS,CAAC,eAAe,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC,GAAG,IAAI;IAIjF;;;;;;;OAOG;IACH,SAAS,CAAC,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;CAGtD"}
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Base Session Manager
3
+ *
4
+ * Provides the abstract foundation for session lifecycle management across
5
+ * all A2A wrapper projects. This module handles the mapping from A2A
6
+ * `contextId` values to backend-specific session entries, TTL-based
7
+ * expiration with periodic cleanup sweeps, and task-to-session tracking
8
+ * required for cancel support.
9
+ *
10
+ * Wrapper projects extend {@link BaseSessionManager} and implement the
11
+ * abstract {@link BaseSessionManager.getOrCreate | getOrCreate} method to
12
+ * provide backend-specific session creation logic (e.g. creating a Copilot
13
+ * SDK session or an OpenCode session). All shared bookkeeping — context map
14
+ * management, cleanup timers, task tracking — lives here so that each
15
+ * wrapper only contains its backend-specific code.
16
+ *
17
+ * The class is parameterized by `TSession`, the backend session object type,
18
+ * enabling full type safety without runtime coupling to any specific backend.
19
+ *
20
+ * @module session/base-session-manager
21
+ */
22
+ // ─── Abstract Base Class ────────────────────────────────────────────────────
23
+ /**
24
+ * Abstract base class for session lifecycle management.
25
+ *
26
+ * Manages the mapping from A2A `contextId` to backend session entries,
27
+ * TTL-based cleanup, and task-to-session tracking for cancel support.
28
+ * Wrapper projects extend this class and implement
29
+ * {@link BaseSessionManager.getOrCreate | getOrCreate} to provide
30
+ * backend-specific session creation logic.
31
+ *
32
+ * Key behaviors:
33
+ * - **TTL-based cleanup**: A periodic timer removes sessions whose
34
+ * `lastUsed` timestamp exceeds the configured TTL.
35
+ * - **Idempotent `startCleanup`**: Calling `startCleanup()` when a timer
36
+ * is already running is a safe no-op.
37
+ * - **Task tracking**: Maps `taskId` → `sessionId` and optionally
38
+ * `taskId` → `contextId` so that cancel operations can locate the
39
+ * correct session and emit the correct contextId.
40
+ * - **Protected helpers**: Subclasses access the context map through
41
+ * {@link getSessionEntry}, {@link setSessionEntry}, and
42
+ * {@link deleteSessionEntry} without direct map access.
43
+ *
44
+ * @typeParam TSession - The backend-specific session object type
45
+ * (e.g. `CopilotSession`, `string` session ID for OpenCode).
46
+ */
47
+ export class BaseSessionManager {
48
+ /**
49
+ * Resolved session configuration controlling TTL, cleanup interval,
50
+ * context reuse, and session title prefix.
51
+ */
52
+ sessionConfig;
53
+ /** A2A contextId → session entry. */
54
+ contextMap = new Map();
55
+ /** taskId → sessionId for cancel support. */
56
+ taskMap = new Map();
57
+ /** taskId → contextId for cancel support. */
58
+ taskContexts = new Map();
59
+ /** Handle for the periodic cleanup interval, or `null` when stopped. */
60
+ cleanupTimer = null;
61
+ /**
62
+ * Create a new session manager instance.
63
+ *
64
+ * @param sessionConfig - Fully resolved session configuration. All
65
+ * optional fields must already be filled with defaults so that the
66
+ * manager can rely on every value being present.
67
+ */
68
+ constructor(sessionConfig) {
69
+ this.sessionConfig = sessionConfig;
70
+ }
71
+ // ─── Cleanup ────────────────────────────────────────────────────────────
72
+ /**
73
+ * Start periodic cleanup of expired sessions.
74
+ *
75
+ * Sessions whose `lastUsed` timestamp exceeds the configured
76
+ * {@link SessionConfig.ttl | ttl} are removed from the context map
77
+ * on each sweep. The sweep interval is controlled by
78
+ * {@link SessionConfig.cleanupInterval | cleanupInterval}.
79
+ *
80
+ * This method is idempotent — calling it when a timer is already
81
+ * running is a safe no-op.
82
+ */
83
+ startCleanup() {
84
+ if (this.cleanupTimer)
85
+ return;
86
+ this.cleanupTimer = setInterval(() => {
87
+ const now = Date.now();
88
+ for (const [contextId, entry] of this.contextMap) {
89
+ if (now - entry.lastUsed > this.sessionConfig.ttl) {
90
+ this.contextMap.delete(contextId);
91
+ }
92
+ }
93
+ }, this.sessionConfig.cleanupInterval);
94
+ // Allow the Node.js process to exit even if the timer is still active.
95
+ if (this.cleanupTimer.unref) {
96
+ this.cleanupTimer.unref();
97
+ }
98
+ }
99
+ /**
100
+ * Stop the periodic cleanup timer.
101
+ *
102
+ * Safe to call even if no timer is running. After calling this method,
103
+ * expired sessions will no longer be automatically evicted until
104
+ * {@link startCleanup} is called again.
105
+ */
106
+ stopCleanup() {
107
+ if (this.cleanupTimer) {
108
+ clearInterval(this.cleanupTimer);
109
+ this.cleanupTimer = null;
110
+ }
111
+ }
112
+ // ─── Task Tracking ──────────────────────────────────────────────────────
113
+ /**
114
+ * Track a task → session + context mapping for cancel support.
115
+ *
116
+ * Called by the executor when a new task begins execution so that
117
+ * subsequent cancel requests can locate the correct session and
118
+ * emit the correct contextId in status events.
119
+ *
120
+ * @param taskId - The A2A task identifier.
121
+ * @param sessionId - The backend session identifier handling this task.
122
+ * @param contextId - Optional A2A contextId associated with this task.
123
+ */
124
+ trackTask(taskId, sessionId, contextId) {
125
+ this.taskMap.set(taskId, sessionId);
126
+ if (contextId) {
127
+ this.taskContexts.set(taskId, contextId);
128
+ }
129
+ }
130
+ /**
131
+ * Get the session identifier for a tracked task.
132
+ *
133
+ * Used by cancel handlers to find the backend session that should
134
+ * be interrupted.
135
+ *
136
+ * @param taskId - The A2A task identifier.
137
+ * @returns The session identifier, or `undefined` if the task is not tracked.
138
+ */
139
+ getSessionForTask(taskId) {
140
+ return this.taskMap.get(taskId);
141
+ }
142
+ /**
143
+ * Get the A2A contextId for a tracked task.
144
+ *
145
+ * Used by cancel handlers to emit the correct contextId in status
146
+ * update events when cancelling a task.
147
+ *
148
+ * @param taskId - The A2A task identifier.
149
+ * @returns The contextId, or `undefined` if the task is not tracked
150
+ * or was tracked without a contextId.
151
+ */
152
+ getContextForTask(taskId) {
153
+ return this.taskContexts.get(taskId);
154
+ }
155
+ /**
156
+ * Remove task tracking for a completed or cancelled task.
157
+ *
158
+ * Should be called when a task finishes (successfully or otherwise)
159
+ * to prevent unbounded growth of the task tracking maps.
160
+ *
161
+ * @param taskId - The A2A task identifier to stop tracking.
162
+ */
163
+ untrackTask(taskId) {
164
+ this.taskMap.delete(taskId);
165
+ this.taskContexts.delete(taskId);
166
+ }
167
+ // ─── Shutdown ───────────────────────────────────────────────────────────
168
+ /**
169
+ * Shut down the session manager.
170
+ *
171
+ * Stops the cleanup timer and clears all internal maps (context map,
172
+ * task map, task contexts). After calling this method, the session
173
+ * manager is in a clean state and should not be reused.
174
+ *
175
+ * Subclasses that need to perform additional cleanup (e.g. destroying
176
+ * backend sessions) should override this method and call `super.shutdown()`.
177
+ */
178
+ shutdown() {
179
+ this.stopCleanup();
180
+ this.contextMap.clear();
181
+ this.taskMap.clear();
182
+ this.taskContexts.clear();
183
+ }
184
+ // ─── Protected Helpers ──────────────────────────────────────────────────
185
+ /**
186
+ * Retrieve a session entry from the context map.
187
+ *
188
+ * Subclasses use this in their {@link getOrCreate} implementation to
189
+ * check for an existing session before creating a new one.
190
+ *
191
+ * @param contextId - The A2A contextId to look up.
192
+ * @returns The session entry, or `undefined` if no session exists
193
+ * for this contextId.
194
+ */
195
+ getSessionEntry(contextId) {
196
+ return this.contextMap.get(contextId);
197
+ }
198
+ /**
199
+ * Store or update a session entry in the context map.
200
+ *
201
+ * Subclasses use this in their {@link getOrCreate} implementation to
202
+ * register a newly created session or update an existing entry.
203
+ *
204
+ * @param contextId - The A2A contextId to associate with the entry.
205
+ * @param entry - The session entry to store.
206
+ */
207
+ setSessionEntry(contextId, entry) {
208
+ this.contextMap.set(contextId, entry);
209
+ }
210
+ /**
211
+ * Remove a session entry from the context map.
212
+ *
213
+ * Subclasses use this when a session is destroyed or invalidated
214
+ * (e.g. backend reports the session no longer exists).
215
+ *
216
+ * @param contextId - The A2A contextId whose entry should be removed.
217
+ */
218
+ deleteSessionEntry(contextId) {
219
+ this.contextMap.delete(contextId);
220
+ }
221
+ }
222
+ //# sourceMappingURL=base-session-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base-session-manager.js","sourceRoot":"","sources":["../../src/session/base-session-manager.ts"],"names":[],"mappings":"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"}