langgraph-agent-common 2.0.0__py3-none-any.whl

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 (31) hide show
  1. langgraph_a2a_api/__init__.py +0 -0
  2. langgraph_a2a_api/adaptor/__init__.py +0 -0
  3. langgraph_a2a_api/adaptor/agent_executor.py +421 -0
  4. langgraph_a2a_api/adaptor/conversion.py +167 -0
  5. langgraph_a2a_api/adaptor/sanitize_messages_for_llm.py +34 -0
  6. langgraph_a2a_api/fastapi_application.py +152 -0
  7. langgraph_a2a_api/langgraph/__init__.py +0 -0
  8. langgraph_a2a_api/langgraph/checkpointer.py +102 -0
  9. langgraph_a2a_api/langgraph/graph.py +49 -0
  10. langgraph_a2a_api/langgraph/store.py +64 -0
  11. langgraph_a2a_api/langgraph/validator.py +42 -0
  12. langgraph_a2a_api/resources/openapi_schema.json +277 -0
  13. langgraph_a2a_api/resources/static/css/style.css +480 -0
  14. langgraph_a2a_api/resources/static/index.html +107 -0
  15. langgraph_a2a_api/resources/static/js/app.js +896 -0
  16. langgraph_a2a_api/server.py +149 -0
  17. langgraph_a2a_api/tasks/__init__.py +0 -0
  18. langgraph_a2a_api/tasks/push_notification_config_store.py +24 -0
  19. langgraph_a2a_api/tasks/task_store.py +66 -0
  20. langgraph_a2a_api/utils/__init__.py +0 -0
  21. langgraph_a2a_api/utils/lock.py +31 -0
  22. langgraph_a2a_api/utils/types.py +3 -0
  23. langgraph_agent_common-2.0.0.dist-info/METADATA +67 -0
  24. langgraph_agent_common-2.0.0.dist-info/RECORD +31 -0
  25. langgraph_agent_common-2.0.0.dist-info/WHEEL +5 -0
  26. langgraph_agent_common-2.0.0.dist-info/top_level.txt +2 -0
  27. langgraph_aux/__init__.py +0 -0
  28. langgraph_aux/callbacks/__init__.py +11 -0
  29. langgraph_aux/callbacks/talos_callback_handler.py +340 -0
  30. langgraph_aux/utils/__init__.py +0 -0
  31. langgraph_aux/utils/stream_output.py +13 -0
@@ -0,0 +1,896 @@
1
+ (function() {
2
+ 'use strict';
3
+
4
+ const state = {
5
+ contexts: {},
6
+ currentContextId: null,
7
+ currentTaskId: null,
8
+ isSending: false,
9
+ hitlState: null,
10
+ selectedHitlType: null,
11
+ abortController: null,
12
+ };
13
+
14
+ const elements = {};
15
+
16
+ function init() {
17
+ cacheElements();
18
+ bindEvents();
19
+ setApiBaseUrl();
20
+ createNewContext();
21
+ loadState();
22
+ }
23
+
24
+ function cacheElements() {
25
+ elements.contextSelect = document.getElementById('contextSelect');
26
+ elements.newContextBtn = document.getElementById('newContextBtn');
27
+ elements.contextIdInput = document.getElementById('contextIdInput');
28
+ elements.taskIdInput = document.getElementById('taskIdInput');
29
+ elements.messageMetaInput = document.getElementById('messageMetaInput');
30
+ elements.taskMetaInput = document.getElementById('taskMetaInput');
31
+ elements.configInput = document.getElementById('configInput');
32
+ elements.validateMetaBtn = document.getElementById('validateMetaBtn');
33
+ elements.validateTaskMetaBtn = document.getElementById('validateTaskMetaBtn');
34
+ elements.validateConfigBtn = document.getElementById('validateConfigBtn');
35
+ elements.metaStatus = document.getElementById('metaStatus');
36
+ elements.taskMetaStatus = document.getElementById('taskMetaStatus');
37
+ elements.configStatus = document.getElementById('configStatus');
38
+ elements.apiBaseUrl = document.getElementById('apiBaseUrl');
39
+ elements.chatMessages = document.getElementById('chatMessages');
40
+ elements.messageInput = document.getElementById('messageInput');
41
+ elements.sendBtn = document.getElementById('sendBtn');
42
+ elements.clearChatBtn = document.getElementById('clearChatBtn');
43
+ elements.exportChatBtn = document.getElementById('exportChatBtn');
44
+ elements.hitlPanel = document.getElementById('hitlPanel');
45
+ elements.hitlPartsJson = document.getElementById('hitlPartsJson');
46
+ elements.hitlResponseForm = document.getElementById('hitlResponseForm');
47
+ elements.hitlResponseInput = document.getElementById('hitlResponseInput');
48
+ elements.hitlEditSection = document.getElementById('hitlEditSection');
49
+ elements.hitlEditInput = document.getElementById('hitlEditInput');
50
+ elements.submitHitlBtn = document.getElementById('submitHitlBtn');
51
+ elements.closeHitlBtn = document.getElementById('closeHitlBtn');
52
+ }
53
+
54
+ function bindEvents() {
55
+ elements.newContextBtn.addEventListener('click', createNewContext);
56
+ elements.contextSelect.addEventListener('change', switchContext);
57
+ elements.validateMetaBtn.addEventListener('click', () => validateJson('messageMeta'));
58
+ elements.validateTaskMetaBtn.addEventListener('click', () => validateJson('taskMeta'));
59
+ elements.validateConfigBtn.addEventListener('click', () => validateJson('config'));
60
+ elements.sendBtn.addEventListener('click', handleSendBtnClick);
61
+ elements.messageInput.addEventListener('keydown', (e) => {
62
+ if (e.key === 'Enter' && !e.shiftKey) {
63
+ e.preventDefault();
64
+ handleSendBtnClick();
65
+ }
66
+ });
67
+ elements.clearChatBtn.addEventListener('click', clearChat);
68
+ elements.exportChatBtn.addEventListener('click', exportChat);
69
+ elements.closeHitlBtn.addEventListener('click', () => elements.hitlPanel.classList.add('hidden'));
70
+
71
+ document.querySelectorAll('.hitl-btn').forEach(btn => {
72
+ btn.addEventListener('click', () => selectHitlType(btn.dataset.type));
73
+ });
74
+ elements.submitHitlBtn.addEventListener('click', submitHitlResponse);
75
+ }
76
+
77
+ function setApiBaseUrl() {
78
+ const protocol = window.location.protocol;
79
+ const host = window.location.host;
80
+ const basePath = window.location.pathname.replace(/\/chat\/?$/, '');
81
+ elements.apiBaseUrl.value = `${protocol}//${host}${basePath}`;
82
+ }
83
+
84
+ function getBaseUrl() {
85
+ return elements.apiBaseUrl.value.replace(/\/$/, '');
86
+ }
87
+
88
+ function generateId() {
89
+ return crypto.randomUUID ? crypto.randomUUID() : 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
90
+ const r = Math.random() * 16 | 0;
91
+ return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
92
+ });
93
+ }
94
+
95
+ function createNewContext() {
96
+ const contextId = generateId();
97
+ state.contexts[contextId] = {
98
+ id: contextId,
99
+ taskId: null,
100
+ messages: [],
101
+ createdAt: new Date().toISOString(),
102
+ };
103
+ state.currentContextId = contextId;
104
+ state.currentTaskId = null;
105
+ updateContextSelector();
106
+ updateContextDisplay();
107
+ clearChat();
108
+ }
109
+
110
+ function switchContext() {
111
+ const contextId = elements.contextSelect.value;
112
+ if (contextId && state.contexts[contextId]) {
113
+ state.currentContextId = contextId;
114
+ state.currentTaskId = state.contexts[contextId].taskId;
115
+ updateContextDisplay();
116
+ renderChatHistory();
117
+ elements.hitlPanel.classList.add('hidden');
118
+ state.hitlState = null;
119
+ }
120
+ }
121
+
122
+ function updateContextSelector() {
123
+ elements.contextSelect.innerHTML = '';
124
+ Object.keys(state.contexts).sort((a, b) => {
125
+ return new Date(state.contexts[b].createdAt) - new Date(state.contexts[a].createdAt);
126
+ }).forEach(id => {
127
+ const option = document.createElement('option');
128
+ option.value = id;
129
+ const ctx = state.contexts[id];
130
+ const msgCount = ctx.messages.length;
131
+ option.textContent = `${id.slice(0, 8)}... (${msgCount} msgs)`;
132
+ if (id === state.currentContextId) {
133
+ option.selected = true;
134
+ }
135
+ elements.contextSelect.appendChild(option);
136
+ });
137
+ }
138
+
139
+ function updateContextDisplay() {
140
+ elements.contextIdInput.value = state.currentContextId || '';
141
+ elements.taskIdInput.value = state.currentTaskId || '';
142
+ }
143
+
144
+ function getCurrentContext() {
145
+ return state.contexts[state.currentContextId];
146
+ }
147
+
148
+ function validateJson(type) {
149
+ let input, statusEl;
150
+ if (type === 'messageMeta') {
151
+ input = elements.messageMetaInput.value.trim();
152
+ statusEl = elements.metaStatus;
153
+ } else if (type === 'taskMeta') {
154
+ input = elements.taskMetaInput.value.trim();
155
+ statusEl = elements.taskMetaStatus;
156
+ } else {
157
+ input = elements.configInput.value.trim();
158
+ statusEl = elements.configStatus;
159
+ }
160
+
161
+ if (!input) {
162
+ statusEl.textContent = 'empty';
163
+ statusEl.className = 'status-indicator valid';
164
+ return true;
165
+ }
166
+
167
+ try {
168
+ JSON.parse(input);
169
+ statusEl.textContent = 'valid';
170
+ statusEl.className = 'status-indicator valid';
171
+ return true;
172
+ } catch (e) {
173
+ statusEl.textContent = 'invalid';
174
+ statusEl.className = 'status-indicator invalid';
175
+ return false;
176
+ }
177
+ }
178
+
179
+ function getJsonInput(type) {
180
+ let input;
181
+ if (type === 'messageMeta') {
182
+ input = elements.messageMetaInput.value.trim();
183
+ } else if (type === 'taskMeta') {
184
+ input = elements.taskMetaInput.value.trim();
185
+ } else {
186
+ input = elements.configInput.value.trim();
187
+ }
188
+ if (!input) return {};
189
+ try {
190
+ return JSON.parse(input);
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ function handleSendBtnClick() {
197
+ if (state.isSending || state.hitlState) {
198
+ cancelTask();
199
+ } else {
200
+ sendMessage();
201
+ }
202
+ }
203
+
204
+ function setBtnCancel(active) {
205
+ if (active) {
206
+ elements.sendBtn.textContent = 'Cancel';
207
+ elements.sendBtn.classList.add('btn-cancel');
208
+ elements.sendBtn.disabled = false;
209
+ } else {
210
+ elements.sendBtn.textContent = 'Send';
211
+ elements.sendBtn.classList.remove('btn-cancel');
212
+ elements.sendBtn.disabled = false;
213
+ }
214
+ }
215
+
216
+ async function cancelTask() {
217
+ const taskId = state.currentTaskId || state.hitlState?.id;
218
+ if (state.abortController) {
219
+ state.abortController.abort();
220
+ state.abortController = null;
221
+ }
222
+
223
+ if (taskId) {
224
+ try {
225
+ await fetch(`${getBaseUrl()}/tasks/${taskId}:cancel`, {
226
+ method: 'POST',
227
+ headers: { 'X-A2A-Protocol-Version': '1.0' },
228
+ });
229
+ } catch (e) { /* ignore */ }
230
+ }
231
+
232
+ removeLoadingMessage();
233
+ elements.hitlPanel.classList.add('hidden');
234
+ state.hitlState = null;
235
+ state.isSending = false;
236
+ state.currentTaskId = null;
237
+ const context = getCurrentContext();
238
+ if (context) context.taskId = null;
239
+ updateContextDisplay();
240
+ setBtnCancel(false);
241
+ addSystemMessage('Task canceled');
242
+ }
243
+
244
+ async function consumeSSE(response) {
245
+ const reader = response.body.getReader();
246
+ const decoder = new TextDecoder();
247
+ let buffer = '';
248
+ let lastEvent = null;
249
+
250
+ while (true) {
251
+ const { done, value } = await reader.read();
252
+ if (done) break;
253
+ buffer += decoder.decode(value, { stream: true });
254
+
255
+ const parts = buffer.split(/\r?\n\r?\n/);
256
+ buffer = parts.pop();
257
+
258
+ for (const part of parts) {
259
+ lastEvent = parseSSEPart(part, lastEvent);
260
+ }
261
+ }
262
+ // Process any remaining buffer
263
+ if (buffer.trim()) {
264
+ lastEvent = parseSSEPart(buffer, lastEvent);
265
+ }
266
+ return lastEvent;
267
+ }
268
+
269
+ function parseSSEPart(part, lastEvent) {
270
+ const lines = part.split(/\r?\n/);
271
+ let eventType = 'message';
272
+ let data = '';
273
+ for (const line of lines) {
274
+ if (line.startsWith('event:')) eventType = line.slice(6).trim();
275
+ else if (line.startsWith('data:')) data += (data ? '\n' : '') + line.slice(5).trim();
276
+ }
277
+ if (!data) return lastEvent;
278
+ try {
279
+ const event = JSON.parse(data);
280
+ if (eventType === 'error') {
281
+ addErrorMessage(`Stream error: ${JSON.stringify(event)}`);
282
+ return lastEvent;
283
+ }
284
+ // Extract taskId as early as possible for cancel support
285
+ const taskId = event.statusUpdate?.taskId || event.task?.id;
286
+ if (taskId && !state.currentTaskId) {
287
+ state.currentTaskId = taskId;
288
+ const context = getCurrentContext();
289
+ if (context) context.taskId = taskId;
290
+ }
291
+ const status = event.statusUpdate?.status || event.task?.status;
292
+ if (status?.message?.parts) {
293
+ const displayParts = status.message.parts.filter(p => !(p.data && p.data.type === 'tool_result') && !p.url && !p.raw);
294
+ const text = displayParts.map(p => p.text || (p.data && JSON.stringify(p.data, null, 2)) || '').filter(Boolean).join('\n');
295
+ if (text) updateLoadingMessage(text);
296
+ }
297
+ return event;
298
+ } catch (e) { return lastEvent; }
299
+ }
300
+
301
+ async function sendMessage() {
302
+ if (state.isSending) return;
303
+
304
+ const text = elements.messageInput.value.trim();
305
+ if (!text) return;
306
+
307
+ const messageMeta = getJsonInput('messageMeta');
308
+ if (messageMeta === null) {
309
+ addSystemMessage('Invalid message metadata JSON');
310
+ return;
311
+ }
312
+
313
+ const config = getJsonInput('config');
314
+ if (config === null) {
315
+ addSystemMessage('Invalid configuration JSON');
316
+ return;
317
+ }
318
+
319
+ state.isSending = true;
320
+ state.abortController = new AbortController();
321
+ setBtnCancel(true);
322
+ elements.messageInput.value = '';
323
+
324
+ addUserMessage(text);
325
+
326
+ const context = getCurrentContext();
327
+ const messageId = generateId();
328
+
329
+ const messagePayload = {
330
+ messageId: messageId,
331
+ contextId: state.currentContextId,
332
+ role: 'ROLE_USER',
333
+ parts: [{ text: text }],
334
+ metadata: messageMeta,
335
+ };
336
+
337
+ if (state.currentTaskId) {
338
+ messagePayload.taskId = state.currentTaskId;
339
+ }
340
+
341
+ const requestBody = {
342
+ message: messagePayload,
343
+ configuration: config,
344
+ metadata: getJsonInput('taskMeta') || {},
345
+ };
346
+
347
+ addLoadingMessage();
348
+
349
+ try {
350
+ const response = await fetch(`${getBaseUrl()}/message:stream`, {
351
+ method: 'POST',
352
+ headers: {
353
+ 'Content-Type': 'application/json',
354
+ 'X-A2A-Protocol-Version': '1.0',
355
+ },
356
+ body: JSON.stringify(requestBody),
357
+ signal: state.abortController.signal,
358
+ });
359
+
360
+ if (!response.ok) {
361
+ const errText = await response.text();
362
+ removeLoadingMessage();
363
+ throw new Error(`HTTP ${response.status}: ${errText}`);
364
+ }
365
+
366
+ const lastEvent = await consumeSSE(response);
367
+
368
+ removeLoadingMessage();
369
+
370
+ if (!lastEvent) {
371
+ throw new Error('No events received from stream');
372
+ }
373
+
374
+ handleResponse(lastEvent, text);
375
+ } catch (error) {
376
+ removeLoadingMessage();
377
+ if (error.name === 'AbortError') return;
378
+ addErrorMessage(`Request failed: ${error.message}`);
379
+ state.isSending = false;
380
+ setBtnCancel(false);
381
+ }
382
+ }
383
+
384
+ function handleResponse(data, userText) {
385
+ const context = getCurrentContext();
386
+
387
+ let task = data.task;
388
+ let message = data.message;
389
+
390
+ // Handle streaming statusUpdate format
391
+ if (!task && !message && data.statusUpdate) {
392
+ task = { id: data.statusUpdate.taskId, status: data.statusUpdate.status };
393
+ }
394
+
395
+ if (!message && task?.status?.message) {
396
+ message = task.status.message;
397
+ }
398
+
399
+ if (!task && message) {
400
+ task = {
401
+ id: message.taskId || generateId(),
402
+ contextId: message.contextId || state.currentContextId,
403
+ status: { state: 'TASK_STATE_COMPLETED', message: message },
404
+ artifacts: [],
405
+ };
406
+ }
407
+
408
+ if (!task) {
409
+ addSystemMessage('No task or message in response');
410
+ state.isSending = false;
411
+ setBtnCancel(false);
412
+ return;
413
+ }
414
+
415
+ const taskState = task.status?.state || 'TASK_STATE_UNKNOWN';
416
+ const statusMessage = task.status?.message;
417
+
418
+ if (statusMessage && statusMessage.role === 'ROLE_AGENT' && statusMessage.parts) {
419
+ addAgentMessageFromParts(statusMessage.parts);
420
+ }
421
+
422
+ addDebugPayload(task.id);
423
+
424
+ if (taskState === 'TASK_STATE_INPUT_REQUIRED') {
425
+ state.currentTaskId = task.id;
426
+ context.taskId = task.id;
427
+ showHitlPanel(task);
428
+ } else {
429
+ state.currentTaskId = null;
430
+ context.taskId = null;
431
+ elements.hitlPanel.classList.add('hidden');
432
+ state.hitlState = null;
433
+ }
434
+
435
+ updateContextDisplay();
436
+
437
+ if (taskState === 'TASK_STATE_COMPLETED') {
438
+ addSystemMessage(`Task completed: ${task.id.slice(0, 8)}`);
439
+ } else if (taskState === 'TASK_STATE_FAILED') {
440
+ addSystemMessage(`Task failed: ${task.id.slice(0, 8)}`);
441
+ }
442
+
443
+ state.isSending = false;
444
+ setBtnCancel(!!state.hitlState);
445
+ }
446
+
447
+ function showHitlPanel(task) {
448
+ state.hitlState = task;
449
+ state.selectedHitlType = null;
450
+
451
+ const parts = task.status?.message?.parts || task.artifacts?.flatMap(a => a.parts) || [];
452
+ const interruptParts = parts.filter(p => p.data && p.data.hitl_id);
453
+
454
+ state.hitlState.interruptParts = interruptParts;
455
+ state.hitlState.currentInterruptIndex = 0;
456
+
457
+ elements.hitlPartsJson.textContent = JSON.stringify(parts, null, 2);
458
+
459
+ if (interruptParts.length > 0) {
460
+ updateInterruptDisplay();
461
+ }
462
+
463
+ elements.hitlPanel.classList.remove('hidden');
464
+ elements.hitlResponseForm.classList.add('hidden');
465
+ elements.submitHitlBtn.classList.add('hidden');
466
+ elements.hitlEditSection.classList.add('hidden');
467
+
468
+ document.querySelectorAll('.hitl-btn').forEach(btn => btn.classList.remove('active'));
469
+ }
470
+
471
+ function updateInterruptDisplay() {
472
+ const interruptParts = state.hitlState.interruptParts;
473
+ const currentIndex = state.hitlState.currentInterruptIndex;
474
+
475
+ if (interruptParts.length === 0) return;
476
+
477
+ const currentPart = interruptParts[currentIndex];
478
+ const hitlId = currentPart.data.hitl_id;
479
+ const interruptValue = currentPart.data.content;
480
+
481
+ let interruptInfo = `Interrupt ${currentIndex + 1}/${interruptParts.length}\n`;
482
+ interruptInfo += `HITL ID: ${hitlId}\n\n`;
483
+ interruptInfo += JSON.stringify(interruptValue, null, 2);
484
+
485
+ elements.hitlPartsJson.textContent = interruptInfo;
486
+
487
+ const navHtml = `
488
+ <div class="hitl-nav">
489
+ <button class="hitl-nav-btn" id="prevInterruptBtn" ${currentIndex === 0 ? 'disabled' : ''}>Previous</button>
490
+ <span class="hitl-nav-info">${currentIndex + 1} of ${interruptParts.length}</span>
491
+ <button class="hitl-nav-btn" id="nextInterruptBtn" ${currentIndex === interruptParts.length - 1 ? 'disabled' : ''}>Next</button>
492
+ </div>
493
+ `;
494
+
495
+ const existingNav = elements.hitlPanel.querySelector('.hitl-nav');
496
+ if (existingNav) {
497
+ existingNav.outerHTML = navHtml;
498
+ } else {
499
+ const hitlContent = elements.hitlPanel.querySelector('.hitl-content');
500
+ hitlContent.insertAdjacentHTML('afterbegin', navHtml);
501
+ }
502
+
503
+ document.getElementById('prevInterruptBtn')?.addEventListener('click', () => {
504
+ if (state.hitlState.currentInterruptIndex > 0) {
505
+ state.hitlState.currentInterruptIndex--;
506
+ updateInterruptDisplay();
507
+ }
508
+ });
509
+
510
+ document.getElementById('nextInterruptBtn')?.addEventListener('click', () => {
511
+ if (state.hitlState.currentInterruptIndex < interruptParts.length - 1) {
512
+ state.hitlState.currentInterruptIndex++;
513
+ updateInterruptDisplay();
514
+ }
515
+ });
516
+ }
517
+
518
+ function selectHitlType(type) {
519
+ state.selectedHitlType = type;
520
+ document.querySelectorAll('.hitl-btn').forEach(btn => {
521
+ btn.classList.toggle('active', btn.dataset.type === type);
522
+ });
523
+
524
+ elements.hitlResponseForm.classList.remove('hidden');
525
+ elements.submitHitlBtn.classList.remove('hidden');
526
+
527
+ if (type === 'approve') {
528
+ elements.hitlResponseInput.value = '';
529
+ elements.hitlResponseInput.placeholder = 'Optional message for approval';
530
+ elements.hitlEditSection.classList.add('hidden');
531
+ } else if (type === 'reject') {
532
+ elements.hitlResponseInput.placeholder = 'Rejection reason...';
533
+ elements.hitlEditSection.classList.add('hidden');
534
+ } else if (type === 'respond') {
535
+ elements.hitlResponseInput.placeholder = 'Response message...';
536
+ elements.hitlEditSection.classList.add('hidden');
537
+ }
538
+ }
539
+
540
+ function submitHitlResponse() {
541
+ if (!state.selectedHitlType || !state.hitlState) return;
542
+
543
+ const message = elements.hitlResponseInput.value.trim();
544
+ const interruptParts = state.hitlState.interruptParts || [];
545
+
546
+ if (interruptParts.length === 0) {
547
+ addSystemMessage('No interrupt parts found');
548
+ return;
549
+ }
550
+
551
+ const currentIndex = state.hitlState.currentInterruptIndex || 0;
552
+ const currentPart = interruptParts[currentIndex];
553
+ const hitlId = currentPart.data.hitl_id;
554
+
555
+ let decision;
556
+
557
+ if (state.selectedHitlType === 'approve') {
558
+ decision = { type: 'approve' };
559
+ } else if (state.selectedHitlType === 'reject') {
560
+ decision = { type: 'reject' };
561
+ if (message) {
562
+ decision.message = message;
563
+ }
564
+ } else if (state.selectedHitlType === 'respond') {
565
+ if (!message) {
566
+ addSystemMessage('Response message is required');
567
+ return;
568
+ }
569
+ decision = { type: 'respond', message: message };
570
+ }
571
+
572
+ const hitlResponse = {
573
+ hitl_id: hitlId,
574
+ decision: decision
575
+ };
576
+
577
+ const messageMeta = getJsonInput('messageMeta') || {};
578
+ const config = getJsonInput('config') || {};
579
+
580
+ const taskId = state.hitlState.id;
581
+ const messageId = generateId();
582
+
583
+ const requestBody = {
584
+ message: {
585
+ messageId: messageId,
586
+ contextId: state.currentContextId,
587
+ taskId: taskId,
588
+ role: 'ROLE_USER',
589
+ parts: [{
590
+ data: hitlResponse,
591
+ }],
592
+ metadata: messageMeta,
593
+ },
594
+ configuration: config,
595
+ metadata: getJsonInput('taskMeta') || {},
596
+ };
597
+
598
+ elements.hitlPanel.classList.add('hidden');
599
+ state.hitlState = null;
600
+ state.selectedHitlType = null;
601
+
602
+ addUserMessage(`[HITL Response] HITL ID: ${hitlId}, ${JSON.stringify(decision)}`);
603
+ addLoadingMessage();
604
+
605
+ state.isSending = true;
606
+ state.abortController = new AbortController();
607
+ setBtnCancel(true);
608
+
609
+ (async () => {
610
+ try {
611
+ const response = await fetch(`${getBaseUrl()}/message:stream`, {
612
+ method: 'POST',
613
+ headers: {
614
+ 'Content-Type': 'application/json',
615
+ 'X-A2A-Protocol-Version': '1.0',
616
+ },
617
+ body: JSON.stringify(requestBody),
618
+ signal: state.abortController.signal,
619
+ });
620
+
621
+ if (!response.ok) {
622
+ const errText = await response.text();
623
+ removeLoadingMessage();
624
+ throw new Error(`HTTP ${response.status}: ${errText}`);
625
+ }
626
+
627
+ const lastEvent = await consumeSSE(response);
628
+ removeLoadingMessage();
629
+
630
+ if (!lastEvent) {
631
+ throw new Error('No events received from stream');
632
+ }
633
+
634
+ handleResponse(lastEvent, '[HITL Response]');
635
+ } catch (error) {
636
+ removeLoadingMessage();
637
+ if (error.name === 'AbortError') return;
638
+ addErrorMessage(`HITL request failed: ${error.message}`);
639
+ state.isSending = false;
640
+ setBtnCancel(false);
641
+ }
642
+ })();
643
+ }
644
+
645
+ function addUserMessage(text) {
646
+ const context = getCurrentContext();
647
+ context.messages.push({ role: 'user', content: text, timestamp: new Date().toISOString() });
648
+ appendMessageToChat('user', text);
649
+ updateContextSelector();
650
+ }
651
+
652
+ function addAgentMessageFromParts(parts) {
653
+ const context = getCurrentContext();
654
+ let displayText = '';
655
+ let hasNonText = false;
656
+
657
+ parts.forEach(part => {
658
+ if (part.text) {
659
+ displayText += part.text;
660
+ } else if (part.data) {
661
+ hasNonText = true;
662
+ displayText += '\n[Data]: ' + JSON.stringify(part.data, null, 2);
663
+ } else if (part.raw) {
664
+ hasNonText = true;
665
+ displayText += '\n[Raw data]';
666
+ } else if (part.url) {
667
+ hasNonText = true;
668
+ displayText += '\n[URL]: ' + part.url;
669
+ }
670
+ });
671
+
672
+ if (!displayText) {
673
+ displayText = JSON.stringify(parts, null, 2);
674
+ hasNonText = true;
675
+ }
676
+
677
+ context.messages.push({ role: 'agent', content: displayText, parts: parts, timestamp: new Date().toISOString() });
678
+ appendMessageToChat('agent', displayText, hasNonText, parts);
679
+ updateContextSelector();
680
+ }
681
+
682
+ function addSystemMessage(text) {
683
+ const context = getCurrentContext();
684
+ context.messages.push({ role: 'system', content: text, timestamp: new Date().toISOString() });
685
+ appendMessageToChat('system', text);
686
+ }
687
+
688
+ function addErrorMessage(text) {
689
+ appendMessageToChat('error', text);
690
+ }
691
+
692
+ function addLoadingMessage() {
693
+ const div = document.createElement('div');
694
+ div.className = 'message agent loading';
695
+ div.id = 'loadingMsg';
696
+ div.innerHTML = '<div class="message-content typing-indicator">Thinking</div>';
697
+ elements.chatMessages.appendChild(div);
698
+ scrollToBottom();
699
+ }
700
+
701
+ function updateLoadingMessage(text) {
702
+ const loading = document.getElementById('loadingMsg');
703
+ if (!loading) return;
704
+ const content = loading.querySelector('.message-content');
705
+ if (content) {
706
+ content.classList.remove('typing-indicator');
707
+ content.textContent += (content.textContent ? '\n' : '') + text;
708
+ scrollToBottom();
709
+ }
710
+ }
711
+
712
+ function removeLoadingMessage() {
713
+ const loading = document.getElementById('loadingMsg');
714
+ if (!loading) return;
715
+ const content = loading.querySelector('.message-content');
716
+ if (content && content.textContent && !content.classList.contains('typing-indicator')) {
717
+ // Keep as persistent thinking message
718
+ loading.removeAttribute('id');
719
+ loading.classList.remove('loading');
720
+ loading.classList.add('thinking');
721
+ const header = document.createElement('div');
722
+ header.className = 'message-header';
723
+ header.textContent = `THINKING - ${new Date().toLocaleTimeString()}`;
724
+ loading.insertBefore(header, content);
725
+ } else {
726
+ loading.remove();
727
+ }
728
+ }
729
+
730
+ function appendMessageToChat(role, content, isJson = false, parts = null) {
731
+ const div = document.createElement('div');
732
+ div.className = `message ${role}`;
733
+
734
+ const header = document.createElement('div');
735
+ header.className = 'message-header';
736
+ header.textContent = `${role.toUpperCase()} - ${new Date().toLocaleTimeString()}`;
737
+ div.appendChild(header);
738
+
739
+ const contentDiv = document.createElement('div');
740
+ contentDiv.className = `message-content ${isJson ? 'json-content' : ''}`;
741
+ contentDiv.textContent = content;
742
+ div.appendChild(contentDiv);
743
+
744
+ if (parts && parts.length > 0) {
745
+ const toggleBtn = document.createElement('button');
746
+ toggleBtn.className = 'btn-small';
747
+ toggleBtn.textContent = 'Show Parts JSON';
748
+ toggleBtn.style.marginTop = '8px';
749
+ toggleBtn.addEventListener('click', () => {
750
+ if (toggleBtn.textContent === 'Show Parts JSON') {
751
+ const jsonPre = document.createElement('pre');
752
+ jsonPre.className = 'json-display';
753
+ jsonPre.style.marginTop = '8px';
754
+ jsonPre.textContent = JSON.stringify(parts, null, 2);
755
+ div.appendChild(jsonPre);
756
+ toggleBtn.textContent = 'Hide Parts JSON';
757
+ } else {
758
+ const jsonPre = div.querySelector('.json-display');
759
+ if (jsonPre) jsonPre.remove();
760
+ toggleBtn.textContent = 'Show Parts JSON';
761
+ }
762
+ });
763
+ div.appendChild(toggleBtn);
764
+ }
765
+
766
+ elements.chatMessages.appendChild(div);
767
+ scrollToBottom();
768
+ }
769
+
770
+ function renderChatHistory() {
771
+ elements.chatMessages.innerHTML = '';
772
+ const context = getCurrentContext();
773
+ if (!context) return;
774
+
775
+ context.messages.forEach(msg => {
776
+ if (msg.role === 'agent' && msg.parts) {
777
+ appendMessageToChat('agent', msg.content, true, msg.parts);
778
+ } else {
779
+ appendMessageToChat(msg.role, msg.content);
780
+ }
781
+ });
782
+ }
783
+
784
+ function clearChat() {
785
+ const context = getCurrentContext();
786
+ if (context) {
787
+ context.messages = [];
788
+ updateContextSelector();
789
+ }
790
+ elements.chatMessages.innerHTML = '';
791
+ }
792
+
793
+ function exportChat() {
794
+ const context = getCurrentContext();
795
+ if (!context) return;
796
+
797
+ const exportData = {
798
+ contextId: state.currentContextId,
799
+ taskId: state.currentTaskId,
800
+ messages: context.messages,
801
+ exportedAt: new Date().toISOString(),
802
+ };
803
+
804
+ const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' });
805
+ const url = URL.createObjectURL(blob);
806
+ const a = document.createElement('a');
807
+ a.href = url;
808
+ a.download = `chat-${state.currentContextId.slice(0, 8)}.json`;
809
+ a.click();
810
+ URL.revokeObjectURL(url);
811
+ }
812
+
813
+ function addDebugPayload(taskId) {
814
+ if (!taskId) return;
815
+ const div = document.createElement('div');
816
+ div.className = 'message system';
817
+
818
+ const toggleBtn = document.createElement('button');
819
+ toggleBtn.className = 'btn-small';
820
+ toggleBtn.textContent = '🔍 Show Task Detail';
821
+ toggleBtn.style.opacity = '0.5';
822
+ toggleBtn.style.fontSize = '11px';
823
+
824
+ toggleBtn.addEventListener('click', async () => {
825
+ if (toggleBtn.textContent === '🔍 Show Task Detail') {
826
+ try {
827
+ const resp = await fetch(`${getBaseUrl()}/tasks/${taskId}`, {
828
+ headers: { 'X-A2A-Protocol-Version': '1.0' },
829
+ });
830
+ const data = await resp.json();
831
+ const pre = document.createElement('pre');
832
+ pre.className = 'json-display';
833
+ pre.style.marginTop = '8px';
834
+ pre.style.maxHeight = '400px';
835
+ pre.style.overflow = 'auto';
836
+ pre.textContent = JSON.stringify(data, null, 2);
837
+ div.appendChild(pre);
838
+ toggleBtn.textContent = '🔍 Hide Task Detail';
839
+ } catch (e) {
840
+ addErrorMessage(`Failed to fetch task: ${e.message}`);
841
+ }
842
+ } else {
843
+ const pre = div.querySelector('.json-display');
844
+ if (pre) pre.remove();
845
+ toggleBtn.textContent = '🔍 Show Task Detail';
846
+ }
847
+ scrollToBottom();
848
+ });
849
+
850
+ div.appendChild(toggleBtn);
851
+ elements.chatMessages.appendChild(div);
852
+ scrollToBottom();
853
+ }
854
+
855
+ function scrollToBottom() {
856
+ elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight;
857
+ }
858
+
859
+ function loadState() {
860
+ try {
861
+ const saved = localStorage.getItem('a2a-chat-state');
862
+ if (saved) {
863
+ const parsed = JSON.parse(saved);
864
+ if (parsed.contexts && Object.keys(parsed.contexts).length > 0) {
865
+ state.contexts = parsed.contexts;
866
+ state.currentContextId = parsed.currentContextId || Object.keys(state.contexts)[0];
867
+ state.currentTaskId = state.contexts[state.currentContextId]?.taskId || null;
868
+ updateContextSelector();
869
+ updateContextDisplay();
870
+ renderChatHistory();
871
+ }
872
+ }
873
+ } catch (e) {
874
+ console.warn('Failed to load saved state:', e);
875
+ }
876
+ }
877
+
878
+ function saveState() {
879
+ try {
880
+ localStorage.setItem('a2a-chat-state', JSON.stringify({
881
+ contexts: state.contexts,
882
+ currentContextId: state.currentContextId,
883
+ }));
884
+ } catch (e) {
885
+ console.warn('Failed to save state:', e);
886
+ }
887
+ }
888
+
889
+ setInterval(saveState, 5000);
890
+
891
+ if (document.readyState === 'loading') {
892
+ document.addEventListener('DOMContentLoaded', init);
893
+ } else {
894
+ init();
895
+ }
896
+ })();