hdsp-jupyter-extension 2.0.25__py3-none-any.whl → 2.0.27__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 (45) hide show
  1. agent_server/langchain/agent_factory.py +14 -14
  2. agent_server/langchain/agent_prompts/planner_prompt.py +17 -27
  3. agent_server/langchain/custom_middleware.py +83 -17
  4. agent_server/langchain/middleware/code_history_middleware.py +126 -37
  5. agent_server/langchain/middleware/subagent_middleware.py +24 -2
  6. agent_server/langchain/models/gpt_oss_chat.py +26 -13
  7. agent_server/langchain/prompts.py +11 -8
  8. agent_server/langchain/tools/jupyter_tools.py +43 -0
  9. agent_server/routers/langchain_agent.py +235 -23
  10. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
  11. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
  12. hdsp_jupyter_extension-2.0.25.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.ffc2b4bc8e6cb300e1e1.js → hdsp_jupyter_extension-2.0.27.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4ab73bb5068405670214.js +2 -2
  13. jupyter_ext/labextension/static/remoteEntry.ffc2b4bc8e6cb300e1e1.js.map → hdsp_jupyter_extension-2.0.27.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4ab73bb5068405670214.js.map +1 -1
  14. {hdsp_jupyter_extension-2.0.25.dist-info → hdsp_jupyter_extension-2.0.27.dist-info}/METADATA +1 -1
  15. {hdsp_jupyter_extension-2.0.25.dist-info → hdsp_jupyter_extension-2.0.27.dist-info}/RECORD +45 -45
  16. jupyter_ext/_version.py +1 -1
  17. jupyter_ext/labextension/build_log.json +1 -1
  18. jupyter_ext/labextension/package.json +2 -2
  19. jupyter_ext/labextension/static/{remoteEntry.ffc2b4bc8e6cb300e1e1.js → remoteEntry.4ab73bb5068405670214.js} +2 -2
  20. hdsp_jupyter_extension-2.0.25.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.ffc2b4bc8e6cb300e1e1.js.map → jupyter_ext/labextension/static/remoteEntry.4ab73bb5068405670214.js.map +1 -1
  21. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
  22. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  23. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js +0 -0
  24. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js.map +0 -0
  25. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js +0 -0
  26. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js.map +0 -0
  27. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js +0 -0
  28. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js.map +0 -0
  29. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js +0 -0
  30. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js.map +0 -0
  31. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
  32. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js +0 -0
  33. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js.map +0 -0
  34. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +0 -0
  35. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +0 -0
  36. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +0 -0
  37. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +0 -0
  38. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
  39. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js.map +0 -0
  40. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
  41. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
  42. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +0 -0
  43. {hdsp_jupyter_extension-2.0.25.data → hdsp_jupyter_extension-2.0.27.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -0
  44. {hdsp_jupyter_extension-2.0.25.dist-info → hdsp_jupyter_extension-2.0.27.dist-info}/WHEEL +0 -0
  45. {hdsp_jupyter_extension-2.0.25.dist-info → hdsp_jupyter_extension-2.0.27.dist-info}/licenses/LICENSE +0 -0
@@ -200,19 +200,20 @@ class ChatGPTOSS(BaseChatModel):
200
200
  tool_calls_list = []
201
201
 
202
202
  if message.tool_calls:
203
- additional_kwargs["tool_calls"] = [
204
- {
205
- "id": tc.id,
206
- "type": "function",
207
- "function": {
208
- "name": tc.function.name,
209
- "arguments": tc.function.arguments,
210
- },
211
- }
212
- for tc in message.tool_calls
213
- ]
214
- # Also convert to LangChain tool_calls format
215
- for tc in message.tool_calls:
203
+ # IMPORTANT: Only use the first tool_call to prevent parallel execution issues
204
+ # LLM sometimes generates multiple tool_calls despite prompt instructions
205
+ tool_calls_to_use = message.tool_calls[:1]
206
+ if len(message.tool_calls) > 1:
207
+ logger.warning(
208
+ "Multiple tool_calls detected (%d), using only first one: %s. Ignored: %s",
209
+ len(message.tool_calls),
210
+ message.tool_calls[0].function.name,
211
+ [tc.function.name for tc in message.tool_calls[1:]],
212
+ )
213
+
214
+ # Convert to LangChain tool_calls format only
215
+ # Do NOT put tool_calls in additional_kwargs to avoid duplicate/conflicting data
216
+ for tc in tool_calls_to_use:
216
217
  try:
217
218
  args = json.loads(tc.function.arguments)
218
219
  except json.JSONDecodeError:
@@ -327,9 +328,21 @@ class ChatGPTOSS(BaseChatModel):
327
328
  tool_call_chunks = []
328
329
 
329
330
  # Handle tool calls in streaming
331
+ # IMPORTANT: Only process the first tool_call (index 0) to prevent parallel execution
330
332
  if delta.tool_calls:
331
333
  for tc in delta.tool_calls:
332
334
  idx = tc.index
335
+ # Only accumulate the first tool_call (index 0)
336
+ if idx > 0:
337
+ if idx == 1 and tc.function and tc.function.name:
338
+ # Log warning only once when we first see index 1
339
+ logger.warning(
340
+ "Multiple tool_calls in stream, ignoring index %d: %s",
341
+ idx,
342
+ tc.function.name,
343
+ )
344
+ continue
345
+
333
346
  if idx not in tool_calls_accum:
334
347
  tool_calls_accum[idx] = {
335
348
  "id": tc.id or "",
@@ -31,12 +31,12 @@ DEFAULT_SYSTEM_PROMPT = """You are an expert Python data scientist and Jupyter n
31
31
  - **[중요] "작업 요약 및 다음 단계 제시"는 summary JSON 출력 후에만 completed 표시**
32
32
 
33
33
  # 모든 작업 완료 후 [필수]
34
- 마지막 todo "작업 요약 및 다음 단계 제시"를 completed로 변경한 후,
35
- **텍스트 응답으로** 아래 JSON을 출력하세요 (todo content가 아님!):
36
- {"summary": "완료된 작업 요약", "next_items": [{"subject": "제목", "description": "설명"}]}
34
+ 마지막 todo "작업 요약 및 다음 단계 제시"를 완료할 때:
35
+ **반드시 final_summary_tool을 호출**하여 요약과 다음 단계를 제시하세요.
36
+ - final_summary_tool(summary="완료된 작업 요약", next_items=[{"subject": "제목", "description": "설명"}, ...])
37
37
  - next_items 3개 이상 필수
38
- - **summary JSON 없이 종료 금지**
39
- - **주의**: JSON todo 항목이 아닌 일반 텍스트 응답으로 출력
38
+ - **final_summary_tool 호출 없이 종료 금지**
39
+ - **주의**: 텍스트로 JSON 출력하지 말고, 반드시 도구 호출로!
40
40
 
41
41
  # 도구 사용
42
42
  - check_resource_tool: 대용량 파일/데이터프레임 작업 전 필수
@@ -94,9 +94,9 @@ TODO_LIST_TOOL_DESCRIPTION = """Todo 리스트 관리 도구.
94
94
  - in_progress 상태는 **동시에 1개만** 허용
95
95
  - **[필수] 마지막 todo는 반드시 "작업 요약 및 다음 단계 제시"로 생성**
96
96
  - **🔴 [실행 순서]**: todo는 반드시 리스트 순서대로 실행하고, "작업 요약 및 다음 단계 제시"는 맨 마지막에 실행
97
- - 이 "작업 요약 및 다음 단계 제시" todo 에서는 전체 작업 요약과 다음 단계를 제시하는 내용을 JSON 형태로 출력:
98
- {"summary": "완료 요약", "next_items": [{"subject": "...", "description": "..."}]}
99
- (next_items 3개 이상 필수)
97
+ - 이 "작업 요약 및 다음 단계 제시" todo 완료 **반드시 final_summary_tool 호출**:
98
+ final_summary_tool(summary="완료 요약", next_items=[{"subject": "...", "description": "..."}])
99
+ (next_items 3개 이상 필수, 텍스트 JSON 출력 금지!)
100
100
  """
101
101
 
102
102
  # List of tools available to the agent
@@ -104,6 +104,7 @@ TOOL_LIST = [
104
104
  "jupyter_cell_tool",
105
105
  "markdown_tool",
106
106
  "ask_user_tool",
107
+ "final_summary_tool",
107
108
  "write_todos",
108
109
  "read_file_tool",
109
110
  "write_file_tool",
@@ -128,6 +129,8 @@ NON_HITL_TOOLS = {
128
129
  "search_notebook_cells_tool",
129
130
  "search_notebook_cells",
130
131
  "write_todos",
132
+ "final_summary_tool",
133
+ "final_summary",
131
134
  # LSP tools (read-only)
132
135
  "diagnostics_tool",
133
136
  "diagnostics",
@@ -172,9 +172,52 @@ def ask_user_tool(
172
172
  }
173
173
 
174
174
 
175
+ class NextItem(BaseModel):
176
+ """Schema for a next step item"""
177
+
178
+ subject: str = Field(description="다음 단계 제목")
179
+ description: str = Field(description="다음 단계 설명")
180
+
181
+
182
+ class FinalSummaryInput(BaseModel):
183
+ """Input schema for final_summary tool"""
184
+
185
+ summary: str = Field(description="완료된 작업에 대한 요약 (한국어)")
186
+ next_items: List[NextItem] = Field(
187
+ description="다음 단계 제안 목록 (3개 이상)", min_length=3
188
+ )
189
+
190
+
191
+ @tool(args_schema=FinalSummaryInput)
192
+ def final_summary_tool(
193
+ summary: str,
194
+ next_items: List[Dict[str, str]],
195
+ ) -> Dict[str, Any]:
196
+ """
197
+ 모든 작업이 완료된 후 최종 요약과 다음 단계를 제시하는 도구.
198
+
199
+ 이 도구는 반드시 모든 todo가 완료된 후, 마지막 "작업 요약 및 다음 단계 제시" todo를 처리할 때만 호출하세요.
200
+
201
+ Args:
202
+ summary: 완료된 작업에 대한 요약 (한국어로 작성)
203
+ next_items: 다음 단계 제안 목록 (각각 subject와 description 포함, 3개 이상)
204
+
205
+ Returns:
206
+ Dict containing the summary and next items for frontend display
207
+ """
208
+ return {
209
+ "tool": "final_summary",
210
+ "summary": summary,
211
+ "next_items": next_items,
212
+ "status": "completed",
213
+ "message": "작업 요약이 완료되었습니다.",
214
+ }
215
+
216
+
175
217
  # Export all tools
176
218
  JUPYTER_TOOLS = [
177
219
  jupyter_cell_tool,
178
220
  markdown_tool,
179
221
  ask_user_tool,
222
+ final_summary_tool,
180
223
  ]
@@ -695,6 +695,19 @@ async def stream_agent(request: AgentRequest):
695
695
  thread_id,
696
696
  )
697
697
 
698
+ # Handle @reset command
699
+ if "@reset" in request.request:
700
+ logger.info(f"@reset command detected for thread: {thread_id}")
701
+ from agent_server.langchain.middleware.code_history_middleware import (
702
+ clear_code_history,
703
+ )
704
+
705
+ # Clear code history and checkpointer for this thread
706
+ clear_code_history(thread_id)
707
+ if thread_id in _simple_agent_checkpointers:
708
+ del _simple_agent_checkpointers[thread_id]
709
+ logger.info(f"Session reset complete for thread: {thread_id}")
710
+
698
711
  async def event_generator():
699
712
  try:
700
713
  # Use simple agent with HITL
@@ -739,6 +752,15 @@ async def stream_agent(request: AgentRequest):
739
752
  len(_simple_agent_checkpointers),
740
753
  )
741
754
 
755
+ # Clear code history for new threads
756
+ if not is_existing_thread:
757
+ from agent_server.langchain.middleware.code_history_middleware import (
758
+ clear_code_history,
759
+ )
760
+
761
+ clear_code_history(thread_id)
762
+ logger.info(f"Code history cleared for new thread: {thread_id}")
763
+
742
764
  resolved_workspace_root = _resolve_workspace_root(request.workspaceRoot)
743
765
 
744
766
  # Get agent mode (single or multi)
@@ -746,20 +768,31 @@ async def stream_agent(request: AgentRequest):
746
768
  logger.info("Agent mode: %s", agent_mode)
747
769
 
748
770
  # Get agent prompts (for multi-agent mode)
749
- # ROOT CAUSE FOUND: Frontend sends both systemPrompt AND agentPrompts
750
- # which creates different cache keys and causes MALFORMED_FUNCTION_CALL
751
- # FIX: Ignore all custom prompts for multi-agent mode until frontend is fixed
752
771
  agent_prompts = None
753
772
  if agent_mode == "multi":
754
- # For multi-agent mode, always use default prompts
773
+ # Multi-agent mode: Use agentPrompts for per-agent customization
774
+ # systemPrompt is for single-agent mode only (DEFAULT_SYSTEM_PROMPT)
755
775
  if request.llmConfig and request.llmConfig.agent_prompts:
756
- logger.warning(
757
- "Multi-agent mode: Ignoring frontend agentPrompts to ensure consistent behavior"
776
+ agent_prompts = {
777
+ "planner": request.llmConfig.agent_prompts.planner,
778
+ "python_developer": (
779
+ request.llmConfig.agent_prompts.python_developer
780
+ ),
781
+ "researcher": request.llmConfig.agent_prompts.researcher,
782
+ "athena_query": request.llmConfig.agent_prompts.athena_query,
783
+ }
784
+ agent_prompts = {k: v for k, v in agent_prompts.items() if v}
785
+ logger.info(
786
+ "Multi-agent mode: Using agentPrompts (%s)",
787
+ list(agent_prompts.keys()),
758
788
  )
759
- # Also ignore system_prompt_override for multi-agent mode
789
+ # In multi-agent mode, DON'T use systemPrompt as override
790
+ # (systemPrompt = single-agent prompt, not planner prompt)
791
+ # Use agentPrompts.planner instead (handled by agent_factory)
760
792
  if system_prompt_override:
761
- logger.warning(
762
- "Multi-agent mode: Ignoring systemPrompt override (len=%d)",
793
+ logger.info(
794
+ "Multi-agent mode: Ignoring systemPrompt override (len=%d) - "
795
+ "use agentPrompts.planner instead",
763
796
  len(system_prompt_override),
764
797
  )
765
798
  system_prompt_override = None
@@ -827,6 +860,13 @@ async def stream_agent(request: AgentRequest):
827
860
  # Prepare config with thread_id
828
861
  config = {"configurable": {"thread_id": thread_id}}
829
862
 
863
+ # Set current thread_id for code history tracking
864
+ from agent_server.langchain.middleware.subagent_middleware import (
865
+ set_current_thread_id,
866
+ )
867
+
868
+ set_current_thread_id(thread_id)
869
+
830
870
  # Check existing state and ALWAYS reset todos for new request
831
871
  # Each new user request starts a fresh todo list
832
872
  should_reset_todos = False
@@ -903,7 +943,6 @@ async def stream_agent(request: AgentRequest):
903
943
  produced_output = False
904
944
  last_finish_reason = None
905
945
  last_signature = None
906
- latest_todos: Optional[List[Dict[str, Any]]] = None
907
946
  # Initialize emitted contents set for this thread (clear any stale data)
908
947
  emitted_contents: set = set()
909
948
  _simple_agent_emitted_contents[thread_id] = emitted_contents
@@ -957,12 +996,10 @@ async def stream_agent(request: AgentRequest):
957
996
  if isinstance(step, dict) and "todos" in step:
958
997
  todos = step["todos"]
959
998
  if todos:
960
- latest_todos = todos
961
999
  yield {"event": "todos", "data": json.dumps({"todos": todos})}
962
1000
  elif isinstance(step, dict):
963
1001
  todos = _extract_todos(step)
964
1002
  if todos:
965
- latest_todos = todos
966
1003
  yield {"event": "todos", "data": json.dumps({"todos": todos})}
967
1004
 
968
1005
  # Process messages (no continue statements to ensure interrupt check always runs)
@@ -1012,7 +1049,6 @@ async def stream_agent(request: AgentRequest):
1012
1049
  )
1013
1050
  todos = _extract_todos(last_message.content)
1014
1051
  if todos:
1015
- latest_todos = todos
1016
1052
  yield {
1017
1053
  "event": "todos",
1018
1054
  "data": json.dumps({"todos": todos}),
@@ -1270,7 +1306,6 @@ async def stream_agent(request: AgentRequest):
1270
1306
  "SSE: Emitting todos event from AIMessage tool_calls: %d items",
1271
1307
  len(todos),
1272
1308
  )
1273
- latest_todos = todos
1274
1309
  yield {
1275
1310
  "event": "todos",
1276
1311
  "data": json.dumps({"todos": todos}),
@@ -1286,7 +1321,7 @@ async def stream_agent(request: AgentRequest):
1286
1321
  "다음단계",
1287
1322
  "다음 단계",
1288
1323
  ]
1289
- has_summary_todo = any(
1324
+ any(
1290
1325
  any(
1291
1326
  kw in t.get("content", "")
1292
1327
  for kw in summary_keywords
@@ -1461,6 +1496,22 @@ async def stream_agent(request: AgentRequest):
1461
1496
  }
1462
1497
  ),
1463
1498
  }
1499
+ elif tool_name in (
1500
+ "final_summary_tool",
1501
+ "final_summary",
1502
+ ):
1503
+ # Final summary - emit summary event for frontend
1504
+ produced_output = True
1505
+ summary_data = {
1506
+ "summary": tool_args.get("summary", ""),
1507
+ "next_items": tool_args.get(
1508
+ "next_items", []
1509
+ ),
1510
+ }
1511
+ yield {
1512
+ "event": "summary",
1513
+ "data": json.dumps(summary_data),
1514
+ }
1464
1515
 
1465
1516
  # Only display content if it's not empty and not a JSON tool response
1466
1517
  if (
@@ -1504,6 +1555,31 @@ async def stream_agent(request: AgentRequest):
1504
1555
  and content_stripped.startswith("{")
1505
1556
  )
1506
1557
  )
1558
+
1559
+ # Check if this is summary/next_items with tool_calls (premature summary)
1560
+ has_summary_pattern = (
1561
+ '"summary"' in content or "'summary'" in content
1562
+ ) and (
1563
+ '"next_items"' in content
1564
+ or "'next_items'" in content
1565
+ )
1566
+ tool_calls = (
1567
+ getattr(last_message, "tool_calls", []) or []
1568
+ )
1569
+ # Check if any tool_call is NOT write_todos (work still in progress)
1570
+ has_non_todo_tool_calls = any(
1571
+ tc.get("name")
1572
+ not in ("write_todos", "write_todos_tool")
1573
+ for tc in tool_calls
1574
+ )
1575
+ # Skip summary if non-write_todos tool_calls exist (work still in progress)
1576
+ if has_summary_pattern and has_non_todo_tool_calls:
1577
+ logger.info(
1578
+ "Initial: SKIPPING premature summary (has non-todo tool_calls): %s",
1579
+ content[:100],
1580
+ )
1581
+ content = None # Skip this content
1582
+
1507
1583
  if (
1508
1584
  content
1509
1585
  and isinstance(content, str)
@@ -1709,6 +1785,39 @@ async def stream_agent(request: AgentRequest):
1709
1785
  }
1710
1786
  produced_output = True
1711
1787
  fallback_response = None
1788
+ if isinstance(fallback_response, AIMessage):
1789
+ # LLM Response - structured JSON format
1790
+ print(LOG_RESPONSE_START, flush=True)
1791
+ response_data = {
1792
+ "type": "AIMessage",
1793
+ "content": fallback_response.content or "",
1794
+ "tool_calls": fallback_response.tool_calls
1795
+ if hasattr(fallback_response, "tool_calls")
1796
+ else [],
1797
+ "additional_kwargs": getattr(
1798
+ fallback_response, "additional_kwargs", {}
1799
+ )
1800
+ or {},
1801
+ "response_metadata": getattr(
1802
+ fallback_response, "response_metadata", {}
1803
+ )
1804
+ or {},
1805
+ "usage_metadata": getattr(
1806
+ fallback_response, "usage_metadata", {}
1807
+ )
1808
+ or {},
1809
+ }
1810
+ print(
1811
+ json.dumps(
1812
+ response_data,
1813
+ indent=2,
1814
+ ensure_ascii=False,
1815
+ default=str,
1816
+ ),
1817
+ flush=True,
1818
+ )
1819
+ print(LOG_RESPONSE_END, flush=True)
1820
+
1712
1821
  if isinstance(fallback_response, AIMessage) and getattr(
1713
1822
  fallback_response, "tool_calls", None
1714
1823
  ):
@@ -1771,6 +1880,17 @@ async def stream_agent(request: AgentRequest):
1771
1880
  }
1772
1881
  ),
1773
1882
  }
1883
+ elif tool_name in ("final_summary_tool", "final_summary"):
1884
+ # Final summary - emit summary event for frontend
1885
+ produced_output = True
1886
+ summary_data = {
1887
+ "summary": tool_args.get("summary", ""),
1888
+ "next_items": tool_args.get("next_items", []),
1889
+ }
1890
+ yield {
1891
+ "event": "summary",
1892
+ "data": json.dumps(summary_data),
1893
+ }
1774
1894
  elif tool_name == "read_file_tool":
1775
1895
  # For file operations, generate code with the LLM
1776
1896
  logger.info(
@@ -1959,14 +2079,28 @@ async def resume_agent(request: ResumeRequest):
1959
2079
  logger.info("Resume: Agent mode: %s", agent_mode)
1960
2080
 
1961
2081
  # Get agent prompts (for multi-agent mode)
1962
- # ROOT CAUSE: Frontend sends both systemPrompt AND agentPrompts
1963
- # FIX: Ignore all custom prompts for multi-agent mode
1964
2082
  agent_prompts = None
1965
2083
  if agent_mode == "multi":
1966
2084
  if request.llmConfig and request.llmConfig.agent_prompts:
1967
- logger.warning("Resume: Multi-agent mode - ignoring agentPrompts")
2085
+ agent_prompts = {
2086
+ "planner": request.llmConfig.agent_prompts.planner,
2087
+ "python_developer": (
2088
+ request.llmConfig.agent_prompts.python_developer
2089
+ ),
2090
+ "researcher": request.llmConfig.agent_prompts.researcher,
2091
+ "athena_query": request.llmConfig.agent_prompts.athena_query,
2092
+ }
2093
+ agent_prompts = {k: v for k, v in agent_prompts.items() if v}
2094
+ logger.info(
2095
+ "Resume: Multi-agent mode - using agentPrompts (%s)",
2096
+ list(agent_prompts.keys()),
2097
+ )
2098
+ # In multi-agent mode, DON'T use systemPrompt as override
1968
2099
  if system_prompt_override:
1969
- logger.warning("Resume: Multi-agent mode - ignoring systemPrompt")
2100
+ logger.info(
2101
+ "Resume: Multi-agent mode - ignoring systemPrompt (len=%d)",
2102
+ len(system_prompt_override),
2103
+ )
1970
2104
  system_prompt_override = None
1971
2105
 
1972
2106
  agent_cache_key = _get_agent_cache_key(
@@ -2011,6 +2145,13 @@ async def resume_agent(request: ResumeRequest):
2011
2145
  # Prepare config with thread_id
2012
2146
  config = {"configurable": {"thread_id": request.threadId}}
2013
2147
 
2148
+ # Set current thread_id for code history tracking
2149
+ from agent_server.langchain.middleware.subagent_middleware import (
2150
+ set_current_thread_id,
2151
+ )
2152
+
2153
+ set_current_thread_id(request.threadId)
2154
+
2014
2155
  pending_actions = _simple_agent_pending_actions.get(request.threadId, [])
2015
2156
 
2016
2157
  # Convert decisions to LangChain format
@@ -2047,7 +2188,7 @@ async def resume_agent(request: ResumeRequest):
2047
2188
  "edit_file_tool",
2048
2189
  "multiedit_file_tool",
2049
2190
  ):
2050
- track_tool_execution(tool_name, args)
2191
+ track_tool_execution(tool_name, args, request.threadId)
2051
2192
  langgraph_decisions.append(
2052
2193
  {
2053
2194
  "type": "edit",
@@ -2341,7 +2482,39 @@ async def resume_agent(request: ResumeRequest):
2341
2482
  # ToolMessage processing continues (no final_answer_tool)
2342
2483
 
2343
2484
  # Handle AIMessage (use elif to avoid processing after ToolMessage)
2344
- elif hasattr(last_message, "content") and last_message.content:
2485
+ elif isinstance(last_message, AIMessage):
2486
+ # LLM Response - structured JSON format
2487
+ print(LOG_RESPONSE_START, flush=True)
2488
+ response_data = {
2489
+ "type": "AIMessage",
2490
+ "content": last_message.content or "",
2491
+ "tool_calls": last_message.tool_calls
2492
+ if hasattr(last_message, "tool_calls")
2493
+ else [],
2494
+ "additional_kwargs": getattr(
2495
+ last_message, "additional_kwargs", {}
2496
+ )
2497
+ or {},
2498
+ "response_metadata": getattr(
2499
+ last_message, "response_metadata", {}
2500
+ )
2501
+ or {},
2502
+ "usage_metadata": getattr(
2503
+ last_message, "usage_metadata", {}
2504
+ )
2505
+ or {},
2506
+ }
2507
+ print(
2508
+ json.dumps(
2509
+ response_data,
2510
+ indent=2,
2511
+ ensure_ascii=False,
2512
+ default=str,
2513
+ ),
2514
+ flush=True,
2515
+ )
2516
+ print(LOG_RESPONSE_END, flush=True)
2517
+
2345
2518
  content = last_message.content
2346
2519
 
2347
2520
  # Handle list content (e.g., multimodal responses)
@@ -2375,6 +2548,28 @@ async def resume_agent(request: ResumeRequest):
2375
2548
  and content_stripped.startswith("{")
2376
2549
  )
2377
2550
  )
2551
+
2552
+ # Check if this is summary/next_items with tool_calls (premature summary)
2553
+ has_summary_pattern = (
2554
+ '"summary"' in content or "'summary'" in content
2555
+ ) and (
2556
+ '"next_items"' in content or "'next_items'" in content
2557
+ )
2558
+ tool_calls = getattr(last_message, "tool_calls", []) or []
2559
+ # Check if any tool_call is NOT write_todos (work still in progress)
2560
+ has_non_todo_tool_calls = any(
2561
+ tc.get("name")
2562
+ not in ("write_todos", "write_todos_tool")
2563
+ for tc in tool_calls
2564
+ )
2565
+ # Skip summary if non-write_todos tool_calls exist (work still in progress)
2566
+ if has_summary_pattern and has_non_todo_tool_calls:
2567
+ logger.info(
2568
+ "Resume: SKIPPING premature summary (has non-todo tool_calls): %s",
2569
+ content[:100],
2570
+ )
2571
+ content = None # Skip this content
2572
+
2378
2573
  if (
2379
2574
  content
2380
2575
  and isinstance(content, str)
@@ -2452,7 +2647,7 @@ async def resume_agent(request: ResumeRequest):
2452
2647
  "다음단계",
2453
2648
  "다음 단계",
2454
2649
  ]
2455
- has_summary_todo = any(
2650
+ any(
2456
2651
  any(
2457
2652
  kw in t.get("content", "")
2458
2653
  for kw in summary_keywords
@@ -2626,6 +2821,21 @@ async def resume_agent(request: ResumeRequest):
2626
2821
  }
2627
2822
  ),
2628
2823
  }
2824
+ elif tool_name in (
2825
+ "final_summary_tool",
2826
+ "final_summary",
2827
+ ):
2828
+ # Final summary - emit summary event for frontend
2829
+ summary_data = {
2830
+ "summary": tool_args.get("summary", ""),
2831
+ "next_items": tool_args.get(
2832
+ "next_items", []
2833
+ ),
2834
+ }
2835
+ yield {
2836
+ "event": "summary",
2837
+ "data": json.dumps(summary_data),
2838
+ }
2629
2839
 
2630
2840
  # Drain and emit any subagent events (tool calls from subagents)
2631
2841
  for subagent_event in get_subagent_debug_events():
@@ -2888,5 +3098,7 @@ async def reset_agent_thread(request: ResetRequest) -> Dict[str, Any]:
2888
3098
  "status": "ok",
2889
3099
  "thread_id": thread_id,
2890
3100
  "cleared": cleared,
2891
- "message": f"Thread {thread_id} has been reset" if cleared else f"Thread {thread_id} had no state to clear",
3101
+ "message": f"Thread {thread_id} has been reset"
3102
+ if cleared
3103
+ else f"Thread {thread_id} had no state to clear",
2892
3104
  }
@@ -722,7 +722,7 @@
722
722
  "@mui/material": {},
723
723
  "react-markdown": {},
724
724
  "hdsp-agent": {
725
- "version": "2.0.25",
725
+ "version": "2.0.27",
726
726
  "singleton": true,
727
727
  "import": "/Users/a421721/Desktop/hdsp/hdsp_agent/extensions/jupyter/lib/index.js"
728
728
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hdsp-agent",
3
- "version": "2.0.25",
3
+ "version": "2.0.27",
4
4
  "description": "HDSP Agent JupyterLab Extension - Thin client for Agent Server",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -132,7 +132,7 @@
132
132
  }
133
133
  },
134
134
  "_build": {
135
- "load": "static/remoteEntry.ffc2b4bc8e6cb300e1e1.js",
135
+ "load": "static/remoteEntry.4ab73bb5068405670214.js",
136
136
  "extension": "./extension",
137
137
  "style": "./style"
138
138
  }
@@ -281,7 +281,7 @@ __webpack_require__.d(exports, {
281
281
  /******/ register("@emotion/react", "11.14.0", () => (Promise.all([__webpack_require__.e("vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js"), __webpack_require__.e("vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195"), __webpack_require__.e("vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js"), __webpack_require__.e("webpack_sharing_consume_default_react"), __webpack_require__.e("node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80")]).then(() => (() => (__webpack_require__(/*! ./node_modules/@emotion/react/dist/emotion-react.browser.development.esm.js */ "./node_modules/@emotion/react/dist/emotion-react.browser.development.esm.js"))))));
282
282
  /******/ register("@emotion/styled", "11.14.1", () => (Promise.all([__webpack_require__.e("vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195"), __webpack_require__.e("vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js"), __webpack_require__.e("webpack_sharing_consume_default_react"), __webpack_require__.e("webpack_sharing_consume_default_emotion_react_emotion_react")]).then(() => (() => (__webpack_require__(/*! ./node_modules/@emotion/styled/dist/emotion-styled.browser.development.esm.js */ "./node_modules/@emotion/styled/dist/emotion-styled.browser.development.esm.js"))))));
283
283
  /******/ register("@mui/material", "5.18.0", () => (Promise.all([__webpack_require__.e("vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js"), __webpack_require__.e("vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195"), __webpack_require__.e("vendors-node_modules_mui_material_index_js"), __webpack_require__.e("vendors-node_modules_mui_material_utils_createSvgIcon_js"), __webpack_require__.e("webpack_sharing_consume_default_react"), __webpack_require__.e("webpack_sharing_consume_default_emotion_react_emotion_react-webpack_sharing_consume_default_e-5f41bf")]).then(() => (() => (__webpack_require__(/*! ./node_modules/@mui/material/index.js */ "./node_modules/@mui/material/index.js"))))));
284
- /******/ register("hdsp-agent", "2.0.25", () => (Promise.all([__webpack_require__.e("vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js"), __webpack_require__.e("vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195"), __webpack_require__.e("vendors-node_modules_mui_material_utils_createSvgIcon_js"), __webpack_require__.e("webpack_sharing_consume_default_react"), __webpack_require__.e("webpack_sharing_consume_default_emotion_react_emotion_react-webpack_sharing_consume_default_e-5f41bf"), __webpack_require__.e("lib_index_js")]).then(() => (() => (__webpack_require__(/*! ./lib/index.js */ "./lib/index.js"))))));
284
+ /******/ register("hdsp-agent", "2.0.27", () => (Promise.all([__webpack_require__.e("vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js"), __webpack_require__.e("vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195"), __webpack_require__.e("vendors-node_modules_mui_material_utils_createSvgIcon_js"), __webpack_require__.e("webpack_sharing_consume_default_react"), __webpack_require__.e("webpack_sharing_consume_default_emotion_react_emotion_react-webpack_sharing_consume_default_e-5f41bf"), __webpack_require__.e("lib_index_js")]).then(() => (() => (__webpack_require__(/*! ./lib/index.js */ "./lib/index.js"))))));
285
285
  /******/ }
286
286
  /******/ break;
287
287
  /******/ }
@@ -622,4 +622,4 @@ __webpack_require__.d(exports, {
622
622
  /******/
623
623
  /******/ })()
624
624
  ;
625
- //# sourceMappingURL=remoteEntry.ffc2b4bc8e6cb300e1e1.js.map
625
+ //# sourceMappingURL=remoteEntry.4ab73bb5068405670214.js.map