langgraph-agent-common 2.0.0__tar.gz → 2.0.2__tar.gz

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 (37) hide show
  1. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/PKG-INFO +1 -1
  2. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/pyproject.toml +1 -1
  3. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/adaptor/agent_executor.py +24 -14
  4. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/langgraph/validator.py +4 -1
  5. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/resources/static/css/style.css +137 -49
  6. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/resources/static/index.html +7 -24
  7. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/resources/static/js/app.js +212 -104
  8. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_agent_common.egg-info/PKG-INFO +1 -1
  9. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/README.md +0 -0
  10. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/setup.cfg +0 -0
  11. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/__init__.py +0 -0
  12. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/adaptor/__init__.py +0 -0
  13. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/adaptor/conversion.py +0 -0
  14. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/adaptor/sanitize_messages_for_llm.py +0 -0
  15. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/fastapi_application.py +0 -0
  16. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/langgraph/__init__.py +0 -0
  17. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/langgraph/checkpointer.py +0 -0
  18. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/langgraph/graph.py +0 -0
  19. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/langgraph/store.py +0 -0
  20. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/resources/openapi_schema.json +0 -0
  21. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/server.py +0 -0
  22. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/tasks/__init__.py +0 -0
  23. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/tasks/push_notification_config_store.py +0 -0
  24. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/tasks/task_store.py +0 -0
  25. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/utils/__init__.py +0 -0
  26. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/utils/lock.py +0 -0
  27. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_a2a_api/utils/types.py +0 -0
  28. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_agent_common.egg-info/SOURCES.txt +0 -0
  29. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_agent_common.egg-info/dependency_links.txt +0 -0
  30. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_agent_common.egg-info/requires.txt +0 -0
  31. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_agent_common.egg-info/top_level.txt +0 -0
  32. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_aux/__init__.py +0 -0
  33. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_aux/callbacks/__init__.py +0 -0
  34. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_aux/callbacks/talos_callback_handler.py +0 -0
  35. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_aux/utils/__init__.py +0 -0
  36. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/src/langgraph_aux/utils/stream_output.py +0 -0
  37. {langgraph_agent_common-2.0.0 → langgraph_agent_common-2.0.2}/test/test_deepagents_a2a_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-agent-common
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: langgraph-agent-common
5
5
  Author-email: Aegon <1006785434@qq.com>
6
6
  Project-URL: repository, https://github.com/liuxiahuiyi/langgraph-agent-common.git
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "langgraph-agent-common"
3
- version = "2.0.0"
3
+ version = "2.0.2"
4
4
  description = "langgraph-agent-common"
5
5
  readme = "README.md"
6
6
  authors = [{ name = "Aegon", email = "1006785434@qq.com" }]
@@ -4,6 +4,7 @@ from typing import Any, Union
4
4
  from pydantic import BaseModel
5
5
  import asyncio
6
6
  import uuid
7
+ from dataclasses import asdict
7
8
  from datetime import datetime, timezone
8
9
 
9
10
  from google.protobuf.json_format import MessageToDict
@@ -91,13 +92,8 @@ class GraphAgentExecutor(AgentExecutor):
91
92
  if current_interrupts:
92
93
  current_interrupt_ids = [c.id for c in current_interrupts]
93
94
  if isinstance(langgraph_message.content, list) and len(current_interrupts) == len(langgraph_message.content) and all([isinstance(part, dict) and part.get("type") == "non_standard" and validate_HITLResponse(part.get("value")) for part in langgraph_message.content]):
94
- decisions = {part.get("value", {}).get("hitl_id"): part.get("value", {}).get("decision") for part in langgraph_message.content}
95
- if set(current_interrupt_ids) == set(decisions.keys()):
96
- resume_decisions = []
97
- for hitl_id in current_interrupt_ids:
98
- resume_decisions.append(decisions[hitl_id])
99
- resume_content = {"decisions": resume_decisions}
100
- else:
95
+ resume_content = {part.get("value", {}).get("hitl_id"): {"decisions": part.get("value", {}).get("decisions")} for part in langgraph_message.content}
96
+ if set(current_interrupt_ids) != set(resume_content.keys()):
101
97
  raise ValueError(f"Interrupt resume content: {langgraph_message.content}. Must contain overall original decision ids {current_interrupt_ids}.")
102
98
  else:
103
99
  raise ValueError(f"Invalid interrupt resume content: {langgraph_message.content}. Must be a list with the same length as interrupt ids: {current_interrupt_ids}. Each item in the list must be a data with HITLResponse schema: {HITLResponseModel.schema()}.")
@@ -123,7 +119,9 @@ class GraphAgentExecutor(AgentExecutor):
123
119
  finally:
124
120
  self.run_controls.pop(task_id, None)
125
121
 
126
- async def _get_interrupts(self, state: StateSnapshot, subgraphs: bool, seen_ids: set[str] = set()) -> list[Interrupt]:
122
+ async def _get_interrupts(self, state: StateSnapshot, subgraphs: bool, seen_ids: set[str] = None) -> list[Interrupt]:
123
+ if seen_ids is None:
124
+ seen_ids = set()
127
125
  interrupts = []
128
126
  for interrupt in state.interrupts:
129
127
  if interrupt.id not in seen_ids:
@@ -175,6 +173,10 @@ class GraphAgentExecutor(AgentExecutor):
175
173
  parts.append(langgraph_part_to_a2a({"type": "non_standard", "value": part}))
176
174
  elif isinstance(part, BaseModel):
177
175
  parts.append(langgraph_part_to_a2a({"type": "non_standard", "value": part.model_dump()}))
176
+ elif hasattr(part, "DESCRIPTOR"):
177
+ parts.append(langgraph_part_to_a2a({"type": "non_standard", "value": MessageToDict(part)}))
178
+ elif hasattr(type(part), "__dataclass_fields__"):
179
+ parts.append(langgraph_part_to_a2a({"type": "non_standard", "value": asdict(part)}))
178
180
  else:
179
181
  raise ValueError(f"Unsupported type of artifact part {part}.")
180
182
  if k in appended_artifacts:
@@ -185,6 +187,10 @@ class GraphAgentExecutor(AgentExecutor):
185
187
  parts = [langgraph_part_to_a2a({"type": "non_standard", "value": v})]
186
188
  elif isinstance(v, BaseModel):
187
189
  parts = [langgraph_part_to_a2a({"type": "non_standard", "value": v.model_dump()})]
190
+ elif hasattr(v, "DESCRIPTOR"):
191
+ parts = [langgraph_part_to_a2a({"type": "non_standard", "value": MessageToDict(v)})]
192
+ elif hasattr(type(v), "__dataclass_fields__"):
193
+ parts = [langgraph_part_to_a2a({"type": "non_standard", "value": asdict(v)})]
188
194
  else:
189
195
  raise ValueError(f"Unsupported type of artifact: {k}:{v}")
190
196
 
@@ -226,11 +232,6 @@ class GraphAgentExecutor(AgentExecutor):
226
232
  yield pre_yield
227
233
  pre_yield = None
228
234
  if step == "__interrupt__":
229
- graph_status = TASK_STATE_INPUT_REQUIRED
230
- pre_yield = {
231
- "messages": [AIMessage(content=any_content_to_langgraph([{"hitl_id": interrupt.id, "content": interrupt.value} for interrupt in update]))],
232
- "graph_status": graph_status,
233
- }
234
235
  break_out = True
235
236
  else:
236
237
  messages = self._normalize_messages(update.get("messages"))
@@ -247,7 +248,16 @@ class GraphAgentExecutor(AgentExecutor):
247
248
  "graph_status": graph_status,
248
249
  }
249
250
  else:
250
- yield pre_yield
251
+ current_state_after = await self.graph_with_ck.aget_state(config, subgraphs=subgraphs)
252
+ current_interrupts_after = await self._get_interrupts(current_state_after, subgraphs)
253
+ if not current_interrupts_after:
254
+ raise RuntimeError(f"__interrupt__ step encountered but no pending interrupts found in state.")
255
+ graph_status = TASK_STATE_INPUT_REQUIRED
256
+ yield {
257
+ "messages": [AIMessage(content=any_content_to_langgraph([{"hitl_id": interrupt.id, "content": interrupt.value} for interrupt in current_interrupts_after]))],
258
+ "graph_status": graph_status,
259
+ }
260
+
251
261
 
252
262
  except GraphRecursionError as e:
253
263
  if pre_yield is not None:
@@ -27,11 +27,14 @@ class RespondDecision(BaseModel):
27
27
  message: str
28
28
 
29
29
 
30
+ Decision = ApproveDecision | EditDecision | RejectDecision | RespondDecision
31
+
32
+
30
33
  class HITLResponseModel(BaseModel):
31
34
  model_config = ConfigDict(extra="forbid")
32
35
 
33
36
  hitl_id: str
34
- decision: ApproveDecision | EditDecision | RejectDecision | RespondDecision
37
+ decisions: list[Decision]
35
38
 
36
39
 
37
40
  def validate_HITLResponse(payload: Any) -> bool:
@@ -310,68 +310,172 @@ textarea:focus {
310
310
  color: var(--warning);
311
311
  }
312
312
 
313
- .hitl-content {
313
+ .hitl-header-actions {
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 8px;
317
+ }
318
+
319
+ .hitl-progress-label {
320
+ font-size: 12px;
321
+ color: var(--text-secondary);
322
+ }
323
+
324
+ .hitl-cards-container {
314
325
  display: flex;
315
326
  flex-direction: column;
316
327
  gap: 12px;
317
328
  }
318
329
 
319
- .hitl-section h5 {
320
- font-size: 13px;
330
+ .hitl-card {
331
+ background: var(--bg-tertiary);
332
+ border: 1px solid var(--border);
333
+ border-radius: 8px;
334
+ padding: 12px;
335
+ }
336
+
337
+ .hitl-card-header {
338
+ display: flex;
339
+ align-items: center;
340
+ gap: 10px;
341
+ margin-bottom: 10px;
342
+ }
343
+
344
+ .hitl-card-title {
345
+ font-size: 14px;
346
+ font-weight: 600;
347
+ color: var(--warning);
348
+ }
349
+
350
+ .hitl-card-id {
351
+ font-size: 11px;
352
+ font-family: monospace;
321
353
  color: var(--text-secondary);
322
- margin-bottom: 8px;
354
+ background: var(--bg-primary);
355
+ padding: 2px 6px;
356
+ border-radius: 3px;
323
357
  }
324
358
 
325
- .json-display {
359
+ .hitl-card-status {
360
+ margin-left: auto;
361
+ font-size: 12px;
362
+ color: var(--text-secondary);
363
+ }
364
+
365
+ .hitl-card-status.decided {
366
+ color: var(--success);
367
+ }
368
+
369
+ .hitl-card-actions {
370
+ margin-bottom: 10px;
371
+ }
372
+
373
+ .hitl-card-actions h6 {
374
+ font-size: 12px;
375
+ color: var(--text-secondary);
376
+ margin-bottom: 6px;
377
+ }
378
+
379
+ .hitl-action-block {
380
+ padding: 10px 0;
381
+ border-bottom: 1px solid var(--border);
382
+ }
383
+
384
+ .hitl-action-block:last-child {
385
+ border-bottom: none;
386
+ }
387
+
388
+ .hitl-action-item {
326
389
  background: var(--bg-primary);
327
- padding: 12px;
328
390
  border-radius: 4px;
391
+ padding: 8px;
392
+ margin-bottom: 6px;
393
+ }
394
+
395
+ .hitl-action-name {
396
+ font-size: 13px;
397
+ font-weight: 500;
398
+ color: var(--accent);
399
+ }
400
+
401
+ .hitl-action-args {
329
402
  font-family: 'Monaco', 'Menlo', monospace;
330
- font-size: 12px;
331
- max-height: 200px;
332
- overflow: auto;
403
+ font-size: 11px;
404
+ color: var(--text-secondary);
405
+ margin-top: 4px;
333
406
  white-space: pre-wrap;
334
407
  word-break: break-word;
335
408
  }
336
409
 
337
- .hitl-decisions {
410
+ .hitl-card-decisions {
338
411
  display: flex;
339
412
  gap: 8px;
340
413
  margin-bottom: 8px;
414
+ flex-wrap: wrap;
341
415
  }
342
416
 
343
- .hitl-nav {
344
- display: flex;
345
- align-items: center;
346
- justify-content: space-between;
347
- margin-bottom: 12px;
348
- padding: 8px;
349
- background: var(--bg-primary);
350
- border-radius: 4px;
417
+ .hitl-card-form {
418
+ margin-top: 8px;
351
419
  }
352
420
 
353
- .hitl-nav-btn {
354
- padding: 6px 12px;
355
- background: var(--bg-tertiary);
421
+ .hitl-card-form.hidden {
422
+ display: none;
423
+ }
424
+
425
+ .hitl-card-input {
426
+ width: 100%;
427
+ padding: 8px;
428
+ background: var(--bg-secondary);
356
429
  border: 1px solid var(--border);
357
430
  border-radius: 4px;
358
431
  color: var(--text-primary);
359
- cursor: pointer;
432
+ font-family: 'Monaco', 'Menlo', monospace;
360
433
  font-size: 12px;
434
+ resize: vertical;
361
435
  }
362
436
 
363
- .hitl-nav-btn:hover:not(:disabled) {
364
- background: var(--border);
437
+ .hitl-card-edit {
438
+ margin-top: 8px;
365
439
  }
366
440
 
367
- .hitl-nav-btn:disabled {
368
- opacity: 0.5;
369
- cursor: not-allowed;
441
+ .hitl-card-edit.hidden {
442
+ display: none;
370
443
  }
371
444
 
372
- .hitl-nav-info {
373
- font-size: 13px;
445
+ .hitl-card-edit h6 {
446
+ font-size: 12px;
374
447
  color: var(--text-secondary);
448
+ margin-bottom: 4px;
449
+ }
450
+
451
+ .hitl-card-edit-input {
452
+ width: 100%;
453
+ padding: 8px;
454
+ background: var(--bg-secondary);
455
+ border: 1px solid var(--border);
456
+ border-radius: 4px;
457
+ color: var(--text-primary);
458
+ font-family: 'Monaco', 'Menlo', monospace;
459
+ font-size: 12px;
460
+ resize: vertical;
461
+ }
462
+
463
+ .json-display {
464
+ background: var(--bg-primary);
465
+ padding: 12px;
466
+ border-radius: 4px;
467
+ font-family: 'Monaco', 'Menlo', monospace;
468
+ font-size: 12px;
469
+ max-height: 200px;
470
+ overflow: auto;
471
+ white-space: pre-wrap;
472
+ word-break: break-word;
473
+ }
474
+
475
+ .hitl-decisions {
476
+ display: flex;
477
+ gap: 8px;
478
+ margin-bottom: 8px;
375
479
  }
376
480
 
377
481
  .hitl-btn {
@@ -395,6 +499,10 @@ textarea:focus {
395
499
  background: var(--accent);
396
500
  }
397
501
 
502
+ .hitl-btn.edit-btn {
503
+ background: var(--warning);
504
+ }
505
+
398
506
  .hitl-btn:hover {
399
507
  opacity: 0.9;
400
508
  }
@@ -403,27 +511,7 @@ textarea:focus {
403
511
  box-shadow: 0 0 0 2px white;
404
512
  }
405
513
 
406
- #hitlResponseForm {
407
- margin-top: 8px;
408
- }
409
-
410
- #hitlResponseForm.hidden {
411
- display: none;
412
- }
413
-
414
- #hitlEditSection {
415
- margin-top: 8px;
416
- }
417
-
418
- #hitlEditSection.hidden {
419
- display: none;
420
- }
421
514
 
422
- #hitlEditSection h6 {
423
- font-size: 12px;
424
- color: var(--text-secondary);
425
- margin-bottom: 4px;
426
- }
427
515
 
428
516
  .chat-input-container {
429
517
  padding: 12px 16px;
@@ -4,7 +4,7 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>A2A Chat UI</title>
7
- <link rel="stylesheet" href="static/css/style.css?v=5">
7
+ <link rel="stylesheet" href="static/css/style.css?v=6">
8
8
  </head>
9
9
  <body>
10
10
  <div class="app-container">
@@ -69,30 +69,13 @@
69
69
  <div id="hitlPanel" class="hitl-panel hidden">
70
70
  <div class="hitl-header">
71
71
  <h4>Input Required (HITL)</h4>
72
- <button id="closeHitlBtn" class="btn-small">Close</button>
73
- </div>
74
- <div class="hitl-content">
75
- <div class="hitl-section">
76
- <h5>Agent Response Parts:</h5>
77
- <pre id="hitlPartsJson" class="json-display"></pre>
78
- </div>
79
- <div class="hitl-section">
80
- <h5>HITL Response:</h5>
81
- <div class="hitl-decisions">
82
- <button class="hitl-btn approve-btn" data-type="approve">Approve</button>
83
- <button class="hitl-btn reject-btn" data-type="reject">Reject</button>
84
- <button class="hitl-btn respond-btn" data-type="respond">Respond</button>
85
- </div>
86
- <div id="hitlResponseForm" class="hidden">
87
- <textarea id="hitlResponseInput" rows="3" placeholder="Enter response message..."></textarea>
88
- <div id="hitlEditSection" class="hidden">
89
- <h6>Edited Action (JSON):</h6>
90
- <textarea id="hitlEditInput" rows="4" placeholder='{"name": "tool_name", "args": {}}'></textarea>
91
- </div>
92
- </div>
93
- <button id="submitHitlBtn" class="btn-primary hidden">Submit Response</button>
72
+ <div class="hitl-header-actions">
73
+ <span id="hitlProgressLabel" class="hitl-progress-label"></span>
74
+ <button id="submitAllHitlBtn" class="btn-primary" disabled>Submit All</button>
75
+ <button id="closeHitlBtn" class="btn-small">Close</button>
94
76
  </div>
95
77
  </div>
78
+ <div id="hitlCardsContainer" class="hitl-cards-container"></div>
96
79
  </div>
97
80
 
98
81
  <div class="chat-input-container">
@@ -102,6 +85,6 @@
102
85
  </main>
103
86
  </div>
104
87
 
105
- <script src="static/js/app.js?v=5"></script>
88
+ <script src="static/js/app.js?v=6"></script>
106
89
  </body>
107
90
  </html>
@@ -7,7 +7,7 @@
7
7
  currentTaskId: null,
8
8
  isSending: false,
9
9
  hitlState: null,
10
- selectedHitlType: null,
10
+ hitlDecisions: {},
11
11
  abortController: null,
12
12
  };
13
13
 
@@ -42,12 +42,9 @@
42
42
  elements.clearChatBtn = document.getElementById('clearChatBtn');
43
43
  elements.exportChatBtn = document.getElementById('exportChatBtn');
44
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');
45
+ elements.hitlCardsContainer = document.getElementById('hitlCardsContainer');
46
+ elements.submitAllHitlBtn = document.getElementById('submitAllHitlBtn');
47
+ elements.hitlProgressLabel = document.getElementById('hitlProgressLabel');
51
48
  elements.closeHitlBtn = document.getElementById('closeHitlBtn');
52
49
  }
53
50
 
@@ -67,11 +64,7 @@
67
64
  elements.clearChatBtn.addEventListener('click', clearChat);
68
65
  elements.exportChatBtn.addEventListener('click', exportChat);
69
66
  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);
67
+ elements.submitAllHitlBtn.addEventListener('click', submitAllHitlResponses);
75
68
  }
76
69
 
77
70
  function setApiBaseUrl() {
@@ -116,6 +109,7 @@
116
109
  renderChatHistory();
117
110
  elements.hitlPanel.classList.add('hidden');
118
111
  state.hitlState = null;
112
+ state.hitlDecisions = {};
119
113
  }
120
114
  }
121
115
 
@@ -232,6 +226,7 @@
232
226
  removeLoadingMessage();
233
227
  elements.hitlPanel.classList.add('hidden');
234
228
  state.hitlState = null;
229
+ state.hitlDecisions = {};
235
230
  state.isSending = false;
236
231
  state.currentTaskId = null;
237
232
  const context = getCurrentContext();
@@ -290,6 +285,9 @@
290
285
  }
291
286
  const status = event.statusUpdate?.status || event.task?.status;
292
287
  if (status?.message?.parts) {
288
+ if (status.message.role === 'ROLE_USER') {
289
+ return event;
290
+ }
293
291
  const displayParts = status.message.parts.filter(p => !(p.data && p.data.type === 'tool_result') && !p.url && !p.raw);
294
292
  const text = displayParts.map(p => p.text || (p.data && JSON.stringify(p.data, null, 2)) || '').filter(Boolean).join('\n');
295
293
  if (text) updateLoadingMessage(text);
@@ -430,6 +428,7 @@
430
428
  context.taskId = null;
431
429
  elements.hitlPanel.classList.add('hidden');
432
430
  state.hitlState = null;
431
+ state.hitlDecisions = {};
433
432
  }
434
433
 
435
434
  updateContextDisplay();
@@ -446,137 +445,243 @@
446
445
 
447
446
  function showHitlPanel(task) {
448
447
  state.hitlState = task;
449
- state.selectedHitlType = null;
448
+ state.hitlDecisions = {}; // Map: hitl_id -> decision object
450
449
 
451
450
  const parts = task.status?.message?.parts || task.artifacts?.flatMap(a => a.parts) || [];
452
451
  const interruptParts = parts.filter(p => p.data && p.data.hitl_id);
453
452
 
454
453
  state.hitlState.interruptParts = interruptParts;
455
- state.hitlState.currentInterruptIndex = 0;
456
454
 
457
- elements.hitlPartsJson.textContent = JSON.stringify(parts, null, 2);
455
+ elements.hitlCardsContainer.innerHTML = '';
458
456
 
459
- if (interruptParts.length > 0) {
460
- updateInterruptDisplay();
457
+ if (interruptParts.length === 0) {
458
+ // Fallback: show raw parts JSON
459
+ const fallbackCard = document.createElement('div');
460
+ fallbackCard.className = 'hitl-card';
461
+ fallbackCard.innerHTML = `
462
+ <div class="hitl-card-header"><span class="hitl-card-title">Raw Parts</span></div>
463
+ <pre class="json-display">${JSON.stringify(parts, null, 2)}</pre>
464
+ `;
465
+ elements.hitlCardsContainer.appendChild(fallbackCard);
466
+ } else {
467
+ interruptParts.forEach((part, index) => {
468
+ const card = createHitlCard(part, index);
469
+ elements.hitlCardsContainer.appendChild(card);
470
+ });
461
471
  }
462
472
 
473
+ updateHitlProgress();
463
474
  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
475
  }
470
476
 
471
- function updateInterruptDisplay() {
472
- const interruptParts = state.hitlState.interruptParts;
473
- const currentIndex = state.hitlState.currentInterruptIndex;
477
+ function createHitlCard(part, index) {
478
+ const hitlId = part.data.hitl_id;
479
+ const content = part.data.content || {};
480
+ const actionRequests = content.action_requests || [];
481
+ const reviewConfigs = content.review_configs || [];
474
482
 
475
- if (interruptParts.length === 0) return;
483
+ const card = document.createElement('div');
484
+ card.className = 'hitl-card';
485
+ card.dataset.hitlId = hitlId;
486
+ card.dataset.index = index;
476
487
 
477
- const currentPart = interruptParts[currentIndex];
478
- const hitlId = currentPart.data.hitl_id;
479
- const interruptValue = currentPart.data.content;
488
+ const header = document.createElement('div');
489
+ header.className = 'hitl-card-header';
490
+ header.innerHTML = `
491
+ <span class="hitl-card-title">HITL #${index + 1}</span>
492
+ <span class="hitl-card-id">${hitlId}</span>
493
+ <span class="hitl-card-status" id="cardStatus_${index}">⏳ 0/${actionRequests.length || 1}</span>
494
+ `;
495
+ card.appendChild(header);
480
496
 
481
- let interruptInfo = `Interrupt ${currentIndex + 1}/${interruptParts.length}\n`;
482
- interruptInfo += `HITL ID: ${hitlId}\n\n`;
483
- interruptInfo += JSON.stringify(interruptValue, null, 2);
497
+ if (actionRequests.length === 0) {
498
+ const allowedDecisions = ['approve', 'reject', 'respond'];
499
+ const actionBlock = createActionBlock(hitlId, index, 0, null, allowedDecisions);
500
+ card.appendChild(actionBlock);
501
+ } else {
502
+ actionRequests.forEach((action, actionIdx) => {
503
+ const config = reviewConfigs[actionIdx] || reviewConfigs[0] || {};
504
+ const allowedDecisions = config.allowed_decisions || ['approve', 'reject', 'respond'];
505
+ const actionBlock = createActionBlock(hitlId, index, actionIdx, action, allowedDecisions);
506
+ card.appendChild(actionBlock);
507
+ });
508
+ }
484
509
 
485
- elements.hitlPartsJson.textContent = interruptInfo;
510
+ return card;
511
+ }
486
512
 
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
- `;
513
+ function createActionBlock(hitlId, cardIndex, actionIdx, action, allowedDecisions) {
514
+ const blockId = `${cardIndex}_${actionIdx}`;
515
+ const block = document.createElement('div');
516
+ block.className = 'hitl-action-block';
494
517
 
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);
518
+ if (action) {
519
+ block.innerHTML += `
520
+ <div class="hitl-action-item">
521
+ <span class="hitl-action-name">${action.name}</span>
522
+ <pre class="hitl-action-args">${JSON.stringify(action.args, null, 2)}</pre>
523
+ </div>
524
+ `;
501
525
  }
502
526
 
503
- document.getElementById('prevInterruptBtn')?.addEventListener('click', () => {
504
- if (state.hitlState.currentInterruptIndex > 0) {
505
- state.hitlState.currentInterruptIndex--;
506
- updateInterruptDisplay();
507
- }
527
+ const decisionsDiv = document.createElement('div');
528
+ decisionsDiv.className = 'hitl-card-decisions';
529
+ allowedDecisions.forEach(type => {
530
+ const btn = document.createElement('button');
531
+ btn.className = `hitl-btn ${type}-btn`;
532
+ btn.dataset.type = type;
533
+ btn.dataset.blockId = blockId;
534
+ btn.textContent = type.charAt(0).toUpperCase() + type.slice(1);
535
+ btn.addEventListener('click', () => selectActionDecision(hitlId, cardIndex, actionIdx, type, block));
536
+ decisionsDiv.appendChild(btn);
508
537
  });
538
+ block.appendChild(decisionsDiv);
539
+
540
+ const formDiv = document.createElement('div');
541
+ formDiv.className = 'hitl-card-form hidden';
542
+ formDiv.id = `actionForm_${blockId}`;
543
+ formDiv.innerHTML = `
544
+ <textarea class="hitl-card-input" id="actionInput_${blockId}" rows="2" placeholder="Enter message..."></textarea>
545
+ <div class="hitl-card-edit hidden" id="actionEdit_${blockId}">
546
+ <h6>Edited Action (JSON):</h6>
547
+ <textarea class="hitl-card-edit-input" id="actionEditInput_${blockId}" rows="3" placeholder='{"name": "tool_name", "args": {}}'></textarea>
548
+ </div>
549
+ `;
550
+ block.appendChild(formDiv);
509
551
 
510
- document.getElementById('nextInterruptBtn')?.addEventListener('click', () => {
511
- if (state.hitlState.currentInterruptIndex < interruptParts.length - 1) {
512
- state.hitlState.currentInterruptIndex++;
513
- updateInterruptDisplay();
514
- }
515
- });
552
+ return block;
516
553
  }
517
554
 
518
- function selectHitlType(type) {
519
- state.selectedHitlType = type;
520
- document.querySelectorAll('.hitl-btn').forEach(btn => {
555
+ function selectActionDecision(hitlId, cardIndex, actionIdx, type, block) {
556
+ block.querySelectorAll('.hitl-btn').forEach(btn => {
521
557
  btn.classList.toggle('active', btn.dataset.type === type);
522
558
  });
523
559
 
524
- elements.hitlResponseForm.classList.remove('hidden');
525
- elements.submitHitlBtn.classList.remove('hidden');
560
+ const blockId = `${cardIndex}_${actionIdx}`;
561
+ const form = document.getElementById(`actionForm_${blockId}`);
562
+ const input = document.getElementById(`actionInput_${blockId}`);
563
+ const editSection = document.getElementById(`actionEdit_${blockId}`);
526
564
 
527
565
  if (type === 'approve') {
528
- elements.hitlResponseInput.value = '';
529
- elements.hitlResponseInput.placeholder = 'Optional message for approval';
530
- elements.hitlEditSection.classList.add('hidden');
566
+ form.classList.add('hidden');
567
+ editSection.classList.add('hidden');
531
568
  } else if (type === 'reject') {
532
- elements.hitlResponseInput.placeholder = 'Rejection reason...';
533
- elements.hitlEditSection.classList.add('hidden');
569
+ form.classList.remove('hidden');
570
+ input.placeholder = 'Rejection reason (optional)...';
571
+ editSection.classList.add('hidden');
534
572
  } else if (type === 'respond') {
535
- elements.hitlResponseInput.placeholder = 'Response message...';
536
- elements.hitlEditSection.classList.add('hidden');
573
+ form.classList.remove('hidden');
574
+ input.placeholder = 'Response message...';
575
+ editSection.classList.add('hidden');
576
+ } else if (type === 'edit') {
577
+ form.classList.remove('hidden');
578
+ input.placeholder = 'Edit message (optional)...';
579
+ editSection.classList.remove('hidden');
580
+ }
581
+
582
+ if (!state.hitlDecisions[hitlId]) {
583
+ state.hitlDecisions[hitlId] = {};
584
+ }
585
+ state.hitlDecisions[hitlId][actionIdx] = { type, blockId };
586
+
587
+ const interruptPart = state.hitlState.interruptParts.find(p => p.data.hitl_id === hitlId);
588
+ const totalActions = (interruptPart.data.content?.action_requests || []).length || 1;
589
+ const decidedCount = Object.keys(state.hitlDecisions[hitlId]).length;
590
+ const statusEl = document.getElementById(`cardStatus_${cardIndex}`);
591
+ if (statusEl) {
592
+ if (decidedCount >= totalActions) {
593
+ statusEl.textContent = `✓ ${decidedCount}/${totalActions}`;
594
+ statusEl.className = 'hitl-card-status decided';
595
+ } else {
596
+ statusEl.textContent = `⏳ ${decidedCount}/${totalActions}`;
597
+ statusEl.className = 'hitl-card-status';
598
+ }
537
599
  }
600
+
601
+ updateHitlProgress();
538
602
  }
539
603
 
540
- function submitHitlResponse() {
541
- if (!state.selectedHitlType || !state.hitlState) return;
604
+ function updateHitlProgress() {
605
+ const interruptParts = state.hitlState?.interruptParts || [];
606
+ let totalActions = 0;
607
+ let decidedActions = 0;
608
+ interruptParts.forEach(p => {
609
+ const actions = (p.data.content?.action_requests || []).length || 1;
610
+ totalActions += actions;
611
+ const cardDecisions = state.hitlDecisions[p.data.hitl_id];
612
+ if (cardDecisions) {
613
+ decidedActions += Object.keys(cardDecisions).length;
614
+ }
615
+ });
542
616
 
543
- const message = elements.hitlResponseInput.value.trim();
544
- const interruptParts = state.hitlState.interruptParts || [];
617
+ elements.hitlProgressLabel.textContent = `${decidedActions}/${totalActions} decided`;
618
+ elements.submitAllHitlBtn.disabled = decidedActions < totalActions;
619
+ }
545
620
 
546
- if (interruptParts.length === 0) {
547
- addSystemMessage('No interrupt parts found');
548
- return;
549
- }
621
+ function submitAllHitlResponses() {
622
+ if (!state.hitlState) return;
550
623
 
551
- const currentIndex = state.hitlState.currentInterruptIndex || 0;
552
- const currentPart = interruptParts[currentIndex];
553
- const hitlId = currentPart.data.hitl_id;
624
+ const interruptParts = state.hitlState.interruptParts || [];
554
625
 
555
- let decision;
626
+ const responseParts = interruptParts.map((part, idx) => {
627
+ const hitlId = part.data.hitl_id;
628
+ const cardDecisions = state.hitlDecisions[hitlId] || {};
629
+ const actionRequests = part.data.content?.action_requests || [];
630
+ const totalActions = actionRequests.length || 1;
631
+
632
+ const decisions = [];
633
+ for (let actionIdx = 0; actionIdx < totalActions; actionIdx++) {
634
+ const decisionInfo = cardDecisions[actionIdx];
635
+ if (!decisionInfo) {
636
+ addSystemMessage(`Decision missing for HITL #${idx + 1}, action #${actionIdx + 1}`);
637
+ return null;
638
+ }
556
639
 
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;
640
+ let decision = { type: decisionInfo.type };
641
+ const blockId = decisionInfo.blockId;
642
+ const inputEl = document.getElementById(`actionInput_${blockId}`);
643
+ const editInputEl = document.getElementById(`actionEditInput_${blockId}`);
644
+ const message = inputEl ? inputEl.value.trim() : '';
645
+ const editJson = editInputEl ? editInputEl.value.trim() : '';
646
+
647
+ if (decisionInfo.type === 'reject' && message) {
648
+ decision.message = message;
649
+ } else if (decisionInfo.type === 'respond') {
650
+ if (!message) {
651
+ addSystemMessage(`Response message is required for HITL #${idx + 1}, action #${actionIdx + 1}`);
652
+ return null;
653
+ }
654
+ decision.message = message;
655
+ } else if (decisionInfo.type === 'edit') {
656
+ if (editJson) {
657
+ try {
658
+ decision.edited_action = JSON.parse(editJson);
659
+ } catch (e) {
660
+ addSystemMessage(`Invalid JSON in edited action for HITL #${idx + 1}, action #${actionIdx + 1}`);
661
+ return null;
662
+ }
663
+ } else {
664
+ addSystemMessage(`Edited action JSON is required for HITL #${idx + 1}, action #${actionIdx + 1}`);
665
+ return null;
666
+ }
667
+ }
668
+
669
+ decisions.push(decision);
568
670
  }
569
- decision = { type: 'respond', message: message };
570
- }
571
671
 
572
- const hitlResponse = {
573
- hitl_id: hitlId,
574
- decision: decision
575
- };
672
+ return {
673
+ data: {
674
+ hitl_id: hitlId,
675
+ decisions: decisions,
676
+ }
677
+ };
678
+ });
679
+
680
+ // Check if any part failed validation
681
+ if (responseParts.some(p => p === null)) return;
576
682
 
577
683
  const messageMeta = getJsonInput('messageMeta') || {};
578
684
  const config = getJsonInput('config') || {};
579
-
580
685
  const taskId = state.hitlState.id;
581
686
  const messageId = generateId();
582
687
 
@@ -586,20 +691,23 @@
586
691
  contextId: state.currentContextId,
587
692
  taskId: taskId,
588
693
  role: 'ROLE_USER',
589
- parts: [{
590
- data: hitlResponse,
591
- }],
694
+ parts: responseParts,
592
695
  metadata: messageMeta,
593
696
  },
594
697
  configuration: config,
595
698
  metadata: getJsonInput('taskMeta') || {},
596
699
  };
597
700
 
701
+ // Clean up HITL panel
598
702
  elements.hitlPanel.classList.add('hidden');
703
+ const decisionSummary = interruptParts.map(p => {
704
+ const d = state.hitlDecisions[p.data.hitl_id];
705
+ return `${p.data.hitl_id}: ${d.type}`;
706
+ }).join(', ');
599
707
  state.hitlState = null;
600
- state.selectedHitlType = null;
708
+ state.hitlDecisions = {};
601
709
 
602
- addUserMessage(`[HITL Response] HITL ID: ${hitlId}, ${JSON.stringify(decision)}`);
710
+ addUserMessage(`[HITL Response] ${decisionSummary}`);
603
711
  addLoadingMessage();
604
712
 
605
713
  state.isSending = true;
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: langgraph-agent-common
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: langgraph-agent-common
5
5
  Author-email: Aegon <1006785434@qq.com>
6
6
  Project-URL: repository, https://github.com/liuxiahuiyi/langgraph-agent-common.git