@botbotgo/agent-harness 0.0.222 → 0.0.224

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/README.md CHANGED
@@ -1024,7 +1024,7 @@ ACP transport notes:
1024
1024
  - `serveAcpHttp(runtime)` exposes JSON-RPC over HTTP plus SSE runtime events so remote operator surfaces can connect without importing the runtime in-process.
1025
1025
  - ACP transport validation now covers the reference-client core flow: capability discovery, request submit, session lookup, request lookup, invalid-JSON handling, notification calls without response ids, stdio JSON-RPC, and HTTP plus SSE runtime notifications.
1026
1026
  - For the thinnest editor or CLI starter, begin with `agent-harness acp serve --workspace . --transport stdio` and mirror the `examples/protocol-hello-world/app/acp-stdio-hello-world.mjs` wire shape. Applications that want an in-process reference client can use `createAcpStdioClient(...)` to issue JSON-RPC requests and route runtime notifications without hand-rolling line parsing.
1027
- - `serveA2aHttp(runtime)` exposes an A2A-compatible HTTP JSON-RPC bridge plus agent card discovery, mapping both existing methods such as `message/send` and A2A v1.0 PascalCase methods such as `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SubscribeToTask`, and `GetExtendedAgentCard` onto the existing session/request runtime surface. The bridge now advertises both `1.0` and `0.3` JSON-RPC interfaces, sets `capabilities.streaming = true`, validates `A2A-Version`, records `A2A-Extensions` into runtime invocation metadata, publishes `TASK_STATE_*` statuses plus the `{ task }` `SendMessage` wrapper, streams task snapshots over SSE for streaming methods, and still returns explicit unsupported errors for push notifications.
1027
+ - `serveA2aHttp(runtime)` exposes an A2A-compatible HTTP JSON-RPC bridge plus agent card discovery, mapping both existing methods such as `message/send` and A2A v1.0 PascalCase methods such as `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SubscribeToTask`, `GetExtendedAgentCard`, and task push-notification config methods onto the existing session/request runtime surface. The bridge now advertises both `1.0` and `0.3` JSON-RPC interfaces, sets `capabilities.streaming = true` plus `capabilities.pushNotifications = true`, validates `A2A-Version`, records `A2A-Extensions` into runtime invocation metadata, publishes `TASK_STATE_*` statuses plus the `{ task }` `SendMessage` wrapper, streams task snapshots over SSE for streaming methods, and can send best-effort webhook task snapshots for configured push notification receivers.
1028
1028
  - `serveAgUiHttp(runtime)` exposes an AG-UI-compatible HTTP SSE bridge that projects runtime lifecycle, text output, upstream thinking, step progress, and tool calls onto `RUN_*`, `TEXT_MESSAGE_*`, `THINKING_TEXT_MESSAGE_*`, `STEP_*`, and `TOOL_CALL_*` events for UI clients.
1029
1029
  - `createRuntimeMcpServer(runtime)` and `serveRuntimeMcpOverStdio(runtime)` expose the persisted runtime control surface itself as MCP tools, including sessions, requests, approvals, artifacts, events, and package export helpers.
1030
1030
  - `listRequestEvents(...)` and `exportRequestPackage(...)` are the request-first inspection helpers.
package/README.zh.md CHANGED
@@ -982,7 +982,7 @@ ACP transport 说明:
982
982
  - `serveAcpHttp(runtime)` 提供基于 HTTP 的 JSON-RPC 与 SSE runtime events,适合远程界面或独立控制面接入。
983
983
  - ACP transport 现已覆盖核心参考客户端流程验证:capability discovery、request submit、session lookup、request lookup、invalid JSON 处理、无 id notification 不返回响应,以及 stdio JSON-RPC 与 HTTP + SSE runtime notifications。
984
984
  - 如果要从最薄的一层 editor / CLI starter 开始,优先用 `agent-harness acp serve --workspace . --transport stdio`,并直接参考 `examples/protocol-hello-world/app/acp-stdio-hello-world.mjs` 的 wire shape。需要在应用内使用 reference client 时,可直接用 `createAcpStdioClient(...)` 发起 JSON-RPC 请求并分流 runtime notifications,避免每个 sidecar 自己重写 line parsing。
985
- - `serveA2aHttp(runtime)` 提供 A2A HTTP JSON-RPC bridge 与 agent card discovery,同时兼容 `message/send` 这类旧方法,以及 `SendMessage`、`SendStreamingMessage`、`GetTask`、`ListTasks`、`CancelTask`、`SubscribeToTask`、`GetExtendedAgentCard` 这类 A2A v1.0 PascalCase 方法,并统一映射到现有 session/request 运行记录。bridge 现在会同时声明 `1.0` 与 `0.3` 两个 JSON-RPC interface、把 `capabilities.streaming` 设为 `true`、校验 `A2A-Version`、把 `A2A-Extensions` 记录进 runtime invocation metadata、发布 `TASK_STATE_*` 状态与 `SendMessage` 的 `{ task }` wrapper,并在 streaming 方法上通过 SSE 输出 task snapshots;push notification 仍返回明确 unsupported error
985
+ - `serveA2aHttp(runtime)` 提供 A2A HTTP JSON-RPC bridge 与 agent card discovery,同时兼容 `message/send` 这类旧方法,以及 `SendMessage`、`SendStreamingMessage`、`GetTask`、`ListTasks`、`CancelTask`、`SubscribeToTask`、`GetExtendedAgentCard` 与 task push-notification config 这类 A2A v1.0 方法,并统一映射到现有 session/request 运行记录。bridge 现在会同时声明 `1.0` 与 `0.3` 两个 JSON-RPC interface、把 `capabilities.streaming` `capabilities.pushNotifications` 都设为 `true`、校验 `A2A-Version`、把 `A2A-Extensions` 记录进 runtime invocation metadata、发布 `TASK_STATE_*` 状态与 `SendMessage` 的 `{ task }` wrapper、在 streaming 方法上通过 SSE 输出 task snapshots,并可向已配置的 webhook receiver 发送 best-effort task push notifications
986
986
  - `serveAgUiHttp(runtime)` 提供 AG-UI HTTP SSE bridge,把 runtime 生命周期、文本输出、upstream thinking、step 进度与 tool call 投影成 `RUN_*`、`TEXT_MESSAGE_*`、`THINKING_TEXT_MESSAGE_*`、`STEP_*` 与 `TOOL_CALL_*` 事件,便于 UI 客户端直接接入。
987
987
  - `createRuntimeMcpServer(runtime)` 与 `serveRuntimeMcpOverStdio(runtime)` 会把持久化 runtime 控制面本身暴露成 MCP tools,包括 sessions、requests、approvals、artifacts、events 与 package export helpers。
988
988
  - `listRequestEvents(...)` 与 `exportRequestPackage(...)` 是 request-first 的检查 helper。
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.221";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.223";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.221";
1
+ export const AGENT_HARNESS_VERSION = "0.0.223";
@@ -1,4 +1,6 @@
1
1
  import { createServer } from "node:http";
2
+ import { AGENT_HARNESS_VERSION } from "../../package-version.js";
3
+ import { createPersistentId } from "../../utils/id.js";
2
4
  const SUPPORTED_A2A_VERSIONS = ["1.0", "0.3"];
3
5
  function normalizePath(value, fallback) {
4
6
  const source = typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
@@ -133,11 +135,59 @@ function parseMessageSendParams(params) {
133
135
  const invocation = metadata && typeof metadata === "object" && !Array.isArray(metadata)
134
136
  ? metadata
135
137
  : undefined;
138
+ const configuration = typed.configuration;
139
+ const nestedPushNotificationConfig = configuration && typeof configuration === "object" && !Array.isArray(configuration)
140
+ ? configuration.pushNotificationConfig
141
+ ?? configuration.taskPushNotificationConfig
142
+ : undefined;
143
+ const pushNotificationConfig = parsePendingPushNotificationConfig(typed.pushNotificationConfig ?? nestedPushNotificationConfig);
136
144
  return {
137
145
  input: textParts.join("\n\n"),
138
146
  ...(agentId ? { agentId } : {}),
139
147
  ...(sessionId ? { sessionId } : {}),
140
148
  ...(invocation ? { invocation } : {}),
149
+ ...(pushNotificationConfig ? { pushNotificationConfig } : {}),
150
+ };
151
+ }
152
+ function parsePushNotificationAuthenticationInfo(value) {
153
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
154
+ return undefined;
155
+ }
156
+ const typed = value;
157
+ const schemes = Array.isArray(typed.schemes)
158
+ ? typed.schemes
159
+ .filter((entry) => typeof entry === "string" && entry.trim().length > 0)
160
+ .map((entry) => entry.trim())
161
+ : [];
162
+ if (schemes.length === 0) {
163
+ return undefined;
164
+ }
165
+ return {
166
+ schemes,
167
+ ...(typeof typed.credentials === "string" && typed.credentials.trim().length > 0
168
+ ? { credentials: typed.credentials.trim() }
169
+ : {}),
170
+ };
171
+ }
172
+ function parsePendingPushNotificationConfig(value) {
173
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
174
+ return undefined;
175
+ }
176
+ const typed = value;
177
+ const rawUrl = typeof typed.url === "string" ? typed.url.trim() : "";
178
+ if (rawUrl.length === 0) {
179
+ throw new Error("A2A push notification config requires `url`.");
180
+ }
181
+ const parsedUrl = new URL(rawUrl);
182
+ if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
183
+ throw new Error("A2A push notification config requires an http or https URL.");
184
+ }
185
+ const authentication = parsePushNotificationAuthenticationInfo(typed.authentication);
186
+ return {
187
+ url: parsedUrl.toString(),
188
+ ...(typeof typed.id === "string" && typed.id.trim().length > 0 ? { id: typed.id.trim() } : {}),
189
+ ...(typeof typed.token === "string" && typed.token.trim().length > 0 ? { token: typed.token.trim() } : {}),
190
+ ...(authentication ? { authentication } : {}),
141
191
  };
142
192
  }
143
193
  function attachServiceParameters(invocation, serviceParameters) {
@@ -198,6 +248,54 @@ function parseTaskListParams(params) {
198
248
  limit: limitValue,
199
249
  };
200
250
  }
251
+ function parsePushNotificationLocatorParams(params) {
252
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
253
+ throw new Error("A2A push notification lookup requires params object.");
254
+ }
255
+ const typed = params;
256
+ const taskId = typeof typed.taskId === "string" && typed.taskId.trim().length > 0 ? typed.taskId.trim() : "";
257
+ const configId = typeof typed.id === "string" && typed.id.trim().length > 0 ? typed.id.trim() : "";
258
+ if (!taskId) {
259
+ throw new Error("A2A push notification lookup requires `taskId`.");
260
+ }
261
+ if (!configId) {
262
+ throw new Error("A2A push notification lookup requires `id`.");
263
+ }
264
+ return { taskId, configId };
265
+ }
266
+ function parsePushNotificationListParams(params) {
267
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
268
+ throw new Error("A2A push notification list requires params object.");
269
+ }
270
+ const typed = params;
271
+ const taskId = typeof typed.taskId === "string" && typed.taskId.trim().length > 0 ? typed.taskId.trim() : "";
272
+ if (!taskId) {
273
+ throw new Error("A2A push notification list requires `taskId`.");
274
+ }
275
+ const pageSize = typeof typed.pageSize === "number" && Number.isFinite(typed.pageSize)
276
+ ? Math.max(1, Math.min(100, Math.trunc(typed.pageSize)))
277
+ : 20;
278
+ return {
279
+ taskId,
280
+ pageSize,
281
+ ...(typeof typed.pageToken === "string" && typed.pageToken.trim().length > 0 ? { pageToken: typed.pageToken.trim() } : {}),
282
+ };
283
+ }
284
+ function parseCreatePushNotificationConfigParams(params) {
285
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
286
+ throw new Error("A2A push notification create requires params object.");
287
+ }
288
+ const typed = params;
289
+ const taskId = typeof typed.taskId === "string" && typed.taskId.trim().length > 0 ? typed.taskId.trim() : "";
290
+ if (!taskId) {
291
+ throw new Error("A2A push notification create requires `taskId`.");
292
+ }
293
+ const config = parsePendingPushNotificationConfig(typed.pushNotificationConfig ?? typed.config ?? typed);
294
+ if (!config) {
295
+ throw new Error("A2A push notification create requires a config object.");
296
+ }
297
+ return { taskId, config };
298
+ }
201
299
  function mapRunState(state) {
202
300
  switch (state) {
203
301
  case "queued":
@@ -429,7 +527,7 @@ function buildAgentCard(runtime, options) {
429
527
  return {
430
528
  name: options.agentName,
431
529
  description: options.agentDescription,
432
- version: "0.1.0",
530
+ version: AGENT_HARNESS_VERSION,
433
531
  // Older JSON-RPC clients may still read these top-level fields; A2A v1.0 discovery uses `supportedInterfaces`.
434
532
  protocolVersion: "1.0",
435
533
  url: options.rpcUrl,
@@ -448,7 +546,7 @@ function buildAgentCard(runtime, options) {
448
546
  ],
449
547
  capabilities: {
450
548
  streaming: true,
451
- pushNotifications: false,
549
+ pushNotifications: true,
452
550
  extendedAgentCard: true,
453
551
  },
454
552
  defaultInputModes: ["text/plain"],
@@ -540,6 +638,42 @@ function withRunResult(task, result, streamedText) {
540
638
  function isTerminalTaskState(state) {
541
639
  return state === "TASK_STATE_COMPLETED" || state === "TASK_STATE_FAILED" || state === "TASK_STATE_CANCELED";
542
640
  }
641
+ function clonePushNotificationConfig(config) {
642
+ return {
643
+ id: config.id,
644
+ taskId: config.taskId,
645
+ url: config.url,
646
+ ...(config.token ? { token: config.token } : {}),
647
+ ...(config.authentication ? { authentication: config.authentication } : {}),
648
+ };
649
+ }
650
+ function fingerprintTaskForPush(task) {
651
+ return JSON.stringify({
652
+ state: task.status.state,
653
+ timestamp: task.status.timestamp,
654
+ message: task.status.message,
655
+ });
656
+ }
657
+ async function sendPushNotification(config, task) {
658
+ const headers = {
659
+ "content-type": "application/json",
660
+ };
661
+ if (config.token) {
662
+ headers["x-a2a-push-token"] = config.token;
663
+ }
664
+ const scheme = config.authentication?.schemes[0]?.toLowerCase();
665
+ if (scheme === "bearer" && config.authentication?.credentials) {
666
+ headers.authorization = `Bearer ${config.authentication.credentials}`;
667
+ }
668
+ else if (scheme === "basic" && config.authentication?.credentials) {
669
+ headers.authorization = `Basic ${config.authentication.credentials}`;
670
+ }
671
+ await fetch(config.url, {
672
+ method: "POST",
673
+ headers,
674
+ body: JSON.stringify({ task }),
675
+ });
676
+ }
543
677
  async function streamTaskUpdates(response, id, method, iterator) {
544
678
  writeSseHeaders(response);
545
679
  for await (const item of iterator) {
@@ -676,6 +810,63 @@ export async function serveA2aOverHttp(runtime, options = {}) {
676
810
  ...(options.provider ? { provider: options.provider } : {}),
677
811
  ...(options.defaultAgentId ? { defaultAgentId: options.defaultAgentId } : {}),
678
812
  });
813
+ const pushConfigsByTask = new Map();
814
+ const registerPushNotificationConfig = (taskId, config) => {
815
+ const entry = {
816
+ id: config.id ?? `push-${createPersistentId()}`,
817
+ taskId,
818
+ url: config.url,
819
+ ...(config.token ? { token: config.token } : {}),
820
+ ...(config.authentication ? { authentication: config.authentication } : {}),
821
+ };
822
+ let taskConfigs = pushConfigsByTask.get(taskId);
823
+ if (!taskConfigs) {
824
+ taskConfigs = new Map();
825
+ pushConfigsByTask.set(taskId, taskConfigs);
826
+ }
827
+ taskConfigs.set(entry.id, entry);
828
+ return entry;
829
+ };
830
+ const listPushNotificationConfigs = (taskId) => [...(pushConfigsByTask.get(taskId)?.values() ?? [])].sort((left, right) => left.id.localeCompare(right.id));
831
+ const getPushNotificationConfig = (taskId, configId) => pushConfigsByTask.get(taskId)?.get(configId);
832
+ const deletePushNotificationConfig = (taskId, configId) => {
833
+ const taskConfigs = pushConfigsByTask.get(taskId);
834
+ if (!taskConfigs) {
835
+ return false;
836
+ }
837
+ const deleted = taskConfigs.delete(configId);
838
+ if (taskConfigs.size === 0) {
839
+ pushConfigsByTask.delete(taskId);
840
+ }
841
+ return deleted;
842
+ };
843
+ const dispatchPushNotificationsForTask = async (taskId) => {
844
+ const task = await hydrateTaskStateFromEvents(runtime, await buildTaskFromRuntime(runtime, taskId));
845
+ if (!task) {
846
+ return;
847
+ }
848
+ const fingerprint = fingerprintTaskForPush(task);
849
+ const configs = listPushNotificationConfigs(taskId);
850
+ await Promise.all(configs.map(async (config) => {
851
+ if (config.lastFingerprint === fingerprint) {
852
+ return;
853
+ }
854
+ try {
855
+ await sendPushNotification(config, task);
856
+ config.lastFingerprint = fingerprint;
857
+ }
858
+ catch {
859
+ // Push delivery is best-effort and should not fail the transport request.
860
+ }
861
+ }));
862
+ };
863
+ const pushRelevantEventTypes = new Set(["run.state.changed", "approval.requested", "approval.resolved"]);
864
+ const unsubscribePushNotifications = runtime.subscribe((event) => {
865
+ if (!pushRelevantEventTypes.has(event.eventType) || !pushConfigsByTask.has(event.runId)) {
866
+ return;
867
+ }
868
+ void dispatchPushNotificationsForTask(event.runId);
869
+ });
679
870
  const httpServer = createServer(async (request, response) => {
680
871
  try {
681
872
  const requestUrl = new URL(request.url ?? "/", `http://${hostname}`);
@@ -719,6 +910,10 @@ export async function serveA2aOverHttp(runtime, options = {}) {
719
910
  const requestRecord = await runtime.getRun(result.runId);
720
911
  const approvals = await runtime.listApprovals({ threadId: result.threadId, runId: result.runId });
721
912
  const task = buildTaskFromSessionAndRequest(toSessionRecord(session), toRequestRecord(requestRecord), approvals, result.output);
913
+ if (task && parsed.pushNotificationConfig) {
914
+ registerPushNotificationConfig(task.id, parsed.pushNotificationConfig);
915
+ await dispatchPushNotificationsForTask(task.id);
916
+ }
722
917
  writeJson(response, 200, toSuccess(payload.id ?? null, toSendMessageResult(payload.method, task)));
723
918
  return;
724
919
  }
@@ -728,12 +923,24 @@ export async function serveA2aOverHttp(runtime, options = {}) {
728
923
  return;
729
924
  }
730
925
  const parsed = parseMessageSendParams(payload.params);
731
- await streamTaskUpdates(response, payload.id ?? null, payload.method, streamSendMessageTaskUpdates(runtime, {
926
+ let registeredPushTaskId;
927
+ const sourceIterator = streamSendMessageTaskUpdates(runtime, {
732
928
  agentId: parsed.agentId ?? options.defaultAgentId,
733
929
  input: parsed.input,
734
930
  ...(parsed.sessionId ? { threadId: parsed.sessionId } : {}),
735
931
  invocation: attachServiceParameters(parsed.invocation, serviceParameters),
736
- }, payload.method));
932
+ }, payload.method);
933
+ const iterator = (async function* () {
934
+ for await (const item of sourceIterator) {
935
+ if (item.task && parsed.pushNotificationConfig && !registeredPushTaskId) {
936
+ registeredPushTaskId = item.task.id;
937
+ registerPushNotificationConfig(item.task.id, parsed.pushNotificationConfig);
938
+ await dispatchPushNotificationsForTask(item.task.id);
939
+ }
940
+ yield item;
941
+ }
942
+ }());
943
+ await streamTaskUpdates(response, payload.id ?? null, payload.method, iterator);
737
944
  return;
738
945
  }
739
946
  if (payload.method === "tasks/get" || payload.method === "GetTask") {
@@ -776,18 +983,55 @@ export async function serveA2aOverHttp(runtime, options = {}) {
776
983
  writeJson(response, 200, toSuccess(payload.id ?? null, {
777
984
  task,
778
985
  streamable: false,
779
- pushNotifications: false,
986
+ pushNotifications: true,
780
987
  nextPollAfterMs: task.status.state === "TASK_STATE_COMPLETED" || task.status.state === "TASK_STATE_FAILED" || task.status.state === "TASK_STATE_CANCELED"
781
988
  ? 0
782
989
  : 1_000,
783
990
  }));
784
991
  return;
785
992
  }
786
- if (payload.method === "CreateTaskPushNotificationConfig"
787
- || payload.method === "GetTaskPushNotificationConfig"
788
- || payload.method === "ListTaskPushNotificationConfigs"
789
- || payload.method === "DeleteTaskPushNotificationConfig") {
790
- writeJson(response, 200, toError(payload.id ?? null, -32003, "A2A push notifications are not supported by this bridge."));
993
+ if (payload.method === "CreateTaskPushNotificationConfig") {
994
+ const { taskId, config } = parseCreatePushNotificationConfigParams(payload.params);
995
+ if (!await buildTaskFromRuntime(runtime, taskId)) {
996
+ writeJson(response, 200, toError(payload.id ?? null, -32001, "Task not found."));
997
+ return;
998
+ }
999
+ const created = registerPushNotificationConfig(taskId, config);
1000
+ await dispatchPushNotificationsForTask(taskId);
1001
+ writeJson(response, 200, toSuccess(payload.id ?? null, clonePushNotificationConfig(created)));
1002
+ return;
1003
+ }
1004
+ if (payload.method === "GetTaskPushNotificationConfig") {
1005
+ const { taskId, configId } = parsePushNotificationLocatorParams(payload.params);
1006
+ const config = getPushNotificationConfig(taskId, configId);
1007
+ if (!config) {
1008
+ writeJson(response, 200, toError(payload.id ?? null, -32001, "Push notification config not found."));
1009
+ return;
1010
+ }
1011
+ writeJson(response, 200, toSuccess(payload.id ?? null, clonePushNotificationConfig(config)));
1012
+ return;
1013
+ }
1014
+ if (payload.method === "ListTaskPushNotificationConfigs") {
1015
+ const { taskId, pageSize, pageToken } = parsePushNotificationListParams(payload.params);
1016
+ const configs = listPushNotificationConfigs(taskId);
1017
+ const startIndex = pageToken ? Number.parseInt(Buffer.from(pageToken, "base64url").toString("utf8"), 10) || 0 : 0;
1018
+ const page = configs.slice(startIndex, startIndex + pageSize).map(clonePushNotificationConfig);
1019
+ const nextIndex = startIndex + page.length;
1020
+ const nextPageToken = nextIndex < configs.length ? Buffer.from(String(nextIndex), "utf8").toString("base64url") : "";
1021
+ const result = {
1022
+ configs: page,
1023
+ nextPageToken,
1024
+ };
1025
+ writeJson(response, 200, toSuccess(payload.id ?? null, result));
1026
+ return;
1027
+ }
1028
+ if (payload.method === "DeleteTaskPushNotificationConfig") {
1029
+ const { taskId, configId } = parsePushNotificationLocatorParams(payload.params);
1030
+ if (!deletePushNotificationConfig(taskId, configId)) {
1031
+ writeJson(response, 200, toError(payload.id ?? null, -32001, "Push notification config not found."));
1032
+ return;
1033
+ }
1034
+ writeJson(response, 200, toSuccess(payload.id ?? null, { deleted: true, id: configId, taskId }));
791
1035
  return;
792
1036
  }
793
1037
  if (payload.method === "GetExtendedAgentCard") {
@@ -832,6 +1076,7 @@ export async function serveA2aOverHttp(runtime, options = {}) {
832
1076
  agentCardUrl: `http://${hostname}:${resolvedPort}${agentCardPath}`,
833
1077
  completed,
834
1078
  close: async () => {
1079
+ unsubscribePushNotifications();
835
1080
  await new Promise((resolve, reject) => {
836
1081
  httpServer.close((error) => {
837
1082
  if (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.222",
3
+ "version": "0.0.224",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",