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
@@ -2,29 +2,43 @@
2
2
  SubAgentMiddleware
3
3
 
4
4
  Middleware that enables subagent delegation via the `task` tool.
5
- Based on Deep Agents library pattern (benchmarked, not installed).
6
-
7
- Key features:
8
- - Provides task(agent_name, description) tool for subagent invocation
9
- - Context isolation: subagents run in clean context
10
- - Synchronous execution: subagent returns result directly to caller
11
- - Nested subagent support: python_developer can call athena_query
12
- - Subagent caching: compiled agents are cached to avoid recompilation overhead
5
+ Benchmarked from Deep Agents library pattern.
6
+
7
+ Architecture:
8
+ - Uses ToolRuntime for state access (instead of global variables)
9
+ - Returns Command for state updates (instead of plain strings)
10
+ - Extracts generated code/SQL to state fields (avoids JSON escaping)
11
+ - ContentInjectionMiddleware handles injection into target tools
12
+
13
+ Key patterns from Deep Agents:
14
+ - _EXCLUDED_STATE_KEYS: isolates messages/todos between parent and subagent
15
+ - Command(update={...}): state update return from tools
16
+ - ToolRuntime.state: access graph state from within tools
13
17
  """
14
18
 
19
+ import contextvars
15
20
  import hashlib
16
21
  import json
17
22
  import logging
18
- from typing import TYPE_CHECKING, Any, Dict, List, Optional
23
+ import re
24
+ import time
25
+ import uuid
26
+ from typing import Any, Callable, Dict, List, Optional, Union
19
27
 
20
- from langchain_core.tools import tool
28
+ from langchain_core.messages import HumanMessage, ToolMessage
29
+ from langchain_core.tools import StructuredTool
21
30
  from pydantic import BaseModel, Field
22
31
 
23
- if TYPE_CHECKING:
24
- pass
25
-
26
32
  logger = logging.getLogger(__name__)
27
33
 
34
+ # Deep Agents pattern: state keys excluded from bidirectional sharing
35
+ _EXCLUDED_STATE_KEYS = ("messages", "todos")
36
+
37
+ # Context variable to track the current main agent's thread_id
38
+ _current_thread_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
39
+ "current_thread_id", default=None
40
+ )
41
+
28
42
  # Global registry for subagent factories (set by AgentFactory)
29
43
  _subagent_factory = None
30
44
  _current_llm_config = None
@@ -32,15 +46,15 @@ _current_llm_config = None
32
46
  _subagent_cache: Dict[str, Any] = {}
33
47
 
34
48
 
49
+ # ---------------------------------------------------------------------------
50
+ # Global factory management (backward-compatible)
51
+ # ---------------------------------------------------------------------------
52
+
35
53
  def set_subagent_factory(factory_func, llm_config: Dict[str, Any]):
36
- """
37
- Set the subagent factory function.
38
- Called by AgentFactory during initialization.
39
- """
54
+ """Set the subagent factory function. Called by AgentFactory."""
40
55
  global _subagent_factory, _current_llm_config, _subagent_cache
41
56
  _subagent_factory = factory_func
42
57
  _current_llm_config = llm_config
43
- # Clear cache when factory changes (new LLM config)
44
58
  _subagent_cache.clear()
45
59
  logger.info("SubAgentMiddleware factory initialized (cache cleared)")
46
60
 
@@ -59,12 +73,7 @@ def _get_config_hash(llm_config: Dict[str, Any]) -> str:
59
73
  def get_or_create_subagent(
60
74
  agent_name: str, factory_func, llm_config: Dict[str, Any]
61
75
  ) -> Any:
62
- """
63
- Get cached subagent or create new one.
64
-
65
- Caching avoids expensive recompilation of LangGraph agents.
66
- Cache key = "{agent_name}_{config_hash}" to handle different LLM configs.
67
- """
76
+ """Get cached subagent or create new one."""
68
77
  global _subagent_cache
69
78
 
70
79
  config_hash = _get_config_hash(llm_config)
@@ -80,43 +89,330 @@ def get_or_create_subagent(
80
89
  logger.info(
81
90
  f"Cached subagent '{agent_name}' (total cached: {len(_subagent_cache)})"
82
91
  )
83
-
84
92
  return subagent
85
93
 
86
94
 
87
95
  def clear_subagent_cache():
88
- """Clear the subagent cache. Useful for testing or config changes."""
96
+ """Clear the subagent cache."""
89
97
  global _subagent_cache
90
98
  count = len(_subagent_cache)
91
99
  _subagent_cache.clear()
92
100
  logger.info(f"Subagent cache cleared ({count} entries removed)")
93
101
 
94
102
 
103
+ # ---------------------------------------------------------------------------
104
+ # Thread ID tracking (for code history middleware)
105
+ # ---------------------------------------------------------------------------
106
+
107
+ def set_current_thread_id(thread_id: str) -> None:
108
+ """Set the current main agent's thread_id for code history tracking."""
109
+ _current_thread_id.set(thread_id)
110
+ logger.debug(f"Set current thread_id: {thread_id}")
111
+
112
+
113
+ def get_current_thread_id() -> Optional[str]:
114
+ """Get the current main agent's thread_id."""
115
+ return _current_thread_id.get()
116
+
117
+
118
+ # ---------------------------------------------------------------------------
119
+ # Content extraction utilities
120
+ # ---------------------------------------------------------------------------
121
+
122
+ def extract_code_block(response: str, lang: str = "python") -> Optional[str]:
123
+ """Extract code/SQL from response using [CODE]/[SQL] markers or fenced blocks.
124
+
125
+ Tries in order:
126
+ 1. [CODE] or [SQL] section marker
127
+ 2. ```python or ```sql fenced code block
128
+ 3. ``` generic fenced code block
129
+
130
+ Args:
131
+ response: Full response text from subagent
132
+ lang: Language type ("python" or "sql")
133
+
134
+ Returns:
135
+ Extracted code string, or None if not found
136
+ """
137
+ if not response:
138
+ return None
139
+
140
+ # Pattern 1: [CODE] or [SQL] section marker with fenced block
141
+ marker = "[CODE]" if lang == "python" else "[SQL]"
142
+ marker_pattern = re.escape(marker) + r'\s*\n\s*```(?:\w+)?\s*\n(.*?)```'
143
+ match = re.search(marker_pattern, response, re.DOTALL)
144
+ if match:
145
+ code = match.group(1).strip()
146
+ if code:
147
+ return code
148
+
149
+ # Pattern 2: [CODE] or [SQL] marker with content after it (no fenced block)
150
+ marker_pattern2 = re.escape(marker) + r'\s*\n(.*?)(?=\n\[|\Z)'
151
+ match = re.search(marker_pattern2, response, re.DOTALL)
152
+ if match:
153
+ code = match.group(1).strip()
154
+ # Remove fenced block markers if present
155
+ if code.startswith(f"```{lang}"):
156
+ code = code[len(f"```{lang}"):].strip()
157
+ if code.startswith("```"):
158
+ code = code[3:].strip()
159
+ if code.endswith("```"):
160
+ code = code[:-3].strip()
161
+ if code:
162
+ return code
163
+
164
+ # Pattern 3: Fenced code block with language
165
+ fenced_pattern = rf'```{lang}\s*\n(.*?)```'
166
+ match = re.search(fenced_pattern, response, re.DOTALL)
167
+ if match:
168
+ code = match.group(1).strip()
169
+ if code:
170
+ return code
171
+
172
+ # Pattern 4: Generic fenced code block (fallback)
173
+ generic_pattern = r'```\s*\n(.*?)```'
174
+ match = re.search(generic_pattern, response, re.DOTALL)
175
+ if match:
176
+ code = match.group(1).strip()
177
+ if code and len(code) > 10: # Minimum meaningful code
178
+ return code
179
+
180
+ return None
181
+
182
+
183
+ def extract_description(response: str) -> Optional[str]:
184
+ """Extract [DESCRIPTION] section from subagent response.
185
+
186
+ Args:
187
+ response: Full response text from subagent
188
+
189
+ Returns:
190
+ Description string, or None if not found
191
+ """
192
+ if not response:
193
+ return None
194
+
195
+ patterns = [
196
+ r'\[DESCRIPTION\]\s*\n(.*?)(?=\n\s*\[(?:CODE|SQL)\])',
197
+ r'\[DESCRIPTION\]\s*(.*?)(?=\s*\[(?:CODE|SQL)\])',
198
+ r'\[DESCRIPTION\]\s*\n(.*?)(?=\n\s*```)',
199
+ r'\[DESCRIPTION\]\s*\n(.+?)(?=\n\n|\Z)',
200
+ ]
201
+
202
+ for pattern in patterns:
203
+ match = re.search(pattern, response, re.DOTALL | re.IGNORECASE)
204
+ if match:
205
+ description = match.group(1).strip()
206
+ if description and len(description) > 5:
207
+ return description
208
+
209
+ return None
210
+
211
+
212
+ def extract_generated_content(
213
+ response: str, agent_type: str
214
+ ) -> tuple:
215
+ """Extract code/SQL from subagent response based on agent type.
216
+
217
+ Args:
218
+ response: Full response text from subagent
219
+ agent_type: "python_developer", "athena_query", etc.
220
+
221
+ Returns:
222
+ Tuple of (content, content_type) or (None, None)
223
+ """
224
+ if agent_type == "python_developer":
225
+ code = extract_code_block(response, lang="python")
226
+ return (code, "python") if code else (None, None)
227
+ elif agent_type == "athena_query":
228
+ sql = extract_code_block(response, lang="sql")
229
+ return (sql, "sql") if sql else (None, None)
230
+ return (None, None)
231
+
232
+
233
+ def _build_summary(
234
+ response: str,
235
+ agent_type: str,
236
+ content_extracted: bool = False,
237
+ ) -> str:
238
+ """Build ToolMessage content for the caller.
239
+
240
+ When content is extracted to state, returns a short summary.
241
+ Otherwise returns the full response.
242
+
243
+ Args:
244
+ response: Full response from subagent
245
+ agent_type: Type of the subagent
246
+ content_extracted: Whether content was extracted to state
247
+
248
+ Returns:
249
+ Summary or full response string
250
+ """
251
+ if not content_extracted:
252
+ return response
253
+
254
+ # Content extracted - return summary only (actual content is in state)
255
+ desc = extract_description(response)
256
+ desc_part = f"\n설명: {desc}" if desc else ""
257
+
258
+ if agent_type == "python_developer":
259
+ return (
260
+ f"python_developer 코드 생성 완료.{desc_part}\n"
261
+ "jupyter_cell_tool() 또는 write_file_tool()로 실행/저장하세요. "
262
+ "(코드는 자동 주입됩니다)"
263
+ )
264
+ elif agent_type == "athena_query":
265
+ return (
266
+ f"athena_query SQL 생성 완료.{desc_part}\n"
267
+ "markdown_tool()로 표시하세요. "
268
+ "(SQL은 자동 주입됩니다)"
269
+ )
270
+ return response
271
+
272
+
273
+ # ---------------------------------------------------------------------------
274
+ # Task tool creation
275
+ # ---------------------------------------------------------------------------
276
+
277
+ def _invoke_subagent(
278
+ caller_name: str,
279
+ agent_name: str,
280
+ description: str,
281
+ context: Optional[str],
282
+ runtime_state: Optional[Dict[str, Any]] = None,
283
+ thread_id: Optional[str] = None,
284
+ ) -> Dict[str, Any]:
285
+ """Execute a subagent and return its result dict.
286
+
287
+ Shared logic between Main Agent task tool and nested task tool.
288
+
289
+ Args:
290
+ caller_name: Name of the calling agent
291
+ agent_name: Name of the subagent to invoke
292
+ description: Task description
293
+ context: Optional additional context
294
+ runtime_state: Optional state from ToolRuntime (for state sharing)
295
+ thread_id: Optional main agent thread_id (for code history lookup)
296
+
297
+ Returns:
298
+ Result dict from subagent.invoke()
299
+ """
300
+ from agent_server.langchain.middleware.subagent_events import (
301
+ clear_current_subagent,
302
+ emit_subagent_complete,
303
+ emit_subagent_start,
304
+ set_current_subagent,
305
+ )
306
+
307
+ emit_subagent_start(agent_name, description)
308
+
309
+ factory_func, llm_config = get_subagent_factory()
310
+ if factory_func is None:
311
+ raise RuntimeError("SubAgentMiddleware not initialized. Call set_subagent_factory first.")
312
+
313
+ try:
314
+ set_current_subagent(agent_name)
315
+
316
+ t0 = time.time()
317
+ subagent = get_or_create_subagent(agent_name, factory_func, llm_config)
318
+ logger.info(f"[TIMING] get_or_create_subagent took {time.time()-t0:.2f}s")
319
+
320
+ # Inject code history for python_developer
321
+ enhanced_context = context
322
+ if agent_name == "python_developer":
323
+ try:
324
+ from agent_server.langchain.middleware.code_history_middleware import (
325
+ get_code_history_tracker,
326
+ get_context_with_history,
327
+ )
328
+ main_thread_id = thread_id or get_current_thread_id()
329
+ tracker = get_code_history_tracker(main_thread_id)
330
+ if tracker.get_entry_count() > 0:
331
+ enhanced_context = get_context_with_history(context, main_thread_id)
332
+ logger.info(
333
+ f"[TIMING] code history injected "
334
+ f"(entries={tracker.get_entry_count()}, "
335
+ f"context_len={len(enhanced_context) if enhanced_context else 0})"
336
+ )
337
+ except Exception as e:
338
+ logger.warning(f"Failed to inject code history: {e}")
339
+
340
+ # Build message content
341
+ if enhanced_context:
342
+ message_content = f"## Task\n{description}\n\n## Context (provided by Main Agent)\n{enhanced_context}"
343
+ else:
344
+ message_content = description
345
+
346
+ logger.info(f"[{caller_name}] Subagent message length: {len(message_content)}")
347
+
348
+ # Build subagent input state (Deep Agents pattern)
349
+ if runtime_state is not None:
350
+ subagent_state = {
351
+ k: v for k, v in runtime_state.items()
352
+ if k not in _EXCLUDED_STATE_KEYS
353
+ }
354
+ subagent_state["messages"] = [HumanMessage(content=message_content)]
355
+ else:
356
+ subagent_state = {"messages": [{"role": "user", "content": message_content}]}
357
+
358
+ subagent_thread_id = f"subagent-{agent_name}-{uuid.uuid4().hex[:8]}"
359
+ subagent_config = {"configurable": {"thread_id": subagent_thread_id}}
360
+
361
+ t_invoke = time.time()
362
+ logger.info(f"[TIMING] About to invoke subagent '{agent_name}'...")
363
+ result = subagent.invoke(subagent_state, config=subagent_config)
364
+ logger.info(f"[TIMING] subagent.invoke() took {time.time()-t_invoke:.2f}s")
365
+
366
+ # Extract response text
367
+ messages = result.get("messages", [])
368
+ if messages:
369
+ final_message = messages[-1]
370
+ response_text = (
371
+ final_message.content
372
+ if hasattr(final_message, "content")
373
+ else str(final_message)
374
+ )
375
+ else:
376
+ response_text = "Subagent completed but returned no messages."
377
+
378
+ logger.info(
379
+ f"[{caller_name}] Subagent '{agent_name}' returned: {str(response_text)[:200]}..."
380
+ )
381
+
382
+ emit_subagent_complete(agent_name, str(response_text)[:100])
383
+ return result
384
+
385
+ except Exception as e:
386
+ error_msg = f"Subagent '{agent_name}' failed: {str(e)}"
387
+ logger.error(error_msg, exc_info=True)
388
+ emit_subagent_complete(agent_name, f"Error: {str(e)[:50]}")
389
+ raise
390
+ finally:
391
+ clear_current_subagent()
392
+
393
+
95
394
  def create_task_tool(
96
395
  caller_name: str,
97
396
  allowed_subagents: Optional[List[str]] = None,
98
- ):
99
- """
100
- Create a task tool for calling subagents.
397
+ ) -> StructuredTool:
398
+ """Create a task tool for nested subagent calls (returns plain string).
101
399
 
102
- The task tool executes subagents synchronously and returns their result.
103
- Subagents like python_developer return generated code/analysis,
104
- which the Main Agent can then execute if needed.
400
+ Used by subagents that can call other subagents (e.g., python_developer athena_query).
401
+ Returns full response text as string (no Command/state extraction).
105
402
 
106
403
  Args:
107
- caller_name: Name of the agent creating this tool (for logging/validation)
108
- allowed_subagents: Optional list of subagent names this agent can call.
109
- If None, all subagents are allowed (for Main Agent).
404
+ caller_name: Name of the agent creating this tool
405
+ allowed_subagents: Optional list of allowed subagent names
110
406
 
111
407
  Returns:
112
- A tool that can be used to delegate tasks to subagents.
408
+ StructuredTool for subagent delegation
113
409
  """
114
410
  from agent_server.langchain.subagents.base import (
115
411
  SUBAGENT_CONFIGS,
116
412
  get_subagent_config,
117
413
  )
118
414
 
119
- # Build description based on allowed subagents
415
+ # Build description
120
416
  if allowed_subagents:
121
417
  available = [
122
418
  f"- {name}: {SUBAGENT_CONFIGS[name].description}"
@@ -124,261 +420,251 @@ def create_task_tool(
124
420
  if name in SUBAGENT_CONFIGS
125
421
  ]
126
422
  else:
127
- # Main Agent (planner) can call non-restricted subagents OR those explicitly allowing "planner"
128
423
  available = [
129
424
  f"- {config.name}: {config.description}"
130
425
  for config in SUBAGENT_CONFIGS.values()
131
426
  if not config.callable_by or caller_name in config.callable_by
132
427
  ]
133
-
134
428
  available_str = "\n".join(available)
135
429
 
136
- # Create Pydantic schema for the task tool (required for Gemini compatibility)
137
- class TaskInput(BaseModel):
138
- """Input schema for task tool"""
139
-
140
- agent_name: str = Field(
141
- description=f"Name of the subagent to invoke. Available: {', '.join(allowed_subagents) if allowed_subagents else 'python_developer, researcher, athena_query'}"
142
- )
143
- description: str = Field(
144
- description="Detailed task description for the subagent (Korean preferred)"
145
- )
146
- context: Optional[str] = Field(
147
- default=None,
148
- description="Additional context for the subagent: resource info (file sizes, memory), previous code, variable state, etc.",
149
- )
150
-
151
- @tool(args_schema=TaskInput)
152
430
  def task_tool(
153
- agent_name: str, description: str, context: Optional[str] = None
431
+ agent_name: str,
432
+ description: str,
433
+ context: Optional[str] = None,
154
434
  ) -> str:
155
- """
156
- Delegate a task to a specialized subagent.
157
-
158
- The subagent will execute the task and return its result (code, analysis, etc.).
159
- Code execution tools (jupyter_cell_tool, write_file_tool) are handled by Main Agent.
160
-
161
- Args:
162
- agent_name: Name of the subagent to invoke
163
- description: Detailed task description for the subagent
164
- context: Additional context (resource info, previous code, etc.)
165
-
166
- Returns:
167
- Result from the subagent execution (string summary or generated code)
168
- """
169
- # Validate subagent exists
435
+ """Delegate a task to a specialized subagent (nested call)."""
170
436
  if agent_name not in SUBAGENT_CONFIGS:
171
- return f"Error: Unknown agent '{agent_name}'. Available agents:\n{available_str}"
437
+ return f"Error: Unknown agent '{agent_name}'. Available:\n{available_str}"
172
438
 
173
- # Validate caller is allowed to call this subagent
174
439
  config = get_subagent_config(agent_name)
175
440
  if allowed_subagents and agent_name not in allowed_subagents:
176
441
  return f"Error: '{caller_name}' cannot call '{agent_name}'. Allowed: {allowed_subagents}"
177
-
178
442
  if config.callable_by and caller_name not in config.callable_by:
179
443
  return f"Error: '{agent_name}' can only be called by: {config.callable_by}"
180
444
 
181
- logger.info(
182
- f"[{caller_name}] Invoking subagent '{agent_name}': {description[:100]}..."
183
- )
445
+ try:
446
+ result = _invoke_subagent(caller_name, agent_name, description, context)
447
+ messages = result.get("messages", [])
448
+ if messages:
449
+ final = messages[-1]
450
+ return final.content if hasattr(final, "content") else str(final)
451
+ return "Subagent completed but returned no messages."
452
+ except Exception as e:
453
+ return f"Error: Subagent '{agent_name}' failed: {str(e)}"
184
454
 
185
- # Import subagent event emitters
186
- from agent_server.langchain.middleware.subagent_events import (
187
- clear_current_subagent,
188
- emit_subagent_complete,
189
- emit_subagent_start,
190
- set_current_subagent,
191
- )
455
+ return StructuredTool.from_function(
456
+ name="task_tool",
457
+ func=task_tool,
458
+ description=f"Delegate a task to a specialized subagent.\n\nAvailable agents:\n{available_str}",
459
+ )
192
460
 
193
- # Emit subagent start event for UI
194
- emit_subagent_start(agent_name, description)
195
461
 
196
- # Get the factory and config
197
- factory_func, llm_config = get_subagent_factory()
198
- if factory_func is None:
199
- return "Error: SubAgentMiddleware not initialized. Call set_subagent_factory first."
462
+ def _create_main_task_tool(
463
+ caller_name: str,
464
+ allowed_subagents: Optional[List[str]] = None,
465
+ ) -> StructuredTool:
466
+ """Create a task tool for Main Agent (returns Command with state update).
200
467
 
201
- try:
202
- import time
203
-
204
- # Set current subagent context for tool call tracking
205
- set_current_subagent(agent_name)
206
-
207
- # Get or create the subagent (cached for performance)
208
- # Avoids expensive LangGraph recompilation on each call
209
- t0 = time.time()
210
- subagent = get_or_create_subagent(agent_name, factory_func, llm_config)
211
- t1 = time.time()
212
- logger.info(f"[TIMING] get_or_create_subagent took {t1-t0:.2f}s")
213
-
214
- # Execute subagent synchronously with clean context
215
- # The subagent runs in isolation, receiving task description + optional context
216
- import uuid
217
-
218
- subagent_thread_id = f"subagent-{agent_name}-{uuid.uuid4().hex[:8]}"
219
- subagent_config = {
220
- "configurable": {
221
- "thread_id": subagent_thread_id,
222
- }
223
- }
468
+ Uses ToolRuntime for state access and returns Command to update
469
+ generated_content/generated_content_type/content_description in state.
224
470
 
225
- # Inject code history for python_developer
226
- enhanced_context = context
227
- if agent_name == "python_developer":
228
- try:
229
- t2 = time.time()
230
- from agent_server.langchain.middleware.code_history_middleware import (
231
- get_code_history_tracker,
232
- get_context_with_history,
233
- )
471
+ Args:
472
+ caller_name: Name of the calling agent (usually "planner")
473
+ allowed_subagents: Optional list of allowed subagent names
234
474
 
235
- tracker = get_code_history_tracker()
236
- if tracker.get_entry_count() > 0:
237
- enhanced_context = get_context_with_history(context)
238
- t3 = time.time()
239
- logger.info(
240
- f"[TIMING] code history injection took {t3-t2:.2f}s "
241
- f"(entries={tracker.get_entry_count()}, "
242
- f"context_len={len(enhanced_context) if enhanced_context else 0})"
243
- )
244
- except Exception as e:
245
- logger.warning(f"Failed to inject code history: {e}")
475
+ Returns:
476
+ StructuredTool that returns Command
477
+ """
478
+ # Lazy import to avoid circular dependency
479
+ from langchain.tools import ToolRuntime
480
+ from langgraph.types import Command
481
+
482
+ from agent_server.langchain.subagents.base import (
483
+ SUBAGENT_CONFIGS,
484
+ get_subagent_config,
485
+ )
486
+
487
+ # Build description
488
+ if allowed_subagents:
489
+ available = [
490
+ f"- {name}: {SUBAGENT_CONFIGS[name].description}"
491
+ for name in allowed_subagents
492
+ if name in SUBAGENT_CONFIGS
493
+ ]
494
+ else:
495
+ available = [
496
+ f"- {config.name}: {config.description}"
497
+ for config in SUBAGENT_CONFIGS.values()
498
+ if not config.callable_by or caller_name in config.callable_by
499
+ ]
500
+ available_str = "\n".join(available)
246
501
 
247
- # Build the message content with optional context
248
- if enhanced_context:
249
- message_content = f"""## Task
250
- {description}
502
+ def task(
503
+ description: str,
504
+ subagent_type: str,
505
+ runtime: ToolRuntime,
506
+ ) -> str:
507
+ """Delegate a task to a specialized subagent.
251
508
 
252
- ## Context (provided by Main Agent)
253
- {enhanced_context}"""
254
- else:
255
- message_content = description
509
+ The subagent executes the task and returns its result.
510
+ For python_developer and athena_query, generated code/SQL is stored
511
+ in state and auto-injected into target tools by ContentInjectionMiddleware.
256
512
 
257
- logger.info(
258
- f"[{caller_name}] Subagent message length: {len(message_content)}"
259
- )
513
+ Args:
514
+ description: Detailed task description for the subagent
515
+ subagent_type: Name of the subagent to invoke
516
+ runtime: ToolRuntime (auto-injected by LangGraph)
260
517
 
261
- # Execute the subagent
262
- t_invoke_start = time.time()
263
- logger.info(f"[TIMING] About to invoke subagent '{agent_name}'...")
264
- result = subagent.invoke(
265
- {"messages": [{"role": "user", "content": message_content}]},
266
- config=subagent_config,
267
- )
268
- t_invoke_end = time.time()
269
- logger.info(
270
- f"[TIMING] subagent.invoke() took {t_invoke_end-t_invoke_start:.2f}s"
518
+ Returns:
519
+ Command with state update, or error string
520
+ """
521
+ if subagent_type not in SUBAGENT_CONFIGS:
522
+ return f"Error: Unknown agent '{subagent_type}'. Available:\n{available_str}"
523
+
524
+ config = get_subagent_config(subagent_type)
525
+ if allowed_subagents and subagent_type not in allowed_subagents:
526
+ return f"Error: '{caller_name}' cannot call '{subagent_type}'."
527
+ if config.callable_by and caller_name not in config.callable_by:
528
+ return f"Error: '{subagent_type}' can only be called by: {config.callable_by}"
529
+
530
+ try:
531
+ # Extract thread_id from runtime config for code history lookup
532
+ main_thread_id = None
533
+ if hasattr(runtime, "config") and runtime.config:
534
+ configurable = runtime.config.get("configurable", {})
535
+ main_thread_id = configurable.get("thread_id")
536
+
537
+ # Invoke subagent with state sharing (Deep Agents pattern)
538
+ result = _invoke_subagent(
539
+ caller_name,
540
+ subagent_type,
541
+ description,
542
+ context=None,
543
+ runtime_state=dict(runtime.state) if runtime.state else None,
544
+ thread_id=main_thread_id,
271
545
  )
272
546
 
273
- # Extract the final message from the result
547
+ # Extract response text
274
548
  messages = result.get("messages", [])
549
+ response_text = ""
275
550
  if messages:
276
- final_message = messages[-1]
277
- if hasattr(final_message, "content"):
278
- response = final_message.content
279
- else:
280
- response = str(final_message)
281
- else:
282
- response = "Subagent completed but returned no messages."
283
-
284
- logger.info(
285
- f"[{caller_name}] Subagent '{agent_name}' returned: {str(response)[:200]}..."
286
- )
287
-
288
- # Extract description from python_developer response for auto-injection
289
- if agent_name == "python_developer":
290
- try:
291
- from agent_server.langchain.middleware.description_injector import (
292
- process_task_tool_response,
293
- )
551
+ final = messages[-1]
552
+ response_text = (
553
+ final.content if hasattr(final, "content") else str(final)
554
+ )
555
+
556
+ # Extract generated content to state (Deep Agents Command pattern)
557
+ content, content_type = extract_generated_content(response_text, subagent_type)
558
+ desc = extract_description(response_text)
559
+
560
+ # Build state update (exclude messages/todos like Deep Agents)
561
+ state_update: Dict[str, Any] = {
562
+ k: v for k, v in result.items()
563
+ if k not in _EXCLUDED_STATE_KEYS
564
+ }
294
565
 
295
- process_task_tool_response(agent_name, str(response))
296
- except Exception as e:
297
- logger.warning(f"Failed to extract description: {e}")
566
+ if content:
567
+ state_update["generated_content"] = content
568
+ state_update["generated_content_type"] = content_type
569
+ state_update["content_description"] = desc
570
+ logger.info(
571
+ f"[State] Extracted {content_type} content "
572
+ f"({len(content)} chars) to state"
573
+ )
574
+
575
+ # Build ToolMessage (summary for Main Agent, since content is in state)
576
+ summary = _build_summary(
577
+ response_text,
578
+ subagent_type,
579
+ content_extracted=bool(content),
580
+ )
298
581
 
299
- # Emit subagent complete event for UI
300
- emit_subagent_complete(agent_name, str(response)[:100])
582
+ if not runtime.tool_call_id:
583
+ raise ValueError("Tool call ID is required for subagent invocation")
301
584
 
302
- return response
585
+ return Command(
586
+ update={
587
+ **state_update,
588
+ "messages": [
589
+ ToolMessage(
590
+ content=summary,
591
+ tool_call_id=runtime.tool_call_id,
592
+ )
593
+ ],
594
+ }
595
+ )
303
596
 
304
597
  except Exception as e:
305
- error_msg = f"Subagent '{agent_name}' failed: {str(e)}"
598
+ error_msg = f"Error: Subagent '{subagent_type}' failed: {str(e)}"
306
599
  logger.error(error_msg, exc_info=True)
307
- # Emit complete event even on error
308
- emit_subagent_complete(agent_name, f"Error: {str(e)[:50]}")
309
- return f"Error: {error_msg}"
310
- finally:
311
- # Always clear subagent context
312
- clear_current_subagent()
313
-
314
- # Update tool docstring with available agents
315
- task_tool.__doc__ = f"""Delegate a task to a specialized subagent.
316
-
317
- Available agents:
318
- {available_str}
319
-
320
- The subagent will analyze the task and return its result.
321
- - python_developer: Returns generated Python code and analysis
322
- - researcher: Returns search results and findings
323
- - athena_query: Returns SQL query string
324
-
325
- For code execution (running Python, writing files), use Main Agent's tools directly.
326
-
327
- IMPORTANT: For python_developer, ALWAYS provide context with:
328
- - Resource info (file sizes, memory) from check_resource_tool
329
- - Previous code context if building on existing work
330
- - Variable names and their current state
331
-
332
- Args:
333
- agent_name: Name of the subagent to invoke
334
- description: Detailed task description for the subagent
335
- context: Additional context (resource info, previous code, etc.)
336
-
337
- Returns:
338
- Result from the subagent execution
339
- """
600
+ return error_msg
601
+
602
+ return StructuredTool.from_function(
603
+ name="task_tool",
604
+ func=task,
605
+ description=(
606
+ f"Delegate a task to a specialized subagent.\n\n"
607
+ f"Available agents:\n{available_str}\n\n"
608
+ "Generated code/SQL is automatically injected into execution tools.\n"
609
+ "After calling task_tool, use the appropriate tool to execute/display:\n"
610
+ "- python_developer → jupyter_cell_tool() or write_file_tool()\n"
611
+ "- athena_query → markdown_tool()\n"
612
+ "- researcher → direct response"
613
+ ),
614
+ )
340
615
 
341
- return task_tool
342
616
 
617
+ # ---------------------------------------------------------------------------
618
+ # SubAgentMiddleware (extends AgentMiddleware)
619
+ # ---------------------------------------------------------------------------
343
620
 
344
621
  class SubAgentMiddleware:
345
- """
346
- Middleware that adds subagent delegation capability.
622
+ """Middleware that adds subagent delegation via the task tool.
347
623
 
348
- This middleware:
349
- 1. Adds the `task` tool to the agent's toolset
350
- 2. The task tool executes subagents synchronously
351
- 3. Subagent results are returned directly to the caller
624
+ Benchmarked from Deep Agents library pattern:
625
+ - Extends AgentMiddleware (tools auto-registered, wrap_model_call hook)
626
+ - Task tool uses ToolRuntime for state access
627
+ - Returns Command for state updates (generated_content in state)
628
+ - ContentInjectionMiddleware handles injection into target tools
352
629
 
353
630
  Usage:
354
631
  middleware = SubAgentMiddleware(
355
- caller_name="main_agent",
356
- allowed_subagents=["python_developer", "researcher"],
632
+ caller_name="planner",
633
+ allowed_subagents=None,
357
634
  )
358
635
 
359
636
  agent = create_agent(
360
637
  model=llm,
361
638
  tools=tools,
362
639
  middleware=[middleware, ...],
640
+ state_schema=HDSPAgentState,
363
641
  )
364
642
  """
365
643
 
644
+ TASK_SYSTEM_PROMPT = """## task_tool 사용법
645
+
646
+ task_tool로 서브에이전트에게 작업을 위임하세요.
647
+ 서브에이전트가 생성한 코드/SQL은 자동으로 실행 도구에 주입됩니다.
648
+
649
+ task_tool 호출 후 처리:
650
+ - python_developer → jupyter_cell_tool() 호출 (코드 자동 주입)
651
+ - python_developer (파일 저장) → write_file_tool() 호출 (코드 자동 주입)
652
+ - athena_query → markdown_tool() 호출 (SQL 자동 주입)
653
+ - researcher → 응답 내용 직접 활용"""
654
+
366
655
  def __init__(
367
656
  self,
368
657
  caller_name: str,
369
658
  allowed_subagents: Optional[List[str]] = None,
370
659
  ):
371
- """
372
- Initialize SubAgentMiddleware.
373
-
374
- Args:
375
- caller_name: Name of the agent using this middleware
376
- allowed_subagents: List of subagents this agent can call.
377
- None means all non-restricted subagents.
378
- """
379
660
  self.caller_name = caller_name
380
661
  self.allowed_subagents = allowed_subagents
381
- self.task_tool = create_task_tool(caller_name, allowed_subagents)
662
+
663
+ # Create task tool (auto-registered via self.tools)
664
+ self.task_tool = _create_main_task_tool(caller_name, allowed_subagents)
665
+
666
+ # AgentMiddleware convention: tools attribute is auto-registered by create_agent
667
+ self.tools = [self.task_tool]
382
668
 
383
669
  logger.info(
384
670
  f"SubAgentMiddleware initialized for '{caller_name}' "
@@ -386,13 +672,5 @@ class SubAgentMiddleware:
386
672
  )
387
673
 
388
674
  def get_tools(self) -> List[Any]:
389
- """Get the tools provided by this middleware."""
675
+ """Get the tools provided by this middleware (backward compat)."""
390
676
  return [self.task_tool]
391
-
392
- def __call__(self, tools: List[Any]) -> List[Any]:
393
- """
394
- Add task tool to the agent's toolset.
395
-
396
- This is called during agent creation to augment the tool list.
397
- """
398
- return tools + [self.task_tool]