@botbotgo/agent-harness 0.0.223 → 0.0.225
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 +1 -1
- package/README.zh.md +1 -1
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/protocol/a2a/http.js +257 -9
- package/package.json +1 -1
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`,
|
|
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
|
|
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.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.224";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export const AGENT_HARNESS_VERSION = "0.0.224";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
2
|
import { AGENT_HARNESS_VERSION } from "../../package-version.js";
|
|
3
|
+
import { createPersistentId } from "../../utils/id.js";
|
|
3
4
|
const SUPPORTED_A2A_VERSIONS = ["1.0", "0.3"];
|
|
4
5
|
function normalizePath(value, fallback) {
|
|
5
6
|
const source = typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
|
|
@@ -134,11 +135,59 @@ function parseMessageSendParams(params) {
|
|
|
134
135
|
const invocation = metadata && typeof metadata === "object" && !Array.isArray(metadata)
|
|
135
136
|
? metadata
|
|
136
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);
|
|
137
144
|
return {
|
|
138
145
|
input: textParts.join("\n\n"),
|
|
139
146
|
...(agentId ? { agentId } : {}),
|
|
140
147
|
...(sessionId ? { sessionId } : {}),
|
|
141
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 } : {}),
|
|
142
191
|
};
|
|
143
192
|
}
|
|
144
193
|
function attachServiceParameters(invocation, serviceParameters) {
|
|
@@ -199,6 +248,54 @@ function parseTaskListParams(params) {
|
|
|
199
248
|
limit: limitValue,
|
|
200
249
|
};
|
|
201
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
|
+
}
|
|
202
299
|
function mapRunState(state) {
|
|
203
300
|
switch (state) {
|
|
204
301
|
case "queued":
|
|
@@ -449,7 +546,7 @@ function buildAgentCard(runtime, options) {
|
|
|
449
546
|
],
|
|
450
547
|
capabilities: {
|
|
451
548
|
streaming: true,
|
|
452
|
-
pushNotifications:
|
|
549
|
+
pushNotifications: true,
|
|
453
550
|
extendedAgentCard: true,
|
|
454
551
|
},
|
|
455
552
|
defaultInputModes: ["text/plain"],
|
|
@@ -541,6 +638,42 @@ function withRunResult(task, result, streamedText) {
|
|
|
541
638
|
function isTerminalTaskState(state) {
|
|
542
639
|
return state === "TASK_STATE_COMPLETED" || state === "TASK_STATE_FAILED" || state === "TASK_STATE_CANCELED";
|
|
543
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-notification-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
|
+
}
|
|
544
677
|
async function streamTaskUpdates(response, id, method, iterator) {
|
|
545
678
|
writeSseHeaders(response);
|
|
546
679
|
for await (const item of iterator) {
|
|
@@ -677,6 +810,63 @@ export async function serveA2aOverHttp(runtime, options = {}) {
|
|
|
677
810
|
...(options.provider ? { provider: options.provider } : {}),
|
|
678
811
|
...(options.defaultAgentId ? { defaultAgentId: options.defaultAgentId } : {}),
|
|
679
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
|
+
});
|
|
680
870
|
const httpServer = createServer(async (request, response) => {
|
|
681
871
|
try {
|
|
682
872
|
const requestUrl = new URL(request.url ?? "/", `http://${hostname}`);
|
|
@@ -720,6 +910,10 @@ export async function serveA2aOverHttp(runtime, options = {}) {
|
|
|
720
910
|
const requestRecord = await runtime.getRun(result.runId);
|
|
721
911
|
const approvals = await runtime.listApprovals({ threadId: result.threadId, runId: result.runId });
|
|
722
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
|
+
}
|
|
723
917
|
writeJson(response, 200, toSuccess(payload.id ?? null, toSendMessageResult(payload.method, task)));
|
|
724
918
|
return;
|
|
725
919
|
}
|
|
@@ -729,12 +923,24 @@ export async function serveA2aOverHttp(runtime, options = {}) {
|
|
|
729
923
|
return;
|
|
730
924
|
}
|
|
731
925
|
const parsed = parseMessageSendParams(payload.params);
|
|
732
|
-
|
|
926
|
+
let registeredPushTaskId;
|
|
927
|
+
const sourceIterator = streamSendMessageTaskUpdates(runtime, {
|
|
733
928
|
agentId: parsed.agentId ?? options.defaultAgentId,
|
|
734
929
|
input: parsed.input,
|
|
735
930
|
...(parsed.sessionId ? { threadId: parsed.sessionId } : {}),
|
|
736
931
|
invocation: attachServiceParameters(parsed.invocation, serviceParameters),
|
|
737
|
-
}, 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);
|
|
738
944
|
return;
|
|
739
945
|
}
|
|
740
946
|
if (payload.method === "tasks/get" || payload.method === "GetTask") {
|
|
@@ -767,6 +973,10 @@ export async function serveA2aOverHttp(runtime, options = {}) {
|
|
|
767
973
|
return;
|
|
768
974
|
}
|
|
769
975
|
if (payload.method === "SubscribeToTask") {
|
|
976
|
+
if (isTerminalTaskState(task.status.state)) {
|
|
977
|
+
writeJson(response, 200, toError(payload.id ?? null, -32004, "SubscribeToTask is not supported for tasks in terminal state."));
|
|
978
|
+
return;
|
|
979
|
+
}
|
|
770
980
|
if (!acceptsSse(request)) {
|
|
771
981
|
writeJson(response, 406, toError(payload.id ?? null, -32004, "A2A streaming subscriptions require `Accept: text/event-stream`."));
|
|
772
982
|
return;
|
|
@@ -777,18 +987,55 @@ export async function serveA2aOverHttp(runtime, options = {}) {
|
|
|
777
987
|
writeJson(response, 200, toSuccess(payload.id ?? null, {
|
|
778
988
|
task,
|
|
779
989
|
streamable: false,
|
|
780
|
-
pushNotifications:
|
|
990
|
+
pushNotifications: true,
|
|
781
991
|
nextPollAfterMs: task.status.state === "TASK_STATE_COMPLETED" || task.status.state === "TASK_STATE_FAILED" || task.status.state === "TASK_STATE_CANCELED"
|
|
782
992
|
? 0
|
|
783
993
|
: 1_000,
|
|
784
994
|
}));
|
|
785
995
|
return;
|
|
786
996
|
}
|
|
787
|
-
if (payload.method === "CreateTaskPushNotificationConfig"
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
997
|
+
if (payload.method === "CreateTaskPushNotificationConfig") {
|
|
998
|
+
const { taskId, config } = parseCreatePushNotificationConfigParams(payload.params);
|
|
999
|
+
if (!await buildTaskFromRuntime(runtime, taskId)) {
|
|
1000
|
+
writeJson(response, 200, toError(payload.id ?? null, -32001, "Task not found."));
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const created = registerPushNotificationConfig(taskId, config);
|
|
1004
|
+
await dispatchPushNotificationsForTask(taskId);
|
|
1005
|
+
writeJson(response, 200, toSuccess(payload.id ?? null, clonePushNotificationConfig(created)));
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (payload.method === "GetTaskPushNotificationConfig") {
|
|
1009
|
+
const { taskId, configId } = parsePushNotificationLocatorParams(payload.params);
|
|
1010
|
+
const config = getPushNotificationConfig(taskId, configId);
|
|
1011
|
+
if (!config) {
|
|
1012
|
+
writeJson(response, 200, toError(payload.id ?? null, -32001, "Push notification config not found."));
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
writeJson(response, 200, toSuccess(payload.id ?? null, clonePushNotificationConfig(config)));
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (payload.method === "ListTaskPushNotificationConfigs") {
|
|
1019
|
+
const { taskId, pageSize, pageToken } = parsePushNotificationListParams(payload.params);
|
|
1020
|
+
const configs = listPushNotificationConfigs(taskId);
|
|
1021
|
+
const startIndex = pageToken ? Number.parseInt(Buffer.from(pageToken, "base64url").toString("utf8"), 10) || 0 : 0;
|
|
1022
|
+
const page = configs.slice(startIndex, startIndex + pageSize).map(clonePushNotificationConfig);
|
|
1023
|
+
const nextIndex = startIndex + page.length;
|
|
1024
|
+
const nextPageToken = nextIndex < configs.length ? Buffer.from(String(nextIndex), "utf8").toString("base64url") : "";
|
|
1025
|
+
const result = {
|
|
1026
|
+
configs: page,
|
|
1027
|
+
nextPageToken,
|
|
1028
|
+
};
|
|
1029
|
+
writeJson(response, 200, toSuccess(payload.id ?? null, result));
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
if (payload.method === "DeleteTaskPushNotificationConfig") {
|
|
1033
|
+
const { taskId, configId } = parsePushNotificationLocatorParams(payload.params);
|
|
1034
|
+
if (!deletePushNotificationConfig(taskId, configId)) {
|
|
1035
|
+
writeJson(response, 200, toError(payload.id ?? null, -32001, "Push notification config not found."));
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
writeJson(response, 200, toSuccess(payload.id ?? null, { deleted: true, id: configId, taskId }));
|
|
792
1039
|
return;
|
|
793
1040
|
}
|
|
794
1041
|
if (payload.method === "GetExtendedAgentCard") {
|
|
@@ -833,6 +1080,7 @@ export async function serveA2aOverHttp(runtime, options = {}) {
|
|
|
833
1080
|
agentCardUrl: `http://${hostname}:${resolvedPort}${agentCardPath}`,
|
|
834
1081
|
completed,
|
|
835
1082
|
close: async () => {
|
|
1083
|
+
unsubscribePushNotifications();
|
|
836
1084
|
await new Promise((resolve, reject) => {
|
|
837
1085
|
httpServer.close((error) => {
|
|
838
1086
|
if (error) {
|