hdsp-jupyter-extension 2.0.27__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 (70) 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 -31
  8. agent_server/langchain/custom_middleware.py +268 -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/content_injection_middleware.py +110 -0
  13. agent_server/langchain/middleware/subagent_events.py +88 -9
  14. agent_server/langchain/middleware/subagent_middleware.py +501 -245
  15. agent_server/langchain/prompts.py +5 -22
  16. agent_server/langchain/state_schema.py +44 -0
  17. agent_server/langchain/tools/jupyter_tools.py +4 -5
  18. agent_server/langchain/tools/tool_registry.py +6 -0
  19. agent_server/routers/chat.py +305 -2
  20. agent_server/routers/config.py +193 -8
  21. agent_server/routers/config_schema.py +254 -0
  22. agent_server/routers/context.py +31 -8
  23. agent_server/routers/langchain_agent.py +276 -155
  24. hdsp_agent_core/managers/config_manager.py +100 -1
  25. {hdsp_jupyter_extension-2.0.27.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
  26. {hdsp_jupyter_extension-2.0.27.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
  27. hdsp_jupyter_extension-2.0.27.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
  28. hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.55727265b00191e68d9a.js.map +1 -0
  29. 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
  30. hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.df05d90f366bfd5fa023.js.map +1 -0
  31. hdsp_jupyter_extension-2.0.27.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4ab73bb5068405670214.js → hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.08fce819ee32e9d25175.js +3 -3
  32. jupyter_ext/labextension/static/remoteEntry.4ab73bb5068405670214.js.map → hdsp_jupyter_extension-2.0.28.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.08fce819ee32e9d25175.js.map +1 -1
  33. {hdsp_jupyter_extension-2.0.27.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/METADATA +1 -1
  34. {hdsp_jupyter_extension-2.0.27.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/RECORD +65 -63
  35. jupyter_ext/_version.py +1 -1
  36. jupyter_ext/handlers.py +41 -0
  37. jupyter_ext/labextension/build_log.json +1 -1
  38. jupyter_ext/labextension/package.json +2 -2
  39. jupyter_ext/labextension/static/{frontend_styles_index_js.b5e4416b4e07ec087aad.js → frontend_styles_index_js.55727265b00191e68d9a.js} +479 -15
  40. jupyter_ext/labextension/static/frontend_styles_index_js.55727265b00191e68d9a.js.map +1 -0
  41. hdsp_jupyter_extension-2.0.27.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js → jupyter_ext/labextension/static/lib_index_js.df05d90f366bfd5fa023.js +1287 -190
  42. jupyter_ext/labextension/static/lib_index_js.df05d90f366bfd5fa023.js.map +1 -0
  43. jupyter_ext/labextension/static/{remoteEntry.4ab73bb5068405670214.js → remoteEntry.08fce819ee32e9d25175.js} +3 -3
  44. hdsp_jupyter_extension-2.0.27.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4ab73bb5068405670214.js.map → jupyter_ext/labextension/static/remoteEntry.08fce819ee32e9d25175.js.map +1 -1
  45. agent_server/langchain/middleware/description_injector.py +0 -150
  46. hdsp_jupyter_extension-2.0.27.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js.map +0 -1
  47. hdsp_jupyter_extension-2.0.27.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js.map +0 -1
  48. jupyter_ext/labextension/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js.map +0 -1
  49. jupyter_ext/labextension/static/lib_index_js.67505497667f9c0a763d.js.map +0 -1
  50. {hdsp_jupyter_extension-2.0.27.data → hdsp_jupyter_extension-2.0.28.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
  51. {hdsp_jupyter_extension-2.0.27.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  52. {hdsp_jupyter_extension-2.0.27.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
  53. {hdsp_jupyter_extension-2.0.27.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
  54. {hdsp_jupyter_extension-2.0.27.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
  55. {hdsp_jupyter_extension-2.0.27.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
  56. {hdsp_jupyter_extension-2.0.27.data → hdsp_jupyter_extension-2.0.28.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
  57. {hdsp_jupyter_extension-2.0.27.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
  58. {hdsp_jupyter_extension-2.0.27.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
  59. {hdsp_jupyter_extension-2.0.27.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
  60. {hdsp_jupyter_extension-2.0.27.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
  61. {hdsp_jupyter_extension-2.0.27.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
  62. {hdsp_jupyter_extension-2.0.27.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
  63. {hdsp_jupyter_extension-2.0.27.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
  64. {hdsp_jupyter_extension-2.0.27.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
  65. {hdsp_jupyter_extension-2.0.27.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
  66. {hdsp_jupyter_extension-2.0.27.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
  67. {hdsp_jupyter_extension-2.0.27.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
  68. {hdsp_jupyter_extension-2.0.27.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
  69. {hdsp_jupyter_extension-2.0.27.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/WHEEL +0 -0
  70. {hdsp_jupyter_extension-2.0.27.dist-info → hdsp_jupyter_extension-2.0.28.dist-info}/licenses/LICENSE +0 -0
@@ -2,30 +2,38 @@
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
 
15
19
  import contextvars
16
20
  import hashlib
17
21
  import json
18
22
  import logging
19
- 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
20
27
 
21
- from langchain_core.tools import tool
28
+ from langchain_core.messages import HumanMessage, ToolMessage
29
+ from langchain_core.tools import StructuredTool
22
30
  from pydantic import BaseModel, Field
23
31
 
24
- if TYPE_CHECKING:
25
- pass
26
-
27
32
  logger = logging.getLogger(__name__)
28
33
 
34
+ # Deep Agents pattern: state keys excluded from bidirectional sharing
35
+ _EXCLUDED_STATE_KEYS = ("messages", "todos")
36
+
29
37
  # Context variable to track the current main agent's thread_id
30
38
  _current_thread_id: contextvars.ContextVar[Optional[str]] = contextvars.ContextVar(
31
39
  "current_thread_id", default=None
@@ -38,15 +46,15 @@ _current_llm_config = None
38
46
  _subagent_cache: Dict[str, Any] = {}
39
47
 
40
48
 
49
+ # ---------------------------------------------------------------------------
50
+ # Global factory management (backward-compatible)
51
+ # ---------------------------------------------------------------------------
52
+
41
53
  def set_subagent_factory(factory_func, llm_config: Dict[str, Any]):
42
- """
43
- Set the subagent factory function.
44
- Called by AgentFactory during initialization.
45
- """
54
+ """Set the subagent factory function. Called by AgentFactory."""
46
55
  global _subagent_factory, _current_llm_config, _subagent_cache
47
56
  _subagent_factory = factory_func
48
57
  _current_llm_config = llm_config
49
- # Clear cache when factory changes (new LLM config)
50
58
  _subagent_cache.clear()
51
59
  logger.info("SubAgentMiddleware factory initialized (cache cleared)")
52
60
 
@@ -65,12 +73,7 @@ def _get_config_hash(llm_config: Dict[str, Any]) -> str:
65
73
  def get_or_create_subagent(
66
74
  agent_name: str, factory_func, llm_config: Dict[str, Any]
67
75
  ) -> Any:
68
- """
69
- Get cached subagent or create new one.
70
-
71
- Caching avoids expensive recompilation of LangGraph agents.
72
- Cache key = "{agent_name}_{config_hash}" to handle different LLM configs.
73
- """
76
+ """Get cached subagent or create new one."""
74
77
  global _subagent_cache
75
78
 
76
79
  config_hash = _get_config_hash(llm_config)
@@ -86,18 +89,21 @@ def get_or_create_subagent(
86
89
  logger.info(
87
90
  f"Cached subagent '{agent_name}' (total cached: {len(_subagent_cache)})"
88
91
  )
89
-
90
92
  return subagent
91
93
 
92
94
 
93
95
  def clear_subagent_cache():
94
- """Clear the subagent cache. Useful for testing or config changes."""
96
+ """Clear the subagent cache."""
95
97
  global _subagent_cache
96
98
  count = len(_subagent_cache)
97
99
  _subagent_cache.clear()
98
100
  logger.info(f"Subagent cache cleared ({count} entries removed)")
99
101
 
100
102
 
103
+ # ---------------------------------------------------------------------------
104
+ # Thread ID tracking (for code history middleware)
105
+ # ---------------------------------------------------------------------------
106
+
101
107
  def set_current_thread_id(thread_id: str) -> None:
102
108
  """Set the current main agent's thread_id for code history tracking."""
103
109
  _current_thread_id.set(thread_id)
@@ -109,31 +115,304 @@ def get_current_thread_id() -> Optional[str]:
109
115
  return _current_thread_id.get()
110
116
 
111
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
+
112
394
  def create_task_tool(
113
395
  caller_name: str,
114
396
  allowed_subagents: Optional[List[str]] = None,
115
- ):
116
- """
117
- Create a task tool for calling subagents.
397
+ ) -> StructuredTool:
398
+ """Create a task tool for nested subagent calls (returns plain string).
118
399
 
119
- The task tool executes subagents synchronously and returns their result.
120
- Subagents like python_developer return generated code/analysis,
121
- 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).
122
402
 
123
403
  Args:
124
- caller_name: Name of the agent creating this tool (for logging/validation)
125
- allowed_subagents: Optional list of subagent names this agent can call.
126
- 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
127
406
 
128
407
  Returns:
129
- A tool that can be used to delegate tasks to subagents.
408
+ StructuredTool for subagent delegation
130
409
  """
131
410
  from agent_server.langchain.subagents.base import (
132
411
  SUBAGENT_CONFIGS,
133
412
  get_subagent_config,
134
413
  )
135
414
 
136
- # Build description based on allowed subagents
415
+ # Build description
137
416
  if allowed_subagents:
138
417
  available = [
139
418
  f"- {name}: {SUBAGENT_CONFIGS[name].description}"
@@ -141,266 +420,251 @@ def create_task_tool(
141
420
  if name in SUBAGENT_CONFIGS
142
421
  ]
143
422
  else:
144
- # Main Agent (planner) can call non-restricted subagents OR those explicitly allowing "planner"
145
423
  available = [
146
424
  f"- {config.name}: {config.description}"
147
425
  for config in SUBAGENT_CONFIGS.values()
148
426
  if not config.callable_by or caller_name in config.callable_by
149
427
  ]
150
-
151
428
  available_str = "\n".join(available)
152
429
 
153
- # Create Pydantic schema for the task tool (required for Gemini compatibility)
154
- class TaskInput(BaseModel):
155
- """Input schema for task tool"""
156
-
157
- agent_name: str = Field(
158
- description=f"Name of the subagent to invoke. Available: {', '.join(allowed_subagents) if allowed_subagents else 'python_developer, researcher, athena_query'}"
159
- )
160
- description: str = Field(
161
- description="Detailed task description for the subagent (Korean preferred)"
162
- )
163
- context: Optional[str] = Field(
164
- default=None,
165
- description="Additional context for the subagent: resource info (file sizes, memory), previous code, variable state, etc.",
166
- )
167
-
168
- @tool(args_schema=TaskInput)
169
430
  def task_tool(
170
- agent_name: str, description: str, context: Optional[str] = None
431
+ agent_name: str,
432
+ description: str,
433
+ context: Optional[str] = None,
171
434
  ) -> str:
172
- """
173
- Delegate a task to a specialized subagent.
174
-
175
- The subagent will execute the task and return its result (code, analysis, etc.).
176
- Code execution tools (jupyter_cell_tool, write_file_tool) are handled by Main Agent.
177
-
178
- Args:
179
- agent_name: Name of the subagent to invoke
180
- description: Detailed task description for the subagent
181
- context: Additional context (resource info, previous code, etc.)
182
-
183
- Returns:
184
- Result from the subagent execution (string summary or generated code)
185
- """
186
- # Validate subagent exists
435
+ """Delegate a task to a specialized subagent (nested call)."""
187
436
  if agent_name not in SUBAGENT_CONFIGS:
188
- return f"Error: Unknown agent '{agent_name}'. Available agents:\n{available_str}"
437
+ return f"Error: Unknown agent '{agent_name}'. Available:\n{available_str}"
189
438
 
190
- # Validate caller is allowed to call this subagent
191
439
  config = get_subagent_config(agent_name)
192
440
  if allowed_subagents and agent_name not in allowed_subagents:
193
441
  return f"Error: '{caller_name}' cannot call '{agent_name}'. Allowed: {allowed_subagents}"
194
-
195
442
  if config.callable_by and caller_name not in config.callable_by:
196
443
  return f"Error: '{agent_name}' can only be called by: {config.callable_by}"
197
444
 
198
- logger.info(
199
- f"[{caller_name}] Invoking subagent '{agent_name}': {description[:100]}..."
200
- )
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)}"
201
454
 
202
- # Import subagent event emitters
203
- from agent_server.langchain.middleware.subagent_events import (
204
- clear_current_subagent,
205
- emit_subagent_complete,
206
- emit_subagent_start,
207
- set_current_subagent,
208
- )
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
+ )
209
460
 
210
- # Emit subagent start event for UI
211
- emit_subagent_start(agent_name, description)
212
461
 
213
- # Get the factory and config
214
- factory_func, llm_config = get_subagent_factory()
215
- if factory_func is None:
216
- 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).
217
467
 
218
- try:
219
- import time
220
-
221
- # Set current subagent context for tool call tracking
222
- set_current_subagent(agent_name)
223
-
224
- # Get or create the subagent (cached for performance)
225
- # Avoids expensive LangGraph recompilation on each call
226
- t0 = time.time()
227
- subagent = get_or_create_subagent(agent_name, factory_func, llm_config)
228
- t1 = time.time()
229
- logger.info(f"[TIMING] get_or_create_subagent took {t1-t0:.2f}s")
230
-
231
- # Execute subagent synchronously with clean context
232
- # The subagent runs in isolation, receiving task description + optional context
233
- import uuid
234
-
235
- subagent_thread_id = f"subagent-{agent_name}-{uuid.uuid4().hex[:8]}"
236
- subagent_config = {
237
- "configurable": {
238
- "thread_id": subagent_thread_id,
239
- }
240
- }
468
+ Uses ToolRuntime for state access and returns Command to update
469
+ generated_content/generated_content_type/content_description in state.
241
470
 
242
- # Inject code history for python_developer
243
- enhanced_context = context
244
- if agent_name == "python_developer":
245
- try:
246
- t2 = time.time()
247
- from agent_server.langchain.middleware.code_history_middleware import (
248
- get_code_history_tracker,
249
- get_context_with_history,
250
- )
471
+ Args:
472
+ caller_name: Name of the calling agent (usually "planner")
473
+ allowed_subagents: Optional list of allowed subagent names
251
474
 
252
- # Get main agent's thread_id for session-scoped history
253
- main_thread_id = get_current_thread_id()
254
- tracker = get_code_history_tracker(main_thread_id)
255
- if tracker.get_entry_count() > 0:
256
- enhanced_context = get_context_with_history(
257
- context, main_thread_id
258
- )
259
- t3 = time.time()
260
- logger.info(
261
- f"[TIMING] code history injection took {t3-t2:.2f}s "
262
- f"(entries={tracker.get_entry_count()}, "
263
- f"thread_id={main_thread_id}, "
264
- f"context_len={len(enhanced_context) if enhanced_context else 0})"
265
- )
266
- except Exception as e:
267
- 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)
268
501
 
269
- # Build the message content with optional context
270
- if enhanced_context:
271
- message_content = f"""## Task
272
- {description}
502
+ def task(
503
+ description: str,
504
+ subagent_type: str,
505
+ runtime: ToolRuntime,
506
+ ) -> str:
507
+ """Delegate a task to a specialized subagent.
273
508
 
274
- ## Context (provided by Main Agent)
275
- {enhanced_context}"""
276
- else:
277
- 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.
278
512
 
279
- logger.info(
280
- f"[{caller_name}] Subagent message length: {len(message_content)}"
281
- )
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)
282
517
 
283
- # Execute the subagent
284
- t_invoke_start = time.time()
285
- logger.info(f"[TIMING] About to invoke subagent '{agent_name}'...")
286
- result = subagent.invoke(
287
- {"messages": [{"role": "user", "content": message_content}]},
288
- config=subagent_config,
289
- )
290
- t_invoke_end = time.time()
291
- logger.info(
292
- 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,
293
545
  )
294
546
 
295
- # Extract the final message from the result
547
+ # Extract response text
296
548
  messages = result.get("messages", [])
549
+ response_text = ""
297
550
  if messages:
298
- final_message = messages[-1]
299
- if hasattr(final_message, "content"):
300
- response = final_message.content
301
- else:
302
- response = str(final_message)
303
- else:
304
- response = "Subagent completed but returned no messages."
305
-
306
- logger.info(
307
- f"[{caller_name}] Subagent '{agent_name}' returned: {str(response)[:200]}..."
308
- )
309
-
310
- # Extract description from python_developer response for auto-injection
311
- if agent_name == "python_developer":
312
- try:
313
- from agent_server.langchain.middleware.description_injector import (
314
- process_task_tool_response,
315
- )
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
+ }
316
565
 
317
- process_task_tool_response(agent_name, str(response))
318
- except Exception as e:
319
- 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
+ )
320
581
 
321
- # Emit subagent complete event for UI
322
- 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")
323
584
 
324
- 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
+ )
325
596
 
326
597
  except Exception as e:
327
- error_msg = f"Subagent '{agent_name}' failed: {str(e)}"
598
+ error_msg = f"Error: Subagent '{subagent_type}' failed: {str(e)}"
328
599
  logger.error(error_msg, exc_info=True)
329
- # Emit complete event even on error
330
- emit_subagent_complete(agent_name, f"Error: {str(e)[:50]}")
331
- return f"Error: {error_msg}"
332
- finally:
333
- # Always clear subagent context
334
- clear_current_subagent()
335
-
336
- # Update tool docstring with available agents
337
- task_tool.__doc__ = f"""Delegate a task to a specialized subagent.
338
-
339
- Available agents:
340
- {available_str}
341
-
342
- The subagent will analyze the task and return its result.
343
- - python_developer: Returns generated Python code and analysis
344
- - researcher: Returns search results and findings
345
- - athena_query: Returns SQL query string
346
-
347
- For code execution (running Python, writing files), use Main Agent's tools directly.
348
-
349
- IMPORTANT: For python_developer, ALWAYS provide context with:
350
- - Resource info (file sizes, memory) from check_resource_tool
351
- - Previous code context if building on existing work
352
- - Variable names and their current state
353
-
354
- Args:
355
- agent_name: Name of the subagent to invoke
356
- description: Detailed task description for the subagent
357
- context: Additional context (resource info, previous code, etc.)
358
-
359
- Returns:
360
- Result from the subagent execution
361
- """
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
+ )
362
615
 
363
- return task_tool
364
616
 
617
+ # ---------------------------------------------------------------------------
618
+ # SubAgentMiddleware (extends AgentMiddleware)
619
+ # ---------------------------------------------------------------------------
365
620
 
366
621
  class SubAgentMiddleware:
367
- """
368
- Middleware that adds subagent delegation capability.
622
+ """Middleware that adds subagent delegation via the task tool.
369
623
 
370
- This middleware:
371
- 1. Adds the `task` tool to the agent's toolset
372
- 2. The task tool executes subagents synchronously
373
- 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
374
629
 
375
630
  Usage:
376
631
  middleware = SubAgentMiddleware(
377
- caller_name="main_agent",
378
- allowed_subagents=["python_developer", "researcher"],
632
+ caller_name="planner",
633
+ allowed_subagents=None,
379
634
  )
380
635
 
381
636
  agent = create_agent(
382
637
  model=llm,
383
638
  tools=tools,
384
639
  middleware=[middleware, ...],
640
+ state_schema=HDSPAgentState,
385
641
  )
386
642
  """
387
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
+
388
655
  def __init__(
389
656
  self,
390
657
  caller_name: str,
391
658
  allowed_subagents: Optional[List[str]] = None,
392
659
  ):
393
- """
394
- Initialize SubAgentMiddleware.
395
-
396
- Args:
397
- caller_name: Name of the agent using this middleware
398
- allowed_subagents: List of subagents this agent can call.
399
- None means all non-restricted subagents.
400
- """
401
660
  self.caller_name = caller_name
402
661
  self.allowed_subagents = allowed_subagents
403
- 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]
404
668
 
405
669
  logger.info(
406
670
  f"SubAgentMiddleware initialized for '{caller_name}' "
@@ -408,13 +672,5 @@ class SubAgentMiddleware:
408
672
  )
409
673
 
410
674
  def get_tools(self) -> List[Any]:
411
- """Get the tools provided by this middleware."""
675
+ """Get the tools provided by this middleware (backward compat)."""
412
676
  return [self.task_tool]
413
-
414
- def __call__(self, tools: List[Any]) -> List[Any]:
415
- """
416
- Add task tool to the agent's toolset.
417
-
418
- This is called during agent creation to augment the tool list.
419
- """
420
- return tools + [self.task_tool]