Skip to content

Commit 34bd049

Browse files
committed
feat: enhance A2A client with task polling and JSON-RPC request handling
- Added polling functionality to monitor task completion in a2aClient.js. - Updated constructor to accept options for polling settings. - Improved artifact handling in App.js to extract content from response artifacts. - Modified MessageItem.js to display actual JSON-RPC requests for user messages. - Adjusted web security settings in electron.js to allow cross-origin requests.
1 parent 6ba4db7 commit 34bd049

File tree

4 files changed

+116
-11
lines changed

4 files changed

+116
-11
lines changed

public/electron.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,10 +150,16 @@ function createWindow() {
150150
nodeIntegration: true,
151151
contextIsolation: false,
152152
enableRemoteModule: true,
153+
webSecurity: false, // Disable web security to bypass CORS
153154
},
154155
show: false, // Don't show until ready-to-show event
155156
});
156157

158+
// Disable the same-origin policy to allow cross-origin requests
159+
mainWindow.webContents.session.webRequest.onBeforeSendHeaders((details, callback) => {
160+
callback({ requestHeaders: { ...details.requestHeaders } });
161+
});
162+
157163
mainWindow.loadURL(
158164
isDev
159165
? 'http://localhost:12121'

src/App.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -321,10 +321,24 @@ function App() {
321321
if (response.result.artifacts && Array.isArray(response.result.artifacts)) {
322322
artifacts = response.result.artifacts;
323323
log('info', 'Found artifacts', artifacts);
324+
325+
// Try to extract content from result artifact
326+
for (const artifact of response.result.artifacts) {
327+
if (artifact.name === 'result' && artifact.parts && Array.isArray(artifact.parts)) {
328+
for (const part of artifact.parts) {
329+
if (part.type === 'text' && part.text) {
330+
log('info', 'Found text in artifacts.result.parts', part.text);
331+
content = part.text;
332+
break;
333+
}
334+
}
335+
}
336+
}
324337
}
325338

326339
// Path for agents using status.message.parts format
327-
if (response.result.status && response.result.status.message &&
340+
if (content === 'No response content found' &&
341+
response.result.status && response.result.status.message &&
328342
response.result.status.message.parts && response.result.status.message.parts.length > 0) {
329343
for (const part of response.result.status.message.parts) {
330344
if (part.type === 'text' && part.text) {
@@ -537,7 +551,9 @@ function App() {
537551
const updated = [...prev[selectedAgent.id]];
538552
const idx = updated.findIndex(msg => msg.id === userMessageId);
539553
if (idx !== -1) {
540-
updated[idx] = { ...updated[idx], rawResponse: response.jsonRpcRequest };
554+
// Make sure we're getting the actual JSON-RPC request object
555+
const jsonRpcPayload = response.jsonRpcRequest || requestObject;
556+
updated[idx] = { ...updated[idx], rawResponse: jsonRpcPayload };
541557
}
542558
return { ...prev, [selectedAgent.id]: updated };
543559
});
@@ -581,7 +597,9 @@ function App() {
581597
const updated = [...prev[selectedAgent.id]];
582598
const idx = updated.findIndex(msg => msg.id === userMessageId);
583599
if (idx !== -1) {
584-
updated[idx] = { ...updated[idx], rawResponse: response.jsonRpcRequest };
600+
// Make sure we're getting the actual JSON-RPC request object
601+
const jsonRpcPayload = response.jsonRpcRequest || requestObject;
602+
updated[idx] = { ...updated[idx], rawResponse: jsonRpcPayload };
585603
}
586604
return { ...prev, [selectedAgent.id]: updated };
587605
});

src/components/MessageItem.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,16 @@ function MessageItem({ message }) {
6868
};
6969

7070
const stateInfo = getStateInfo();
71-
// Use the raw response data instead of simplified object
71+
72+
// Make sure we display the actual JSON-RPC request for user messages
7273
const rawResponseData = message.rawResponse || message;
74+
75+
// Check if this is a user message with a real JSON-RPC request
76+
const hasJsonRpcRequest = isUser &&
77+
message.rawResponse &&
78+
message.rawResponse.jsonrpc === '2.0' &&
79+
message.rawResponse.method &&
80+
message.rawResponse.params;
7381

7482
// Render an artifact
7583
const renderArtifact = (artifact, index) => {
@@ -187,10 +195,10 @@ function MessageItem({ message }) {
187195
<Box sx={{ ml: 1 }}>
188196
<JsonViewer
189197
data={rawResponseData}
190-
buttonLabel="Raw"
198+
buttonLabel={hasJsonRpcRequest ? "RPC" : "Raw"}
191199
buttonSize="small"
192200
buttonVariant="text"
193-
buttonColor="primary"
201+
buttonColor={hasJsonRpcRequest ? "success" : "primary"}
194202
buttonIcon={<DataObjectIcon fontSize="small" />}
195203
/>
196204
</Box>
@@ -243,4 +251,4 @@ function MessageItem({ message }) {
243251
);
244252
}
245253

246-
export default MessageItem;
254+
export default MessageItem;

src/services/a2aClient.js

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ import { v4 as uuidv4 } from 'uuid';
44
* A2A Client service for communicating with AI agents
55
*/
66
class A2AClient {
7-
constructor(agentUrl) {
7+
constructor(agentUrl, options = {}) {
88
this.agentUrl = agentUrl;
99
this.headers = {
10-
'Content-Type': 'application/json'
10+
'Content-Type': 'application/json',
11+
'Origin': window.location.origin,
12+
'Access-Control-Allow-Origin': '*',
1113
};
14+
// Polling settings with defaults
15+
this.pollInterval = options.pollInterval || 1000; // 1 second by default
16+
this.maxPollAttempts = options.maxPollAttempts || 30; // 30 attempts max (30 seconds)
1217
}
1318

1419
/**
@@ -93,6 +98,56 @@ class A2AClient {
9398
return request;
9499
}
95100

101+
/**
102+
* Poll a task until it completes or fails
103+
* @param {string} taskId The task ID to poll
104+
* @param {string} sessionId The session ID
105+
* @returns {Promise<Object>} The final task result
106+
*/
107+
async pollTaskUntilComplete(taskId, sessionId) {
108+
this._log('info', 'Starting task polling', { taskId, sessionId });
109+
110+
let attempts = 0;
111+
let taskState = null;
112+
113+
while (attempts < this.maxPollAttempts) {
114+
attempts++;
115+
116+
try {
117+
const result = await this.getTask(taskId);
118+
119+
if (!result || !result.result) {
120+
this._log('error', 'Invalid task response', result);
121+
throw new Error('Invalid task response structure');
122+
}
123+
124+
const status = result.result.status;
125+
taskState = status && status.state ? status.state : null;
126+
127+
this._log('info', `Task poll attempt ${attempts}`, { taskId, state: taskState });
128+
129+
// Terminal states
130+
if (taskState === 'completed' || taskState === 'failed' || taskState === 'canceled') {
131+
this._log('info', 'Task polling complete', { taskId, finalState: taskState });
132+
return {
133+
taskId,
134+
sessionId,
135+
...result
136+
};
137+
}
138+
139+
// Wait before polling again
140+
await new Promise(resolve => setTimeout(resolve, this.pollInterval));
141+
} catch (error) {
142+
this._log('error', 'Error polling task', { taskId, error: error.message });
143+
throw error;
144+
}
145+
}
146+
147+
this._log('warn', 'Task polling timed out', { taskId, attempts });
148+
throw new Error(`Task polling timed out after ${attempts} attempts`);
149+
}
150+
96151
/**
97152
* Send a task to the agent
98153
* @param {string} message The message text
@@ -175,6 +230,24 @@ class A2AClient {
175230
taskId
176231
});
177232

233+
// Check if the task is in a "submitted" state and needs polling
234+
if (result && result.result && result.result.status && result.result.status.state === 'submitted') {
235+
this._log('info', 'Task submitted, starting polling', { taskId });
236+
237+
// Store the original request before polling
238+
const originalRequest = jsonRpcRequest;
239+
240+
// Poll until completion
241+
const pollingResult = await this.pollTaskUntilComplete(taskId, actualSessionId);
242+
243+
// Return the result with the original request attached
244+
return {
245+
...pollingResult,
246+
jsonRpcRequest: originalRequest
247+
};
248+
}
249+
250+
// If not in submitted state, return the result directly
178251
return {
179252
taskId,
180253
sessionId: actualSessionId,
@@ -240,7 +313,7 @@ class A2AClient {
240313
};
241314

242315
// Create the JSON-RPC request
243-
const jsonRpcRequest = this._createJsonRpcRequest('send_task_streaming', params);
316+
const jsonRpcRequest = this._createJsonRpcRequest('tasks/send', params);
244317

245318
try {
246319
// Log the actual JSON-RPC payload being sent
@@ -416,7 +489,7 @@ class A2AClient {
416489
};
417490

418491
// Create the JSON-RPC request
419-
const jsonRpcRequest = this._createJsonRpcRequest('get_task', params);
492+
const jsonRpcRequest = this._createJsonRpcRequest('tasks/get', params);
420493

421494
try {
422495
const response = await fetch(this.agentUrl, {

0 commit comments

Comments
 (0)