@ynhcj/xiaoyi-channel 0.0.1-beta

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 (60) hide show
  1. package/dist/index.d.ts +16 -0
  2. package/dist/index.js +21 -0
  3. package/dist/src/bot.d.ts +17 -0
  4. package/dist/src/bot.js +260 -0
  5. package/dist/src/channel.d.ts +6 -0
  6. package/dist/src/channel.js +87 -0
  7. package/dist/src/client.d.ts +35 -0
  8. package/dist/src/client.js +147 -0
  9. package/dist/src/config-schema.d.ts +54 -0
  10. package/dist/src/config-schema.js +55 -0
  11. package/dist/src/config.d.ts +17 -0
  12. package/dist/src/config.js +45 -0
  13. package/dist/src/file-download.d.ts +17 -0
  14. package/dist/src/file-download.js +53 -0
  15. package/dist/src/file-upload.d.ts +23 -0
  16. package/dist/src/file-upload.js +129 -0
  17. package/dist/src/formatter.d.ts +77 -0
  18. package/dist/src/formatter.js +252 -0
  19. package/dist/src/heartbeat.d.ts +39 -0
  20. package/dist/src/heartbeat.js +102 -0
  21. package/dist/src/monitor.d.ts +17 -0
  22. package/dist/src/monitor.js +191 -0
  23. package/dist/src/onboarding.d.ts +6 -0
  24. package/dist/src/onboarding.js +173 -0
  25. package/dist/src/outbound.d.ts +6 -0
  26. package/dist/src/outbound.js +208 -0
  27. package/dist/src/parser.d.ts +49 -0
  28. package/dist/src/parser.js +99 -0
  29. package/dist/src/push.d.ts +23 -0
  30. package/dist/src/push.js +146 -0
  31. package/dist/src/reply-dispatcher.d.ts +15 -0
  32. package/dist/src/reply-dispatcher.js +160 -0
  33. package/dist/src/runtime.d.ts +11 -0
  34. package/dist/src/runtime.js +18 -0
  35. package/dist/src/tools/calendar-tool.d.ts +6 -0
  36. package/dist/src/tools/calendar-tool.js +167 -0
  37. package/dist/src/tools/location-tool.d.ts +5 -0
  38. package/dist/src/tools/location-tool.js +136 -0
  39. package/dist/src/tools/note-tool.d.ts +5 -0
  40. package/dist/src/tools/note-tool.js +130 -0
  41. package/dist/src/tools/search-note-tool.d.ts +5 -0
  42. package/dist/src/tools/search-note-tool.js +130 -0
  43. package/dist/src/tools/session-manager.d.ts +29 -0
  44. package/dist/src/tools/session-manager.js +74 -0
  45. package/dist/src/tools/tool-context.d.ts +16 -0
  46. package/dist/src/tools/tool-context.js +7 -0
  47. package/dist/src/types.d.ts +163 -0
  48. package/dist/src/types.js +2 -0
  49. package/dist/src/utils/config-manager.d.ts +26 -0
  50. package/dist/src/utils/config-manager.js +56 -0
  51. package/dist/src/utils/crypto.d.ts +8 -0
  52. package/dist/src/utils/crypto.js +14 -0
  53. package/dist/src/utils/logger.d.ts +6 -0
  54. package/dist/src/utils/logger.js +34 -0
  55. package/dist/src/utils/session.d.ts +34 -0
  56. package/dist/src/utils/session.js +50 -0
  57. package/dist/src/websocket.d.ts +123 -0
  58. package/dist/src/websocket.js +547 -0
  59. package/openclaw.plugin.json +10 -0
  60. package/package.json +71 -0
@@ -0,0 +1,55 @@
1
+ // JSON Schema definition for XY channel configuration
2
+ export const xyConfigSchema = {
3
+ type: "object",
4
+ properties: {
5
+ enabled: {
6
+ type: "boolean",
7
+ description: "Enable/disable the XY channel",
8
+ default: false,
9
+ },
10
+ wsUrl1: {
11
+ type: "string",
12
+ description: "Primary WebSocket URL",
13
+ default: "ws://localhost:8765/ws/link",
14
+ },
15
+ wsUrl2: {
16
+ type: "string",
17
+ description: "Secondary WebSocket URL",
18
+ default: "ws://localhost:8768/ws/link",
19
+ },
20
+ apiKey: {
21
+ type: "string",
22
+ description: "API key for authentication",
23
+ },
24
+ uid: {
25
+ type: "string",
26
+ description: "User ID for file upload",
27
+ },
28
+ agentId: {
29
+ type: "string",
30
+ description: "Agent ID for this bot instance",
31
+ },
32
+ apiId: {
33
+ type: "string",
34
+ description: "API ID for push messages",
35
+ },
36
+ pushId: {
37
+ type: "string",
38
+ description: "Push ID for push messages",
39
+ },
40
+ fileUploadUrl: {
41
+ type: "string",
42
+ description: "Base URL for file upload service",
43
+ default: "http://localhost:8767",
44
+ },
45
+ pushUrl: {
46
+ type: "string",
47
+ description: "URL for push message service",
48
+ },
49
+ defaultSessionId: {
50
+ type: "string",
51
+ description: "Default session ID for push notifications (used when no target is specified, e.g., in cron jobs)",
52
+ },
53
+ },
54
+ required: ["apiKey", "agentId", "uid", "apiId", "pushId"],
55
+ };
@@ -0,0 +1,17 @@
1
+ import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2
+ import type { XYChannelConfig } from "./types.js";
3
+ /**
4
+ * Resolve and validate Xiaoyi channel configuration from ClawdbotConfig.
5
+ * Simplified version - only supports single account (no multi-account management).
6
+ */
7
+ export declare function resolveXYConfig(cfg: ClawdbotConfig): XYChannelConfig;
8
+ /**
9
+ * List available account IDs.
10
+ * Simplified - always returns single account "default".
11
+ */
12
+ export declare function listXYAccountIds(): string[];
13
+ /**
14
+ * Get default account ID.
15
+ * Simplified - always returns "default".
16
+ */
17
+ export declare function getDefaultXYAccountId(): string;
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Resolve and validate Xiaoyi channel configuration from ClawdbotConfig.
3
+ * Simplified version - only supports single account (no multi-account management).
4
+ */
5
+ export function resolveXYConfig(cfg) {
6
+ const xyConfig = cfg.channels?.["xiaoyi-channel"];
7
+ if (!xyConfig) {
8
+ throw new Error("Xiaoyi channel configuration not found in openclaw.json");
9
+ }
10
+ // Validate required fields
11
+ const required = ["apiKey", "agentId", "uid", "apiId", "pushId"];
12
+ for (const field of required) {
13
+ if (!xyConfig[field]) {
14
+ throw new Error(`XY channel configuration missing required field: ${field}`);
15
+ }
16
+ }
17
+ // Return configuration with defaults
18
+ return {
19
+ enabled: xyConfig.enabled ?? false,
20
+ wsUrl1: xyConfig.wsUrl1 ?? "ws://localhost:8765/ws/link",
21
+ wsUrl2: xyConfig.wsUrl2 ?? "ws://localhost:8768/ws/link",
22
+ apiKey: xyConfig.apiKey,
23
+ uid: xyConfig.uid,
24
+ agentId: xyConfig.agentId,
25
+ apiId: xyConfig.apiId,
26
+ pushId: xyConfig.pushId,
27
+ fileUploadUrl: xyConfig.fileUploadUrl ?? "http://localhost:8767",
28
+ pushUrl: xyConfig.pushUrl,
29
+ defaultSessionId: xyConfig.defaultSessionId,
30
+ };
31
+ }
32
+ /**
33
+ * List available account IDs.
34
+ * Simplified - always returns single account "default".
35
+ */
36
+ export function listXYAccountIds() {
37
+ return ["default"];
38
+ }
39
+ /**
40
+ * Get default account ID.
41
+ * Simplified - always returns "default".
42
+ */
43
+ export function getDefaultXYAccountId() {
44
+ return "default";
45
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Download a file from URL to local path.
3
+ */
4
+ export declare function downloadFile(url: string, destPath: string): Promise<void>;
5
+ /**
6
+ * Download files from A2A file parts.
7
+ * Returns array of local file paths.
8
+ */
9
+ export declare function downloadFilesFromParts(fileParts: Array<{
10
+ name: string;
11
+ mimeType: string;
12
+ uri: string;
13
+ }>, tempDir?: string): Promise<Array<{
14
+ path: string;
15
+ name: string;
16
+ mimeType: string;
17
+ }>>;
@@ -0,0 +1,53 @@
1
+ // File download utilities
2
+ import fetch from "node-fetch";
3
+ import fs from "fs/promises";
4
+ import path from "path";
5
+ import { logger } from "./utils/logger.js";
6
+ /**
7
+ * Download a file from URL to local path.
8
+ */
9
+ export async function downloadFile(url, destPath) {
10
+ logger.debug(`Downloading file from ${url} to ${destPath}`);
11
+ try {
12
+ const response = await fetch(url);
13
+ if (!response.ok) {
14
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
15
+ }
16
+ const arrayBuffer = await response.arrayBuffer();
17
+ const buffer = Buffer.from(arrayBuffer);
18
+ await fs.writeFile(destPath, buffer);
19
+ logger.debug(`File downloaded successfully: ${destPath}`);
20
+ }
21
+ catch (error) {
22
+ logger.error(`Failed to download file from ${url}:`, error);
23
+ throw error;
24
+ }
25
+ }
26
+ /**
27
+ * Download files from A2A file parts.
28
+ * Returns array of local file paths.
29
+ */
30
+ export async function downloadFilesFromParts(fileParts, tempDir = "/tmp/xy_channel") {
31
+ // Create temp directory if it doesn't exist
32
+ await fs.mkdir(tempDir, { recursive: true });
33
+ const downloadedFiles = [];
34
+ for (const filePart of fileParts) {
35
+ const { name, mimeType, uri } = filePart;
36
+ // Generate safe file name
37
+ const safeName = name.replace(/[^a-zA-Z0-9._-]/g, "_");
38
+ const destPath = path.join(tempDir, `${Date.now()}_${safeName}`);
39
+ try {
40
+ await downloadFile(uri, destPath);
41
+ downloadedFiles.push({
42
+ path: destPath,
43
+ name,
44
+ mimeType,
45
+ });
46
+ }
47
+ catch (error) {
48
+ logger.error(`Failed to download file ${name}:`, error);
49
+ // Continue with other files
50
+ }
51
+ }
52
+ return downloadedFiles;
53
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Service for uploading files to XY file storage.
3
+ * Implements three-phase upload: prepare → upload → complete.
4
+ */
5
+ export declare class XYFileUploadService {
6
+ private baseUrl;
7
+ private apiKey;
8
+ private uid;
9
+ constructor(baseUrl: string, apiKey: string, uid: string);
10
+ /**
11
+ * Upload a file using the three-phase process.
12
+ * Returns the objectId (as fileId) for use in A2A messages.
13
+ */
14
+ uploadFile(filePath: string, objectType?: string): Promise<string>;
15
+ /**
16
+ * Upload multiple files and return their file IDs.
17
+ */
18
+ uploadFiles(filePaths: string[], objectType?: string): Promise<Array<{
19
+ filePath: string;
20
+ fileId: string;
21
+ fileName: string;
22
+ }>>;
23
+ }
@@ -0,0 +1,129 @@
1
+ // Three-phase file upload service
2
+ // OSMS file upload implementation
3
+ import fetch from "node-fetch";
4
+ import fs from "fs/promises";
5
+ import path from "path";
6
+ import { calculateSHA256 } from "./utils/crypto.js";
7
+ /**
8
+ * Service for uploading files to XY file storage.
9
+ * Implements three-phase upload: prepare → upload → complete.
10
+ */
11
+ export class XYFileUploadService {
12
+ baseUrl;
13
+ apiKey;
14
+ uid;
15
+ constructor(baseUrl, apiKey, uid) {
16
+ this.baseUrl = baseUrl;
17
+ this.apiKey = apiKey;
18
+ this.uid = uid;
19
+ }
20
+ /**
21
+ * Upload a file using the three-phase process.
22
+ * Returns the objectId (as fileId) for use in A2A messages.
23
+ */
24
+ async uploadFile(filePath, objectType = "TEMPORARY_MATERIAL_DOC") {
25
+ console.log(`[XY File Upload] Starting file upload: ${filePath}`);
26
+ try {
27
+ // Read file
28
+ const fileBuffer = await fs.readFile(filePath);
29
+ const fileName = path.basename(filePath);
30
+ const fileSha256 = calculateSHA256(fileBuffer);
31
+ const fileSize = fileBuffer.length;
32
+ // Phase 1: Prepare
33
+ console.log(`[XY File Upload] Phase 1: Prepare upload for ${fileName}`);
34
+ const prepareResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/prepare`, {
35
+ method: "POST",
36
+ headers: {
37
+ "Content-Type": "application/json",
38
+ "x-uid": this.uid,
39
+ "x-api-key": this.apiKey,
40
+ "x-request-from": "openclaw",
41
+ },
42
+ body: JSON.stringify({
43
+ objectType,
44
+ fileName,
45
+ fileSha256,
46
+ fileSize,
47
+ fileOwnerInfo: {
48
+ uid: this.uid,
49
+ teamId: this.uid,
50
+ },
51
+ useEdge: false,
52
+ }),
53
+ });
54
+ if (!prepareResp.ok) {
55
+ throw new Error(`Prepare failed: HTTP ${prepareResp.status}`);
56
+ }
57
+ const prepareData = await prepareResp.json();
58
+ console.log(`[XY File Upload] Prepare response:`, JSON.stringify(prepareData, null, 2));
59
+ if (prepareData.code !== "0") {
60
+ throw new Error(`Prepare failed: ${prepareData.desc}`);
61
+ }
62
+ const { objectId, draftId, uploadInfos } = prepareData;
63
+ console.log(`[XY File Upload] Prepare complete: objectId=${objectId}, draftId=${draftId}`);
64
+ // Phase 2: Upload
65
+ console.log(`[XY File Upload] Phase 2: Upload file data`);
66
+ const uploadInfo = uploadInfos[0]; // Single-part upload
67
+ const uploadResp = await fetch(uploadInfo.url, {
68
+ method: uploadInfo.method,
69
+ headers: uploadInfo.headers,
70
+ body: fileBuffer,
71
+ });
72
+ console.log(`[XY File Upload] Upload response status: ${uploadResp.status}, url: ${uploadInfo.url}`);
73
+ console.log(`[XY File Upload] Upload response headers:`, JSON.stringify(Object.fromEntries(uploadResp.headers.entries()), null, 2));
74
+ if (!uploadResp.ok) {
75
+ const uploadErrorText = await uploadResp.text();
76
+ console.log(`[XY File Upload] Upload error response:`, uploadErrorText);
77
+ throw new Error(`Upload failed: HTTP ${uploadResp.status}`);
78
+ }
79
+ console.log(`[XY File Upload] Upload complete`);
80
+ // Phase 3: Complete
81
+ console.log(`[XY File Upload] Phase 3: Complete upload`);
82
+ const completeResp = await fetch(`${this.baseUrl}/osms/v1/file/manager/complete`, {
83
+ method: "POST",
84
+ headers: {
85
+ "Content-Type": "application/json",
86
+ "x-uid": this.uid,
87
+ "x-api-key": this.apiKey,
88
+ "x-request-from": "openclaw",
89
+ },
90
+ body: JSON.stringify({
91
+ objectId,
92
+ draftId,
93
+ }),
94
+ });
95
+ if (!completeResp.ok) {
96
+ throw new Error(`Complete failed: HTTP ${completeResp.status}`);
97
+ }
98
+ const completeData = await completeResp.json();
99
+ console.log(`[XY File Upload] Complete response:`, JSON.stringify(completeData, null, 2));
100
+ console.log(`[XY File Upload] File upload successful: ${fileName} → objectId=${objectId}`);
101
+ return objectId;
102
+ }
103
+ catch (error) {
104
+ console.error(`[XY File Upload] File upload failed for ${filePath}:`, error);
105
+ return "";
106
+ }
107
+ }
108
+ /**
109
+ * Upload multiple files and return their file IDs.
110
+ */
111
+ async uploadFiles(filePaths, objectType = "TEMPORARY_MATERIAL_DOC") {
112
+ const results = [];
113
+ for (const filePath of filePaths) {
114
+ try {
115
+ const fileId = await this.uploadFile(filePath, objectType);
116
+ results.push({
117
+ filePath,
118
+ fileId,
119
+ fileName: path.basename(filePath),
120
+ });
121
+ }
122
+ catch (error) {
123
+ console.error(`[XY File Upload] Failed to upload ${filePath}, skipping:`, error);
124
+ // Continue with other files
125
+ }
126
+ }
127
+ return results;
128
+ }
129
+ }
@@ -0,0 +1,77 @@
1
+ import type { XYChannelConfig, A2ACommand } from "./types.js";
2
+ /**
3
+ * Parameters for sending an A2A response.
4
+ */
5
+ export interface SendA2AResponseParams {
6
+ config: XYChannelConfig;
7
+ sessionId: string;
8
+ taskId: string;
9
+ messageId: string;
10
+ text?: string;
11
+ append: boolean;
12
+ final: boolean;
13
+ files?: Array<{
14
+ fileName: string;
15
+ fileType: string;
16
+ fileId: string;
17
+ }>;
18
+ }
19
+ /**
20
+ * Send an A2A artifact update response.
21
+ */
22
+ export declare function sendA2AResponse(params: SendA2AResponseParams): Promise<void>;
23
+ /**
24
+ * Parameters for sending a status update.
25
+ */
26
+ export interface SendStatusUpdateParams {
27
+ config: XYChannelConfig;
28
+ sessionId: string;
29
+ taskId: string;
30
+ messageId: string;
31
+ text: string;
32
+ state: "submitted" | "working" | "input-required" | "completed" | "canceled" | "failed" | "unknown";
33
+ }
34
+ /**
35
+ * Send an A2A task status update.
36
+ * Follows A2A protocol standard format with nested status object.
37
+ */
38
+ export declare function sendStatusUpdate(params: SendStatusUpdateParams): Promise<void>;
39
+ /**
40
+ * Parameters for sending a command.
41
+ */
42
+ export interface SendCommandParams {
43
+ config: XYChannelConfig;
44
+ sessionId: string;
45
+ taskId: string;
46
+ messageId: string;
47
+ command: A2ACommand;
48
+ }
49
+ /**
50
+ * Send a command as an artifact update (final=false).
51
+ */
52
+ export declare function sendCommand(params: SendCommandParams): Promise<void>;
53
+ /**
54
+ * Parameters for sending a clearContext response.
55
+ */
56
+ export interface SendClearContextResponseParams {
57
+ config: XYChannelConfig;
58
+ sessionId: string;
59
+ messageId: string;
60
+ }
61
+ /**
62
+ * Send a clearContext response.
63
+ */
64
+ export declare function sendClearContextResponse(params: SendClearContextResponseParams): Promise<void>;
65
+ /**
66
+ * Parameters for sending a tasks/cancel response.
67
+ */
68
+ export interface SendTasksCancelResponseParams {
69
+ config: XYChannelConfig;
70
+ sessionId: string;
71
+ taskId: string;
72
+ messageId: string;
73
+ }
74
+ /**
75
+ * Send a tasks/cancel response.
76
+ */
77
+ export declare function sendTasksCancelResponse(params: SendTasksCancelResponseParams): Promise<void>;
@@ -0,0 +1,252 @@
1
+ // OpenClaw → A2A format conversion
2
+ import { v4 as uuidv4 } from "uuid";
3
+ import { getXYWebSocketManager } from "./client.js";
4
+ import { getXYRuntime } from "./runtime.js";
5
+ /**
6
+ * Send an A2A artifact update response.
7
+ */
8
+ export async function sendA2AResponse(params) {
9
+ const { config, sessionId, taskId, messageId, text, append, final, files } = params;
10
+ const runtime = getXYRuntime();
11
+ const log = runtime?.log ?? console.log;
12
+ const error = runtime?.error ?? console.error;
13
+ // Build artifact update event
14
+ const artifact = {
15
+ taskId,
16
+ kind: "artifact-update",
17
+ append,
18
+ lastChunk: true,
19
+ final,
20
+ artifact: {
21
+ artifactId: uuidv4(),
22
+ parts: [],
23
+ },
24
+ };
25
+ // Add text part (even if empty string, to maintain parts structure)
26
+ if (text !== undefined) {
27
+ artifact.artifact.parts.push({
28
+ kind: "text",
29
+ text,
30
+ });
31
+ }
32
+ // Add file parts if provided
33
+ if (files && files.length > 0) {
34
+ artifact.artifact.parts.push({
35
+ kind: "data",
36
+ data: { fileInfo: files },
37
+ });
38
+ }
39
+ // Build JSON-RPC response
40
+ const jsonRpcResponse = {
41
+ jsonrpc: "2.0",
42
+ id: messageId,
43
+ result: artifact,
44
+ };
45
+ // Send via WebSocket
46
+ const wsManager = getXYWebSocketManager(config);
47
+ const outboundMessage = {
48
+ msgType: "agent_response",
49
+ agentId: config.agentId,
50
+ sessionId,
51
+ taskId,
52
+ msgDetail: JSON.stringify(jsonRpcResponse),
53
+ };
54
+ // 📋 Log complete response body
55
+ log(`[A2A_RESPONSE] 📤 Sending A2A artifact-update response:`);
56
+ log(`[A2A_RESPONSE] - sessionId: ${sessionId}`);
57
+ log(`[A2A_RESPONSE] - taskId: ${taskId}`);
58
+ log(`[A2A_RESPONSE] - messageId: ${messageId}`);
59
+ log(`[A2A_RESPONSE] - append: ${append}`);
60
+ log(`[A2A_RESPONSE] - final: ${final}`);
61
+ log(`[A2A_RESPONSE] - text length: ${text?.length ?? 0}`);
62
+ log(`[A2A_RESPONSE] - files count: ${files?.length ?? 0}`);
63
+ log(`[A2A_RESPONSE] 📦 Complete outbound message:`);
64
+ log(JSON.stringify(outboundMessage, null, 2));
65
+ log(`[A2A_RESPONSE] 📦 JSON-RPC response body:`);
66
+ log(JSON.stringify(jsonRpcResponse, null, 2));
67
+ await wsManager.sendMessage(sessionId, outboundMessage);
68
+ log(`[A2A_RESPONSE] ✅ Message sent successfully`);
69
+ }
70
+ /**
71
+ * Send an A2A task status update.
72
+ * Follows A2A protocol standard format with nested status object.
73
+ */
74
+ export async function sendStatusUpdate(params) {
75
+ const { config, sessionId, taskId, messageId, text, state } = params;
76
+ const runtime = getXYRuntime();
77
+ const log = runtime?.log ?? console.log;
78
+ const error = runtime?.error ?? console.error;
79
+ // Build status update event following A2A protocol standard
80
+ const statusUpdate = {
81
+ taskId,
82
+ kind: "status-update",
83
+ final: false, // Status updates should not end the stream
84
+ status: {
85
+ message: {
86
+ role: "agent",
87
+ parts: [
88
+ {
89
+ kind: "text",
90
+ text,
91
+ },
92
+ ],
93
+ },
94
+ state,
95
+ },
96
+ };
97
+ // Build JSON-RPC response
98
+ const jsonRpcResponse = {
99
+ jsonrpc: "2.0",
100
+ id: messageId,
101
+ result: statusUpdate,
102
+ };
103
+ // Send via WebSocket
104
+ const wsManager = getXYWebSocketManager(config);
105
+ const outboundMessage = {
106
+ msgType: "agent_response",
107
+ agentId: config.agentId,
108
+ sessionId,
109
+ taskId,
110
+ msgDetail: JSON.stringify(jsonRpcResponse),
111
+ };
112
+ // 📋 Log complete response body
113
+ log(`[A2A_STATUS] 📤 Sending A2A status-update:`);
114
+ log(`[A2A_STATUS] - sessionId: ${sessionId}`);
115
+ log(`[A2A_STATUS] - taskId: ${taskId}`);
116
+ log(`[A2A_STATUS] - messageId: ${messageId}`);
117
+ log(`[A2A_STATUS] - state: ${state}`);
118
+ log(`[A2A_STATUS] - text: "${text}"`);
119
+ log(`[A2A_STATUS] 📦 Complete outbound message:`);
120
+ log(JSON.stringify(outboundMessage, null, 2));
121
+ log(`[A2A_STATUS] 📦 JSON-RPC response body:`);
122
+ log(JSON.stringify(jsonRpcResponse, null, 2));
123
+ await wsManager.sendMessage(sessionId, outboundMessage);
124
+ log(`[A2A_STATUS] ✅ Status update sent successfully`);
125
+ }
126
+ /**
127
+ * Send a command as an artifact update (final=false).
128
+ */
129
+ export async function sendCommand(params) {
130
+ const { config, sessionId, taskId, messageId, command } = params;
131
+ const runtime = getXYRuntime();
132
+ const log = runtime?.log ?? console.log;
133
+ const error = runtime?.error ?? console.error;
134
+ // Build artifact update with command as data
135
+ // Wrap command in commands array as per protocol requirement
136
+ const artifact = {
137
+ taskId,
138
+ kind: "artifact-update",
139
+ append: false,
140
+ lastChunk: true,
141
+ final: false, // Commands are not final
142
+ artifact: {
143
+ artifactId: uuidv4(),
144
+ parts: [
145
+ {
146
+ kind: "data",
147
+ data: {
148
+ commands: [command],
149
+ },
150
+ },
151
+ ],
152
+ },
153
+ };
154
+ // Build JSON-RPC response
155
+ const jsonRpcResponse = {
156
+ jsonrpc: "2.0",
157
+ id: messageId,
158
+ result: artifact,
159
+ };
160
+ // Send via WebSocket
161
+ const wsManager = getXYWebSocketManager(config);
162
+ const outboundMessage = {
163
+ msgType: "agent_response",
164
+ agentId: config.agentId,
165
+ sessionId,
166
+ taskId,
167
+ msgDetail: JSON.stringify(jsonRpcResponse),
168
+ };
169
+ // 📋 Log complete response body
170
+ log(`[A2A_COMMAND] 📤 Sending A2A command:`);
171
+ log(`[A2A_COMMAND] - sessionId: ${sessionId}`);
172
+ log(`[A2A_COMMAND] - taskId: ${taskId}`);
173
+ log(`[A2A_COMMAND] - messageId: ${messageId}`);
174
+ log(`[A2A_COMMAND] - command: ${command.header.namespace}::${command.header.name}`);
175
+ log(`[A2A_COMMAND] 📦 Complete outbound message:`);
176
+ log(JSON.stringify(outboundMessage, null, 2));
177
+ log(`[A2A_COMMAND] 📦 JSON-RPC response body:`);
178
+ log(JSON.stringify(jsonRpcResponse, null, 2));
179
+ await wsManager.sendMessage(sessionId, outboundMessage);
180
+ log(`[A2A_COMMAND] ✅ Command sent successfully`);
181
+ }
182
+ /**
183
+ * Send a clearContext response.
184
+ */
185
+ export async function sendClearContextResponse(params) {
186
+ const { config, sessionId, messageId } = params;
187
+ const runtime = getXYRuntime();
188
+ const log = runtime?.log ?? console.log;
189
+ const error = runtime?.error ?? console.error;
190
+ // Build JSON-RPC response for clearContext
191
+ const jsonRpcResponse = {
192
+ jsonrpc: "2.0",
193
+ id: messageId,
194
+ result: {
195
+ status: {
196
+ state: "cleared",
197
+ },
198
+ },
199
+ error: {
200
+ code: 0,
201
+ // Note: Using any to bypass type check as the response format differs from standard A2A types
202
+ message: "",
203
+ },
204
+ };
205
+ // Send via WebSocket
206
+ const wsManager = getXYWebSocketManager(config);
207
+ const outboundMessage = {
208
+ msgType: "agent_response",
209
+ agentId: config.agentId,
210
+ sessionId,
211
+ taskId: sessionId, // Use sessionId as taskId for clearContext
212
+ msgDetail: JSON.stringify(jsonRpcResponse),
213
+ };
214
+ await wsManager.sendMessage(sessionId, outboundMessage);
215
+ log(`Sent clearContext response: sessionId=${sessionId}`);
216
+ }
217
+ /**
218
+ * Send a tasks/cancel response.
219
+ */
220
+ export async function sendTasksCancelResponse(params) {
221
+ const { config, sessionId, taskId, messageId } = params;
222
+ const runtime = getXYRuntime();
223
+ const log = runtime?.log ?? console.log;
224
+ const error = runtime?.error ?? console.error;
225
+ // Build JSON-RPC response for tasks/cancel
226
+ // Note: Using any to bypass type check as the response format differs from standard A2A types
227
+ const jsonRpcResponse = {
228
+ jsonrpc: "2.0",
229
+ id: messageId,
230
+ result: {
231
+ id: taskId,
232
+ status: {
233
+ state: "canceled",
234
+ },
235
+ },
236
+ error: {
237
+ code: 0,
238
+ message: "",
239
+ },
240
+ };
241
+ // Send via WebSocket
242
+ const wsManager = getXYWebSocketManager(config);
243
+ const outboundMessage = {
244
+ msgType: "agent_response",
245
+ agentId: config.agentId,
246
+ sessionId,
247
+ taskId,
248
+ msgDetail: JSON.stringify(jsonRpcResponse),
249
+ };
250
+ await wsManager.sendMessage(sessionId, outboundMessage);
251
+ log(`Sent tasks/cancel response: sessionId=${sessionId}, taskId=${taskId}`);
252
+ }