hdsp-jupyter-extension 2.0.26__py3-none-any.whl → 2.0.28__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 (71) hide show
  1. agent_server/context_providers/__init__.py +4 -2
  2. agent_server/context_providers/actions.py +73 -7
  3. agent_server/context_providers/file.py +23 -23
  4. agent_server/langchain/__init__.py +2 -2
  5. agent_server/langchain/agent.py +18 -251
  6. agent_server/langchain/agent_factory.py +26 -4
  7. agent_server/langchain/agent_prompts/planner_prompt.py +22 -35
  8. agent_server/langchain/custom_middleware.py +278 -43
  9. agent_server/langchain/llm_factory.py +102 -54
  10. agent_server/langchain/logging_utils.py +1 -1
  11. agent_server/langchain/middleware/__init__.py +5 -0
  12. agent_server/langchain/middleware/code_history_middleware.py +126 -37
  13. agent_server/langchain/middleware/content_injection_middleware.py +110 -0
  14. agent_server/langchain/middleware/subagent_events.py +88 -9
  15. agent_server/langchain/middleware/subagent_middleware.py +518 -240
  16. agent_server/langchain/prompts.py +5 -22
  17. agent_server/langchain/state_schema.py +44 -0
  18. agent_server/langchain/tools/jupyter_tools.py +4 -5
  19. agent_server/langchain/tools/tool_registry.py +6 -0
  20. agent_server/routers/chat.py +305 -2
  21. agent_server/routers/config.py +193 -8
  22. agent_server/routers/config_schema.py +254 -0
  23. agent_server/routers/context.py +31 -8
  24. agent_server/routers/langchain_agent.py +310 -153
  25. hdsp_agent_core/managers/config_manager.py +100 -1
  26. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
  27. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
  28. hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js → hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.55727265b00191e68d9a.js +479 -15
  29. hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.55727265b00191e68d9a.js.map +1 -0
  30. jupyter_ext/labextension/static/lib_index_js.67505497667f9c0a763d.js → hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.df05d90f366bfd5fa023.js +1287 -190
  31. hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.df05d90f366bfd5fa023.js.map +1 -0
  32. hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.0fe2dcbbd176ee0efceb.js → hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.08fce819ee32e9d25175.js +3 -3
  33. jupyter_ext/labextension/static/remoteEntry.0fe2dcbbd176ee0efceb.js.map → hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.08fce819ee32e9d25175.js.map +1 -1
  34. {hdsp_jupyter_extension-2.0.26.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/METADATA +1 -1
  35. {hdsp_jupyter_extension-2.0.26.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/RECORD +66 -64
  36. jupyter_ext/_version.py +1 -1
  37. jupyter_ext/handlers.py +41 -0
  38. jupyter_ext/labextension/build_log.json +1 -1
  39. jupyter_ext/labextension/package.json +2 -2
  40. jupyter_ext/labextension/static/{frontend_styles_index_js.b5e4416b4e07ec087aad.js → frontend_styles_index_js.55727265b00191e68d9a.js} +479 -15
  41. jupyter_ext/labextension/static/frontend_styles_index_js.55727265b00191e68d9a.js.map +1 -0
  42. hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js → jupyter_ext/labextension/static/lib_index_js.df05d90f366bfd5fa023.js +1287 -190
  43. jupyter_ext/labextension/static/lib_index_js.df05d90f366bfd5fa023.js.map +1 -0
  44. jupyter_ext/labextension/static/{remoteEntry.0fe2dcbbd176ee0efceb.js → remoteEntry.08fce819ee32e9d25175.js} +3 -3
  45. hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.0fe2dcbbd176ee0efceb.js.map → jupyter_ext/labextension/static/remoteEntry.08fce819ee32e9d25175.js.map +1 -1
  46. agent_server/langchain/middleware/description_injector.py +0 -150
  47. hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js.map +0 -1
  48. hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js.map +0 -1
  49. jupyter_ext/labextension/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js.map +0 -1
  50. jupyter_ext/labextension/static/lib_index_js.67505497667f9c0a763d.js.map +0 -1
  51. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
  52. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  53. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.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
  54. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.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
  55. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.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
  56. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.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
  57. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
  58. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.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
  59. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.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
  60. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +0 -0
  61. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.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
  62. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +0 -0
  63. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.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
  64. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
  65. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.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
  66. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
  67. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
  68. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +0 -0
  69. {hdsp_jupyter_extension-2.0.26.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -0
  70. {hdsp_jupyter_extension-2.0.26.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/WHEEL +0 -0
  71. {hdsp_jupyter_extension-2.0.26.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/licenses/LICENSE +0 -0
@@ -19,10 +19,7 @@ from langgraph.checkpoint.memory import InMemorySaver
19
19
  from pydantic import BaseModel, ConfigDict, Field
20
20
  from sse_starlette.sse import EventSourceResponse
21
21
 
22
- from agent_server.langchain.agent import (
23
- _get_all_tools,
24
- create_agent_system,
25
- )
22
+ from agent_server.langchain.agent import create_agent_system
26
23
  from agent_server.langchain.llm_factory import create_llm
27
24
  from agent_server.langchain.logging_utils import (
28
25
  LOG_RESPONSE_END,
@@ -33,7 +30,10 @@ from agent_server.langchain.middleware.code_history_middleware import (
33
30
  )
34
31
 
35
32
  # Note: Subagent middleware is used by agent_factory, not directly by router
36
- from agent_server.langchain.middleware.subagent_events import drain_subagent_events
33
+ from agent_server.langchain.middleware.subagent_events import (
34
+ drain_subagent_events,
35
+ drain_summarization_events,
36
+ )
37
37
 
38
38
  logger = logging.getLogger(__name__)
39
39
  router = APIRouter(prefix="/langchain", tags=["langchain-agent"])
@@ -63,11 +63,43 @@ def get_subagent_debug_events():
63
63
  """
64
64
  Drain subagent events and convert to SSE debug events.
65
65
 
66
+ If any subagent_complete event is found, appends a "LLM 응답 대기 중"
67
+ event so the UI doesn't show "완료" during the next LLM call.
68
+
66
69
  Returns:
67
70
  List of SSE event dicts for debug display
68
71
  """
69
72
  events = drain_subagent_events()
70
73
  sse_events = []
74
+ has_complete = False
75
+ for event in events:
76
+ sse_events.append(
77
+ {
78
+ "event": "debug",
79
+ "data": json.dumps(event.to_status_dict()),
80
+ }
81
+ )
82
+ if event.event_type == "subagent_complete":
83
+ has_complete = True
84
+ if has_complete:
85
+ sse_events.append(
86
+ {
87
+ "event": "debug",
88
+ "data": json.dumps({"status": "LLM 응답 대기 중", "icon": "thinking"}),
89
+ }
90
+ )
91
+ return sse_events
92
+
93
+
94
+ def get_summarization_debug_events():
95
+ """
96
+ Drain summarization events and convert to SSE debug events.
97
+
98
+ Returns:
99
+ List of SSE event dicts for debug display
100
+ """
101
+ events = drain_summarization_events()
102
+ sse_events = []
71
103
  for event in events:
72
104
  sse_events.append(
73
105
  {
@@ -104,15 +136,19 @@ def _get_tool_status_message(
104
136
  }
105
137
  elif tool_name_normalized in ("task", "task_tool"):
106
138
  # Show subagent delegation details with expand support
107
- agent_name = tool_args.get("agent_name", "unknown")
139
+ agent_name = tool_args.get("subagent_type") or tool_args.get(
140
+ "agent_name", "unknown"
141
+ )
108
142
  description = tool_args.get("description", "")
109
143
  short_desc = description[:50] + "..." if len(description) > 50 else description
110
144
  return {
111
- "status": f"{agent_name} Agent 실행: {short_desc}",
145
+ "status": f"Subagent-{agent_name} 실행: {short_desc}",
112
146
  "icon": "agent",
113
147
  "expandable": len(description) > 50,
114
- "full_text": f"{agent_name} Agent 실행: {description}",
148
+ "full_text": f"Subagent-{agent_name} 실행: {description}",
115
149
  }
150
+ elif tool_name_normalized in ("final_summary_tool", "final_summary"):
151
+ return {"status": "작업 마무리중...", "icon": "summary"}
116
152
  elif tool_name_normalized in ("list_workspace_tool", "list_workspace"):
117
153
  path = tool_args.get("path", ".")
118
154
  pattern = tool_args.get("pattern", "*")
@@ -224,12 +260,12 @@ class LLMConfig(BaseModel):
224
260
  system_prompt: Optional[str] = Field(
225
261
  default=None,
226
262
  alias="systemPrompt",
227
- description="Override system prompt for single agent mode",
263
+ description="Override system prompt",
228
264
  )
229
265
  agent_prompts: Optional[AgentPromptsConfig] = Field(
230
266
  default=None,
231
267
  alias="agentPrompts",
232
- description="Per-agent system prompts for multi-agent mode",
268
+ description="Per-agent system prompts",
233
269
  )
234
270
  resource_context: Optional[Union[Dict[str, Any], str]] = Field(
235
271
  default=None,
@@ -273,10 +309,6 @@ class AgentRequest(BaseModel):
273
309
  default=None,
274
310
  description="Thread ID for conversation persistence (required for HITL)",
275
311
  )
276
- agentMode: Optional[str] = Field(
277
- default="single",
278
- description="Agent mode: 'single' (all tools) or 'multi' (Planner + Subagents)",
279
- )
280
312
 
281
313
 
282
314
  class ResumeDecision(BaseModel):
@@ -304,10 +336,6 @@ class ResumeRequest(BaseModel):
304
336
  workspaceRoot: Optional[str] = Field(
305
337
  default=".", description="Workspace root directory"
306
338
  )
307
- agentMode: Optional[str] = Field(
308
- default="single",
309
- description="Agent mode: 'single' (all tools) or 'multi' (Planner + Subagents)",
310
- )
311
339
 
312
340
 
313
341
  class ExecutionResult(BaseModel):
@@ -354,20 +382,18 @@ def _get_agent_cache_key(
354
382
  llm_config: Dict[str, Any],
355
383
  workspace_root: str,
356
384
  system_prompt_override: Optional[str] = None,
357
- agent_mode: str = "single",
358
385
  agent_prompts: Optional[Dict[str, str]] = None,
359
386
  ) -> str:
360
387
  """Generate cache key for agent instance.
361
388
 
362
389
  Agent instances are cached based on LLM config, workspace root, system prompt,
363
- agent mode, and agent prompts. Different configurations require different agent instances.
390
+ and agent prompts. Different configurations require different agent instances.
364
391
 
365
392
  Args:
366
393
  llm_config: LLM configuration dictionary
367
394
  workspace_root: Workspace root directory
368
395
  system_prompt_override: Optional custom system prompt
369
- agent_mode: "single" or "multi" agent mode
370
- agent_prompts: Optional dict of per-agent prompts (for multi-agent mode)
396
+ agent_prompts: Optional dict of per-agent prompts
371
397
 
372
398
  Returns:
373
399
  MD5 hash of the configuration as cache key
@@ -382,15 +408,23 @@ def _get_agent_cache_key(
382
408
  )
383
409
 
384
410
  cache_data = (
385
- f"{config_str}|{workspace_root}|{prompt_str}|{agent_mode}|{agent_prompts_str}"
411
+ f"{config_str}|{workspace_root}|{prompt_str}|{agent_prompts_str}"
386
412
  )
387
413
  cache_key = hashlib.md5(cache_data.encode()).hexdigest()
388
414
 
389
415
  return cache_key
390
416
 
391
417
 
392
- def _normalize_action_request(action: Dict[str, Any]) -> Dict[str, Any]:
393
- """Normalize HITL action request payload across LangChain versions."""
418
+ def _normalize_action_request(
419
+ action: Dict[str, Any],
420
+ state: Optional[Dict[str, Any]] = None,
421
+ ) -> Dict[str, Any]:
422
+ """Normalize HITL action request payload across LangChain versions.
423
+
424
+ Also injects generated_content from state into tool args for display
425
+ (ContentInjectionMiddleware.wrap_tool_call runs at execution time,
426
+ but HITL interrupts before execution — so we inject here for display).
427
+ """
394
428
  logger.info(f"[_normalize_action_request] Called with action: {str(action)[:200]}")
395
429
  name = (
396
430
  action.get("name")
@@ -407,39 +441,32 @@ def _normalize_action_request(action: Dict[str, Any]) -> Dict[str, Any]:
407
441
  or action.get("parameters")
408
442
  or {}
409
443
  )
444
+
445
+ # Inject generated_content from state into tool args for HITL display
446
+ if state and isinstance(args, dict):
447
+ content = state.get("generated_content")
448
+ content_type = state.get("generated_content_type")
449
+ desc = state.get("content_description")
450
+ if content and content_type:
451
+ if content_type == "python":
452
+ if name == "jupyter_cell_tool" and not args.get("code"):
453
+ args = {**args, "code": content}
454
+ if desc and not args.get("description"):
455
+ args["description"] = desc
456
+ elif name == "write_file_tool" and not args.get("content"):
457
+ args = {**args, "content": content}
458
+ elif content_type == "sql":
459
+ if name == "markdown_tool" and not args.get("content"):
460
+ sql_md = f"```sql\n{content}\n```"
461
+ if desc:
462
+ sql_md = f"{desc}\n\n{sql_md}"
463
+ args = {**args, "content": sql_md}
464
+
410
465
  # Try to get description from action first, then from args (for jupyter_cell_tool etc)
411
466
  description = action.get("description", "") or (
412
467
  args.get("description", "") if isinstance(args, dict) else ""
413
468
  )
414
469
 
415
- # Auto-inject description for jupyter_cell_tool from python_developer's response
416
- # Only inject into args.description, keep top-level description as HITL default
417
- if name == "jupyter_cell_tool":
418
- logger.info(
419
- f"[HITL] jupyter_cell_tool detected, current description: '{description[:50] if description else 'None'}'"
420
- )
421
- try:
422
- from agent_server.langchain.middleware.description_injector import (
423
- clear_pending_description,
424
- get_pending_description,
425
- )
426
-
427
- pending = get_pending_description()
428
- if pending:
429
- # Inject into args.description only (for detailed description display)
430
- # Keep top-level description as HITL approval message
431
- if isinstance(args, dict):
432
- args = dict(args)
433
- args["description"] = pending
434
- clear_pending_description()
435
- logger.info(
436
- f"[HITL] Auto-injected description into args: {pending[:80]}..."
437
- )
438
- else:
439
- logger.info("[HITL] No pending description from python_developer")
440
- except Exception as e:
441
- logger.warning(f"Failed to inject description: {e}")
442
-
443
470
  return {"name": name, "arguments": args, "description": description}
444
471
 
445
472
 
@@ -695,6 +722,19 @@ async def stream_agent(request: AgentRequest):
695
722
  thread_id,
696
723
  )
697
724
 
725
+ # Handle /reset command
726
+ if "/reset" in request.request:
727
+ logger.info(f"/reset command detected for thread: {thread_id}")
728
+ from agent_server.langchain.middleware.code_history_middleware import (
729
+ clear_code_history,
730
+ )
731
+
732
+ # Clear code history and checkpointer for this thread
733
+ clear_code_history(thread_id)
734
+ if thread_id in _simple_agent_checkpointers:
735
+ del _simple_agent_checkpointers[thread_id]
736
+ logger.info(f"Session reset complete for thread: {thread_id}")
737
+
698
738
  async def event_generator():
699
739
  try:
700
740
  # Use simple agent with HITL
@@ -739,59 +779,48 @@ async def stream_agent(request: AgentRequest):
739
779
  len(_simple_agent_checkpointers),
740
780
  )
741
781
 
742
- resolved_workspace_root = _resolve_workspace_root(request.workspaceRoot)
782
+ # Clear code history for new threads
783
+ if not is_existing_thread:
784
+ from agent_server.langchain.middleware.code_history_middleware import (
785
+ clear_code_history,
786
+ )
787
+
788
+ clear_code_history(thread_id)
789
+ logger.info(f"Code history cleared for new thread: {thread_id}")
743
790
 
744
- # Get agent mode (single or multi)
745
- agent_mode = getattr(request, "agentMode", "single") or "single"
746
- logger.info("Agent mode: %s", agent_mode)
791
+ resolved_workspace_root = _resolve_workspace_root(request.workspaceRoot)
747
792
 
748
- # Get agent prompts (for multi-agent mode)
793
+ # Get agent prompts for per-agent customization
749
794
  agent_prompts = None
750
- if agent_mode == "multi":
751
- # Multi-agent mode: Use agentPrompts for per-agent customization
752
- # systemPrompt is for single-agent mode only (DEFAULT_SYSTEM_PROMPT)
753
- if request.llmConfig and request.llmConfig.agent_prompts:
754
- agent_prompts = {
755
- "planner": request.llmConfig.agent_prompts.planner,
756
- "python_developer": (
757
- request.llmConfig.agent_prompts.python_developer
758
- ),
759
- "researcher": request.llmConfig.agent_prompts.researcher,
760
- "athena_query": request.llmConfig.agent_prompts.athena_query,
761
- }
762
- agent_prompts = {k: v for k, v in agent_prompts.items() if v}
763
- logger.info(
764
- "Multi-agent mode: Using agentPrompts (%s)",
765
- list(agent_prompts.keys()),
766
- )
767
- # In multi-agent mode, DON'T use systemPrompt as override
768
- # (systemPrompt = single-agent prompt, not planner prompt)
769
- # Use agentPrompts.planner instead (handled by agent_factory)
770
- if system_prompt_override:
771
- logger.info(
772
- "Multi-agent mode: Ignoring systemPrompt override (len=%d) - "
773
- "use agentPrompts.planner instead",
774
- len(system_prompt_override),
775
- )
776
- system_prompt_override = None
777
- elif request.llmConfig and request.llmConfig.agent_prompts:
778
- # Single-agent mode: can use custom prompts (not applicable currently)
795
+ if request.llmConfig and request.llmConfig.agent_prompts:
779
796
  agent_prompts = {
780
797
  "planner": request.llmConfig.agent_prompts.planner,
781
- "python_developer": request.llmConfig.agent_prompts.python_developer,
798
+ "python_developer": (
799
+ request.llmConfig.agent_prompts.python_developer
800
+ ),
782
801
  "researcher": request.llmConfig.agent_prompts.researcher,
783
802
  "athena_query": request.llmConfig.agent_prompts.athena_query,
784
803
  }
785
804
  agent_prompts = {k: v for k, v in agent_prompts.items() if v}
805
+ logger.info(
806
+ "Using agentPrompts (%s)",
807
+ list(agent_prompts.keys()),
808
+ )
809
+ # Don't use systemPrompt as override — use agentPrompts.planner instead
810
+ if system_prompt_override:
811
+ logger.info(
812
+ "Ignoring systemPrompt override (len=%d) - "
813
+ "use agentPrompts.planner instead",
814
+ len(system_prompt_override),
815
+ )
816
+ system_prompt_override = None
786
817
 
787
818
  # Get or create cached agent
788
- # DEBUG: Log cache key components
789
819
  logger.info(
790
- "DEBUG: Cache key components - provider=%s, workspace=%s, mode=%s, "
820
+ "Cache key components - provider=%s, workspace=%s, "
791
821
  "has_system_prompt=%s, has_agent_prompts=%s",
792
822
  config_dict.get("provider"),
793
823
  resolved_workspace_root[:50] if resolved_workspace_root else None,
794
- agent_mode,
795
824
  bool(system_prompt_override),
796
825
  bool(agent_prompts),
797
826
  )
@@ -800,23 +829,20 @@ async def stream_agent(request: AgentRequest):
800
829
  llm_config=config_dict,
801
830
  workspace_root=resolved_workspace_root,
802
831
  system_prompt_override=system_prompt_override,
803
- agent_mode=agent_mode,
804
832
  agent_prompts=agent_prompts,
805
833
  )
806
834
 
807
835
  if agent_cache_key in _simple_agent_instances:
808
836
  agent = _simple_agent_instances[agent_cache_key]
809
837
  logger.info(
810
- "Using cached agent for key %s (mode=%s, total cached: %d)",
838
+ "Using cached agent for key %s (total cached: %d)",
811
839
  agent_cache_key[:8],
812
- agent_mode,
813
840
  len(_simple_agent_instances),
814
841
  )
815
842
  else:
816
843
  logger.info(
817
- "Creating new agent for key %s (mode=%s)",
844
+ "Creating new agent for key %s",
818
845
  agent_cache_key[:8],
819
- agent_mode,
820
846
  )
821
847
  agent = create_agent_system(
822
848
  llm_config=config_dict,
@@ -824,20 +850,25 @@ async def stream_agent(request: AgentRequest):
824
850
  enable_hitl=True,
825
851
  checkpointer=checkpointer,
826
852
  system_prompt_override=system_prompt_override,
827
- agent_mode=agent_mode,
828
853
  agent_prompts=agent_prompts,
829
854
  )
830
855
  _simple_agent_instances[agent_cache_key] = agent
831
856
  logger.info(
832
- "Agent cached for key %s (mode=%s, total cached: %d)",
857
+ "Agent cached for key %s (total cached: %d)",
833
858
  agent_cache_key[:8],
834
- agent_mode,
835
859
  len(_simple_agent_instances),
836
860
  )
837
861
 
838
862
  # Prepare config with thread_id
839
863
  config = {"configurable": {"thread_id": thread_id}}
840
864
 
865
+ # Set current thread_id for code history tracking
866
+ from agent_server.langchain.middleware.subagent_middleware import (
867
+ set_current_thread_id,
868
+ )
869
+
870
+ set_current_thread_id(thread_id)
871
+
841
872
  # Check existing state and ALWAYS reset todos for new request
842
873
  # Each new user request starts a fresh todo list
843
874
  should_reset_todos = False
@@ -871,8 +902,11 @@ async def stream_agent(request: AgentRequest):
871
902
  previous_todos_context = None
872
903
  if should_reset_todos:
873
904
  try:
874
- agent.update_state(config, {"todos": []})
875
- logger.info("Reset todos in agent state for thread %s", thread_id)
905
+ agent.update_state(config, {"todos": [], "todo_active": False})
906
+ logger.info(
907
+ "Reset todos and todo_active in agent state for thread %s",
908
+ thread_id,
909
+ )
876
910
  # Prepare event to notify frontend (will be yielded after function setup)
877
911
  todos_reset_event = {
878
912
  "event": "todos",
@@ -891,7 +925,8 @@ async def stream_agent(request: AgentRequest):
891
925
  items_summary += "..."
892
926
  previous_todos_context = (
893
927
  f"[SYSTEM] 이전 todo list가 완료 혹은 취소되었습니다. 완료된 작업: {items_summary}. "
894
- f"새 작업을 시작합니다. 이전 todo list 신규 작업을 append 하지 말고 새로운 todo list를 생성하세요."
928
+ f"새 작업을 시작합니다. 이전 todo list 초기화되었습니다. "
929
+ f"간단한 작업(1-2단계)이면 write_todos 없이 바로 실행하세요."
895
930
  )
896
931
  logger.info(
897
932
  "Injecting previous todos context: %s",
@@ -930,6 +965,11 @@ async def stream_agent(request: AgentRequest):
930
965
  "data": json.dumps({"status": "LLM 응답 대기 중", "icon": "thinking"}),
931
966
  }
932
967
 
968
+ # Track message count for summarization detection
969
+ # SummarizationMiddleware keeps ~3 messages after compression
970
+ previous_message_count = 0
971
+ summarization_detected = False
972
+
933
973
  # Main streaming loop
934
974
  async for step in _async_stream_wrapper(
935
975
  agent, agent_input, config, stream_mode="values"
@@ -940,6 +980,11 @@ async def stream_agent(request: AgentRequest):
940
980
  f"Thread {thread_id} cancelled by user, stopping stream"
941
981
  )
942
982
  clear_cancelled_thread(thread_id)
983
+ # Reset todo_active on cancellation
984
+ try:
985
+ agent.update_state(config, {"todo_active": False})
986
+ except Exception:
987
+ pass
943
988
  yield {
944
989
  "event": "cancelled",
945
990
  "data": json.dumps(
@@ -976,6 +1021,30 @@ async def stream_agent(request: AgentRequest):
976
1021
  # Process messages (no continue statements to ensure interrupt check always runs)
977
1022
  if isinstance(step, dict) and "messages" in step:
978
1023
  messages = step["messages"]
1024
+ current_message_count = len(messages) if messages else 0
1025
+
1026
+ # Detect summarization by checking for lc_source: "summarization" marker
1027
+ # SummarizationMiddleware injects summary into system prompt with this marker
1028
+ if not summarization_detected and messages:
1029
+ for msg in messages:
1030
+ # Check additional_kwargs for lc_source
1031
+ additional_kwargs = getattr(msg, "additional_kwargs", {}) or {}
1032
+ if additional_kwargs.get("lc_source") == "summarization":
1033
+ summarization_detected = True
1034
+ logger.info(
1035
+ f"[Agent] Summarization detected via lc_source marker"
1036
+ )
1037
+ yield {
1038
+ "event": "debug",
1039
+ "data": json.dumps({
1040
+ "status": "대화가 자동으로 압축되었습니다.",
1041
+ "icon": "check"
1042
+ }),
1043
+ }
1044
+ break
1045
+
1046
+ previous_message_count = current_message_count
1047
+
979
1048
  should_process_message = False
980
1049
  if messages:
981
1050
  last_message = messages[-1]
@@ -1118,10 +1187,12 @@ async def stream_agent(request: AgentRequest):
1118
1187
  }
1119
1188
  return # Exit the generator
1120
1189
  else:
1121
- logger.warning(
1122
- "All %d todos completed but no summary JSON in current step - NOT auto-terminating",
1190
+ logger.info(
1191
+ "All %d todos completed (no summary in step) - "
1192
+ "continuing to wait for final_summary_tool",
1123
1193
  len(todos),
1124
1194
  )
1195
+ # Don't auto-terminate — let agent call final_summary_tool
1125
1196
 
1126
1197
  tool_name = getattr(last_message, "name", "") or ""
1127
1198
  logger.info(
@@ -1309,7 +1380,14 @@ async def stream_agent(request: AgentRequest):
1309
1380
  has_summary_json = (
1310
1381
  '"summary"' in msg_content
1311
1382
  and '"next_items"' in msg_content
1383
+ ) or (
1384
+ "'summary'" in msg_content
1385
+ and "'next_items'" in msg_content
1312
1386
  )
1387
+ # Check if last_message is a ToolMessage from final_summary_tool
1388
+ is_final_summary_tool_msg = (
1389
+ getattr(last_message, "name", "") or ""
1390
+ ) in ("final_summary_tool", "final_summary")
1313
1391
  # Also check for markdown summary format
1314
1392
  has_markdown_summary = any(
1315
1393
  kw in msg_content
@@ -1323,15 +1401,18 @@ async def stream_agent(request: AgentRequest):
1323
1401
  ]
1324
1402
  )
1325
1403
  has_summary = (
1326
- has_summary_json or has_markdown_summary
1404
+ has_summary_json
1405
+ or has_markdown_summary
1406
+ or is_final_summary_tool_msg
1327
1407
  )
1328
1408
 
1329
1409
  # Only check current AIMessage for summary (not history, to avoid false positives)
1330
1410
  if not has_summary:
1331
- logger.warning(
1332
- "All todos completed but no summary JSON in current message - NOT auto-terminating"
1411
+ logger.info(
1412
+ "All todos completed (no summary in message) - "
1413
+ "continuing to wait for final_summary_tool"
1333
1414
  )
1334
- # Don't terminate - let agent continue to generate summary
1415
+ # Don't auto-terminate let agent call final_summary_tool
1335
1416
  else:
1336
1417
  logger.info(
1337
1418
  "All %d todos completed and summary exists in current message, auto-terminating",
@@ -1587,6 +1668,10 @@ async def stream_agent(request: AgentRequest):
1587
1668
  for subagent_event in get_subagent_debug_events():
1588
1669
  yield subagent_event
1589
1670
 
1671
+ # Drain and emit any summarization events (context compression)
1672
+ for summarization_event in get_summarization_debug_events():
1673
+ yield summarization_event
1674
+
1590
1675
  # Check for interrupt AFTER processing todos and messages
1591
1676
  # This ensures todos/debug events are emitted even in interrupt steps
1592
1677
  if isinstance(step, dict) and "__interrupt__" in step:
@@ -1621,7 +1706,10 @@ async def stream_agent(request: AgentRequest):
1621
1706
  f"[INTERRUPT] action_requests count: {len(action_requests)}, first: {str(action_requests[0])[:200] if action_requests else 'none'}"
1622
1707
  )
1623
1708
  normalized_actions = [
1624
- _normalize_action_request(a) for a in action_requests
1709
+ _normalize_action_request(
1710
+ a, state=step if isinstance(step, dict) else None
1711
+ )
1712
+ for a in action_requests
1625
1713
  ]
1626
1714
  if normalized_actions:
1627
1715
  _simple_agent_pending_actions[thread_id] = (
@@ -1682,7 +1770,27 @@ async def stream_agent(request: AgentRequest):
1682
1770
  )
1683
1771
 
1684
1772
  llm = create_llm(fallback_config)
1685
- tools = _get_all_tools()
1773
+ from agent_server.langchain.tools import (
1774
+ jupyter_cell_tool,
1775
+ markdown_tool,
1776
+ ask_user_tool,
1777
+ read_file_tool,
1778
+ write_file_tool,
1779
+ edit_file_tool,
1780
+ multiedit_file_tool,
1781
+ search_notebook_cells_tool,
1782
+ execute_command_tool,
1783
+ check_resource_tool,
1784
+ diagnostics_tool,
1785
+ references_tool,
1786
+ )
1787
+ tools = [
1788
+ jupyter_cell_tool, markdown_tool, ask_user_tool,
1789
+ read_file_tool, write_file_tool, edit_file_tool,
1790
+ multiedit_file_tool, search_notebook_cells_tool,
1791
+ execute_command_tool, check_resource_tool,
1792
+ diagnostics_tool, references_tool,
1793
+ ]
1686
1794
  # Force tool calling - use tool_config for Gemini, tool_choice for others
1687
1795
  provider = config_dict.get("provider", "gemini")
1688
1796
  if provider == "gemini":
@@ -2045,56 +2153,48 @@ async def resume_agent(request: ResumeRequest):
2045
2153
 
2046
2154
  checkpointer = _simple_agent_checkpointers.get(request.threadId)
2047
2155
 
2048
- # Get agent mode (single or multi)
2049
- agent_mode = getattr(request, "agentMode", "single") or "single"
2050
- logger.info("Resume: Agent mode: %s", agent_mode)
2051
-
2052
- # Get agent prompts (for multi-agent mode)
2156
+ # Get agent prompts for per-agent customization
2053
2157
  agent_prompts = None
2054
- if agent_mode == "multi":
2055
- if request.llmConfig and request.llmConfig.agent_prompts:
2056
- agent_prompts = {
2057
- "planner": request.llmConfig.agent_prompts.planner,
2058
- "python_developer": (
2059
- request.llmConfig.agent_prompts.python_developer
2060
- ),
2061
- "researcher": request.llmConfig.agent_prompts.researcher,
2062
- "athena_query": request.llmConfig.agent_prompts.athena_query,
2063
- }
2064
- agent_prompts = {k: v for k, v in agent_prompts.items() if v}
2065
- logger.info(
2066
- "Resume: Multi-agent mode - using agentPrompts (%s)",
2067
- list(agent_prompts.keys()),
2068
- )
2069
- # In multi-agent mode, DON'T use systemPrompt as override
2070
- if system_prompt_override:
2071
- logger.info(
2072
- "Resume: Multi-agent mode - ignoring systemPrompt (len=%d)",
2073
- len(system_prompt_override),
2074
- )
2075
- system_prompt_override = None
2158
+ if request.llmConfig and request.llmConfig.agent_prompts:
2159
+ agent_prompts = {
2160
+ "planner": request.llmConfig.agent_prompts.planner,
2161
+ "python_developer": (
2162
+ request.llmConfig.agent_prompts.python_developer
2163
+ ),
2164
+ "researcher": request.llmConfig.agent_prompts.researcher,
2165
+ "athena_query": request.llmConfig.agent_prompts.athena_query,
2166
+ }
2167
+ agent_prompts = {k: v for k, v in agent_prompts.items() if v}
2168
+ logger.info(
2169
+ "Resume: Using agentPrompts (%s)",
2170
+ list(agent_prompts.keys()),
2171
+ )
2172
+ # Don't use systemPrompt as override — use agentPrompts.planner instead
2173
+ if system_prompt_override:
2174
+ logger.info(
2175
+ "Resume: Ignoring systemPrompt (len=%d)",
2176
+ len(system_prompt_override),
2177
+ )
2178
+ system_prompt_override = None
2076
2179
 
2077
2180
  agent_cache_key = _get_agent_cache_key(
2078
2181
  llm_config=config_dict,
2079
2182
  workspace_root=resolved_workspace_root,
2080
2183
  system_prompt_override=system_prompt_override,
2081
- agent_mode=agent_mode,
2082
2184
  agent_prompts=agent_prompts,
2083
2185
  )
2084
2186
 
2085
2187
  if agent_cache_key in _simple_agent_instances:
2086
2188
  agent = _simple_agent_instances[agent_cache_key]
2087
2189
  logger.info(
2088
- "Resume: Using cached agent for key %s (mode=%s, total cached: %d)",
2190
+ "Resume: Using cached agent for key %s (total cached: %d)",
2089
2191
  agent_cache_key[:8],
2090
- agent_mode,
2091
2192
  len(_simple_agent_instances),
2092
2193
  )
2093
2194
  else:
2094
2195
  logger.info(
2095
- "Resume: Creating new agent for key %s (mode=%s)",
2196
+ "Resume: Creating new agent for key %s",
2096
2197
  agent_cache_key[:8],
2097
- agent_mode,
2098
2198
  )
2099
2199
  agent = create_agent_system(
2100
2200
  llm_config=config_dict,
@@ -2102,20 +2202,25 @@ async def resume_agent(request: ResumeRequest):
2102
2202
  enable_hitl=True,
2103
2203
  checkpointer=checkpointer,
2104
2204
  system_prompt_override=system_prompt_override,
2105
- agent_mode=agent_mode,
2106
2205
  agent_prompts=agent_prompts,
2107
2206
  )
2108
2207
  _simple_agent_instances[agent_cache_key] = agent
2109
2208
  logger.info(
2110
- "Resume: Agent cached for key %s (mode=%s, total cached: %d)",
2209
+ "Resume: Agent cached for key %s (total cached: %d)",
2111
2210
  agent_cache_key[:8],
2112
- agent_mode,
2113
2211
  len(_simple_agent_instances),
2114
2212
  )
2115
2213
 
2116
2214
  # Prepare config with thread_id
2117
2215
  config = {"configurable": {"thread_id": request.threadId}}
2118
2216
 
2217
+ # Set current thread_id for code history tracking
2218
+ from agent_server.langchain.middleware.subagent_middleware import (
2219
+ set_current_thread_id,
2220
+ )
2221
+
2222
+ set_current_thread_id(request.threadId)
2223
+
2119
2224
  pending_actions = _simple_agent_pending_actions.get(request.threadId, [])
2120
2225
 
2121
2226
  # Convert decisions to LangChain format
@@ -2152,7 +2257,7 @@ async def resume_agent(request: ResumeRequest):
2152
2257
  "edit_file_tool",
2153
2258
  "multiedit_file_tool",
2154
2259
  ):
2155
- track_tool_execution(tool_name, args)
2260
+ track_tool_execution(tool_name, args, request.threadId)
2156
2261
  langgraph_decisions.append(
2157
2262
  {
2158
2263
  "type": "edit",
@@ -2205,6 +2310,10 @@ async def resume_agent(request: ResumeRequest):
2205
2310
 
2206
2311
  step_count = 0
2207
2312
 
2313
+ # Track message count for summarization detection
2314
+ previous_message_count = 0
2315
+ summarization_detected = False
2316
+
2208
2317
  async for step in _async_stream_wrapper(
2209
2318
  agent,
2210
2319
  Command(resume={"decisions": langgraph_decisions}),
@@ -2217,6 +2326,11 @@ async def resume_agent(request: ResumeRequest):
2217
2326
  f"Thread {request.threadId} cancelled by user, stopping resume stream"
2218
2327
  )
2219
2328
  clear_cancelled_thread(request.threadId)
2329
+ # Reset todo_active on cancellation
2330
+ try:
2331
+ agent.update_state(config, {"todo_active": False})
2332
+ except Exception:
2333
+ pass
2220
2334
  yield {
2221
2335
  "event": "cancelled",
2222
2336
  "data": json.dumps(
@@ -2252,6 +2366,30 @@ async def resume_agent(request: ResumeRequest):
2252
2366
  # Process messages (no continue statements to ensure interrupt check always runs)
2253
2367
  if isinstance(step, dict) and "messages" in step:
2254
2368
  messages = step["messages"]
2369
+ current_message_count = len(messages) if messages else 0
2370
+
2371
+ # Detect summarization by checking for lc_source: "summarization" marker
2372
+ # SummarizationMiddleware injects summary into system prompt with this marker
2373
+ if not summarization_detected and messages:
2374
+ for msg in messages:
2375
+ # Check additional_kwargs for lc_source
2376
+ additional_kwargs = getattr(msg, "additional_kwargs", {}) or {}
2377
+ if additional_kwargs.get("lc_source") == "summarization":
2378
+ summarization_detected = True
2379
+ logger.info(
2380
+ f"[Agent-Resume] Summarization detected via lc_source marker"
2381
+ )
2382
+ yield {
2383
+ "event": "debug",
2384
+ "data": json.dumps({
2385
+ "status": "대화가 자동으로 압축되었습니다.",
2386
+ "icon": "check"
2387
+ }),
2388
+ }
2389
+ break
2390
+
2391
+ previous_message_count = current_message_count
2392
+
2255
2393
  should_process_message = False
2256
2394
  if messages:
2257
2395
  last_message = messages[-1]
@@ -2421,10 +2559,12 @@ async def resume_agent(request: ResumeRequest):
2421
2559
  }
2422
2560
  return # Exit the generator
2423
2561
  else:
2424
- logger.warning(
2425
- "Resume: All %d todos completed but no summary JSON in current step - NOT auto-terminating",
2562
+ logger.info(
2563
+ "Resume: All %d todos completed (no summary in step) - "
2564
+ "continuing to wait for final_summary_tool",
2426
2565
  len(todos),
2427
2566
  )
2567
+ # Don't auto-terminate — let agent call final_summary_tool
2428
2568
 
2429
2569
  tool_name = getattr(last_message, "name", "") or ""
2430
2570
  logger.info(
@@ -2628,7 +2768,14 @@ async def resume_agent(request: ResumeRequest):
2628
2768
  has_summary_json = (
2629
2769
  '"summary"' in msg_content
2630
2770
  and '"next_items"' in msg_content
2771
+ ) or (
2772
+ "'summary'" in msg_content
2773
+ and "'next_items'" in msg_content
2631
2774
  )
2775
+ # Check if last_message is a ToolMessage from final_summary_tool
2776
+ is_final_summary_tool_msg = (
2777
+ getattr(last_message, "name", "") or ""
2778
+ ) in ("final_summary_tool", "final_summary")
2632
2779
  # Also check for markdown summary format
2633
2780
  has_markdown_summary = any(
2634
2781
  kw in msg_content
@@ -2642,15 +2789,18 @@ async def resume_agent(request: ResumeRequest):
2642
2789
  ]
2643
2790
  )
2644
2791
  has_summary = (
2645
- has_summary_json or has_markdown_summary
2792
+ has_summary_json
2793
+ or has_markdown_summary
2794
+ or is_final_summary_tool_msg
2646
2795
  )
2647
2796
 
2648
2797
  # Only check current AIMessage for summary (not history, to avoid false positives)
2649
2798
  if not has_summary:
2650
- logger.warning(
2651
- "Resume: All todos completed but no summary JSON in current message - NOT auto-terminating"
2799
+ logger.info(
2800
+ "Resume: All todos completed (no summary in message) - "
2801
+ "continuing to wait for final_summary_tool"
2652
2802
  )
2653
- # Don't terminate - let agent continue to generate summary
2803
+ # Don't auto-terminate let agent call final_summary_tool
2654
2804
  else:
2655
2805
  logger.info(
2656
2806
  "Resume: All %d todos completed and summary exists in current message, auto-terminating",
@@ -2805,6 +2955,10 @@ async def resume_agent(request: ResumeRequest):
2805
2955
  for subagent_event in get_subagent_debug_events():
2806
2956
  yield subagent_event
2807
2957
 
2958
+ # Drain and emit any summarization events (context compression)
2959
+ for summarization_event in get_summarization_debug_events():
2960
+ yield summarization_event
2961
+
2808
2962
  # Check for interrupt AFTER processing todos and messages
2809
2963
  # This ensures todos/debug events are emitted even in interrupt steps
2810
2964
  if isinstance(step, dict) and "__interrupt__" in step:
@@ -2828,7 +2982,10 @@ async def resume_agent(request: ResumeRequest):
2828
2982
  f"[RESUME INTERRUPT] action_requests count: {len(action_requests)}, first: {str(action_requests[0])[:200] if action_requests else 'none'}"
2829
2983
  )
2830
2984
  normalized_actions = [
2831
- _normalize_action_request(a) for a in action_requests
2985
+ _normalize_action_request(
2986
+ a, state=step if isinstance(step, dict) else None
2987
+ )
2988
+ for a in action_requests
2832
2989
  ]
2833
2990
  if normalized_actions:
2834
2991
  _simple_agent_pending_actions[request.threadId] = (