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
@@ -93,7 +93,6 @@ def _create_vllm_llm(llm_config: Dict[str, Any], callbacks):
93
93
  from langchain_openai import ChatOpenAI
94
94
 
95
95
  vllm_config = llm_config.get("vllm", {})
96
- # User provides full base URL (e.g., https://openrouter.ai/api/v1)
97
96
  endpoint = vllm_config.get("endpoint", "http://localhost:8000/v1")
98
97
  model = vllm_config.get("model", "default")
99
98
  api_key = vllm_config.get("apiKey", "dummy")
@@ -140,9 +139,11 @@ def _create_vllm_llm(llm_config: Dict[str, Any], callbacks):
140
139
 
141
140
 
142
141
  def create_summarization_llm(llm_config: Dict[str, Any]):
143
- """Create LLM for summarization middleware.
142
+ """Create LLM for summarization middleware and /compact feature.
144
143
 
145
- Uses the same provider as the main LLM but with simpler configuration.
144
+ Priority:
145
+ 1. If llm_config["summarization"]["enabled"] is True, use that config
146
+ 2. Otherwise, fall back to main provider with default summarization model
146
147
 
147
148
  Args:
148
149
  llm_config: Configuration dictionary
@@ -150,60 +151,107 @@ def create_summarization_llm(llm_config: Dict[str, Any]):
150
151
  Returns:
151
152
  LLM instance suitable for summarization, or None if unavailable
152
153
  """
153
- provider = llm_config.get("provider", "gemini")
154
-
155
154
  try:
156
- if provider == "gemini":
157
- from langchain_google_genai import ChatGoogleGenerativeAI
158
-
159
- gemini_config = llm_config.get("gemini", {})
160
- api_key = gemini_config.get("apiKey")
161
- if api_key:
162
- return ChatGoogleGenerativeAI(
163
- model="gemini-2.5-flash",
164
- google_api_key=api_key,
165
- temperature=0.0,
166
- )
167
- elif provider == "openai":
168
- from langchain_openai import ChatOpenAI
169
-
170
- openai_config = llm_config.get("openai", {})
171
- api_key = openai_config.get("apiKey")
172
- if api_key:
173
- return ChatOpenAI(
174
- model="gpt-4o-mini",
175
- api_key=api_key,
176
- temperature=0.0,
177
- )
178
- elif provider == "vllm":
179
- vllm_config = llm_config.get("vllm", {})
180
- # User provides full base URL (e.g., https://openrouter.ai/api/v1)
181
- endpoint = vllm_config.get("endpoint", "http://localhost:8000/v1")
182
- model = vllm_config.get("model", "default")
183
- api_key = vllm_config.get("apiKey", "dummy")
184
-
185
- # Use ChatGPTOSS for gpt-oss models (but not via OpenRouter)
186
- is_openrouter = "openrouter" in endpoint.lower()
187
- if "gpt-oss" in model.lower() and not is_openrouter:
188
- from agent_server.langchain.models import ChatGPTOSS
189
-
190
- return ChatGPTOSS(
191
- model=model,
192
- base_url=endpoint,
193
- api_key=api_key,
194
- temperature=0.0,
195
- )
196
-
197
- from langchain_openai import ChatOpenAI
198
-
199
- return ChatOpenAI(
200
- model=model,
201
- api_key=api_key,
202
- base_url=endpoint, # Use endpoint as-is
203
- temperature=0.0,
155
+ # 1. Check for dedicated summarization config
156
+ summarization_config = llm_config.get("summarization", {})
157
+ if summarization_config.get("enabled"):
158
+ sum_provider = summarization_config.get("provider", "gemini")
159
+ sum_model = summarization_config.get("model")
160
+ logger.info(
161
+ f"Using dedicated summarization LLM: provider={sum_provider}, model={sum_model or 'default'}"
162
+ )
163
+ return _create_llm_for_provider(
164
+ llm_config, sum_provider, sum_model, for_summarization=True
204
165
  )
166
+
167
+ # 2. Fall back to main provider with default summarization model
168
+ provider = llm_config.get("provider", "gemini")
169
+ logger.info(f"Using main provider for summarization: {provider}")
170
+ return _create_llm_for_provider(
171
+ llm_config, provider, None, for_summarization=True
172
+ )
173
+
205
174
  except Exception as e:
206
175
  logger.warning(f"Failed to create summarization LLM: {e}")
207
176
  return None
208
177
 
209
- return None
178
+
179
+ def _create_llm_for_provider(
180
+ llm_config: Dict[str, Any],
181
+ provider: str,
182
+ model_override: str = None,
183
+ for_summarization: bool = False,
184
+ ):
185
+ """Create LLM instance for a specific provider.
186
+
187
+ Args:
188
+ llm_config: Full configuration dictionary (for credentials)
189
+ provider: Provider to use ('gemini', 'openai', 'vllm')
190
+ model_override: Optional model name override
191
+ for_summarization: If True, use lightweight default models
192
+
193
+ Returns:
194
+ LLM instance or None
195
+ """
196
+ if provider == "gemini":
197
+ from langchain_google_genai import ChatGoogleGenerativeAI
198
+
199
+ gemini_config = llm_config.get("gemini", {})
200
+ api_key = gemini_config.get("apiKey")
201
+ if not api_key:
202
+ logger.warning("No Gemini API key found")
203
+ return None
204
+
205
+ model = model_override or ("gemini-2.5-flash" if for_summarization else gemini_config.get("model", "gemini-2.5-flash"))
206
+ return ChatGoogleGenerativeAI(
207
+ model=model,
208
+ google_api_key=api_key,
209
+ temperature=0.0,
210
+ )
211
+
212
+ elif provider == "openai":
213
+ from langchain_openai import ChatOpenAI
214
+
215
+ openai_config = llm_config.get("openai", {})
216
+ api_key = openai_config.get("apiKey")
217
+ if not api_key:
218
+ logger.warning("No OpenAI API key found")
219
+ return None
220
+
221
+ model = model_override or ("gpt-4o-mini" if for_summarization else openai_config.get("model", "gpt-4"))
222
+ return ChatOpenAI(
223
+ model=model,
224
+ api_key=api_key,
225
+ temperature=0.0,
226
+ )
227
+
228
+ elif provider == "vllm":
229
+ vllm_config = llm_config.get("vllm", {})
230
+ endpoint = vllm_config.get("endpoint", "http://localhost:8000/v1")
231
+ api_key = vllm_config.get("apiKey", "dummy")
232
+ model = model_override or vllm_config.get("model", "default")
233
+
234
+ # Use ChatGPTOSS for gpt-oss models (but not via OpenRouter)
235
+ is_openrouter = "openrouter" in endpoint.lower()
236
+ if "gpt-oss" in model.lower() and not is_openrouter:
237
+ from agent_server.langchain.models import ChatGPTOSS
238
+
239
+ return ChatGPTOSS(
240
+ model=model,
241
+ base_url=endpoint,
242
+ api_key=api_key,
243
+ temperature=0.0,
244
+ )
245
+
246
+ from langchain_openai import ChatOpenAI
247
+
248
+ return ChatOpenAI(
249
+ model=model,
250
+ api_key=api_key,
251
+ base_url=endpoint,
252
+ temperature=0.0,
253
+ )
254
+
255
+ else:
256
+ logger.warning(f"Unknown provider: {provider}")
257
+ return None
@@ -37,7 +37,7 @@ def disable_langchain_logging():
37
37
 
38
38
 
39
39
  # Auto-disable on import (comment this line to re-enable all logs)
40
- disable_langchain_logging()
40
+ # disable_langchain_logging() # TEMPORARILY ENABLED FOR DEBUGGING
41
41
 
42
42
  LOG_SEPARATOR = "=" * 96
43
43
  LOG_SUBSECTION = "-" * 96
@@ -3,10 +3,14 @@ Middleware Module
3
3
 
4
4
  Custom middleware for the multi-agent architecture:
5
5
  - SubAgentMiddleware: Handles subagent delegation via task tool
6
+ - ContentInjectionMiddleware: Injects generated code/SQL into tool args
6
7
  - SkillMiddleware: Progressive skill loading for code generation agents
7
8
  - Existing middleware from custom_middleware.py is also available
8
9
  """
9
10
 
11
+ from agent_server.langchain.middleware.content_injection_middleware import (
12
+ ContentInjectionMiddleware,
13
+ )
10
14
  from agent_server.langchain.middleware.skill_middleware import (
11
15
  SkillMiddleware,
12
16
  get_skill_middleware,
@@ -18,6 +22,7 @@ from agent_server.langchain.middleware.subagent_middleware import (
18
22
 
19
23
  __all__ = [
20
24
  "SubAgentMiddleware",
25
+ "ContentInjectionMiddleware",
21
26
  "create_task_tool",
22
27
  "SkillMiddleware",
23
28
  "get_skill_middleware",
@@ -12,11 +12,12 @@ Features:
12
12
 
13
13
  import logging
14
14
  import threading
15
- import tiktoken
16
15
  from dataclasses import dataclass, field
17
16
  from datetime import datetime
18
17
  from typing import Any, Dict, List, Optional
19
18
 
19
+ import tiktoken
20
+
20
21
  logger = logging.getLogger(__name__)
21
22
 
22
23
  # Token limit for context (including system prompt)
@@ -31,7 +32,9 @@ PYTHON_DEV_SYSTEM_PROMPT_TOKENS = 2000
31
32
  class CodeHistoryEntry:
32
33
  """Represents a single code execution or file operation."""
33
34
 
34
- tool_name: str # jupyter_cell_tool, write_file_tool, edit_file_tool, multiedit_file_tool
35
+ tool_name: (
36
+ str # jupyter_cell_tool, write_file_tool, edit_file_tool, multiedit_file_tool
37
+ )
35
38
  timestamp: datetime = field(default_factory=datetime.now)
36
39
 
37
40
  # For jupyter_cell_tool
@@ -50,7 +53,9 @@ class CodeHistoryEntry:
50
53
  timestamp_str = self.timestamp.strftime("%H:%M:%S")
51
54
 
52
55
  if self.tool_name == "jupyter_cell_tool":
53
- output_preview = self._truncate(self.output, 500) if self.output else "(no output)"
56
+ output_preview = (
57
+ self._truncate(self.output, 500) if self.output else "(no output)"
58
+ )
54
59
  return f"""## Cell ({timestamp_str})
55
60
  ```python
56
61
  {self.code}
@@ -86,7 +91,7 @@ Changes: {edit_count} edits applied"""
86
91
  if self.tool_name == "jupyter_cell_tool":
87
92
  # Extract first meaningful line of code
88
93
  if self.code:
89
- first_line = self.code.strip().split('\n')[0][:60]
94
+ first_line = self.code.strip().split("\n")[0][:60]
90
95
  return f"- Cell: {first_line}..."
91
96
  return "- Cell: (empty)"
92
97
 
@@ -156,7 +161,9 @@ class CodeHistoryTracker:
156
161
  output=output,
157
162
  )
158
163
  self._history.append(entry)
159
- logger.info(f"CodeHistory: Added jupyter_cell (total: {len(self._history)})")
164
+ logger.info(
165
+ f"CodeHistory: Added jupyter_cell (total: {len(self._history)})"
166
+ )
160
167
 
161
168
  def add_write_file(self, file_path: str, content: str) -> None:
162
169
  """Track a write_file_tool execution."""
@@ -167,7 +174,9 @@ class CodeHistoryTracker:
167
174
  content=content,
168
175
  )
169
176
  self._history.append(entry)
170
- logger.info(f"CodeHistory: Added write_file {file_path} (total: {len(self._history)})")
177
+ logger.info(
178
+ f"CodeHistory: Added write_file {file_path} (total: {len(self._history)})"
179
+ )
171
180
 
172
181
  def add_edit_file(self, file_path: str, old_content: str, new_content: str) -> None:
173
182
  """Track an edit_file_tool execution."""
@@ -179,7 +188,9 @@ class CodeHistoryTracker:
179
188
  new_content=new_content,
180
189
  )
181
190
  self._history.append(entry)
182
- logger.info(f"CodeHistory: Added edit_file {file_path} (total: {len(self._history)})")
191
+ logger.info(
192
+ f"CodeHistory: Added edit_file {file_path} (total: {len(self._history)})"
193
+ )
183
194
 
184
195
  def add_multiedit_file(self, file_path: str, edits: List[Dict[str, str]]) -> None:
185
196
  """Track a multiedit_file_tool execution."""
@@ -190,7 +201,9 @@ class CodeHistoryTracker:
190
201
  edits=edits,
191
202
  )
192
203
  self._history.append(entry)
193
- logger.info(f"CodeHistory: Added multiedit_file {file_path} (total: {len(self._history)})")
204
+ logger.info(
205
+ f"CodeHistory: Added multiedit_file {file_path} (total: {len(self._history)})"
206
+ )
194
207
 
195
208
  def get_context_for_subagent(
196
209
  self,
@@ -216,8 +229,12 @@ class CodeHistoryTracker:
216
229
  return existing_context or ""
217
230
 
218
231
  # Calculate available tokens for history
219
- existing_tokens = self._count_tokens(existing_context) if existing_context else 0
220
- available_tokens = max_tokens - system_prompt_tokens - existing_tokens - 500 # 500 buffer
232
+ existing_tokens = (
233
+ self._count_tokens(existing_context) if existing_context else 0
234
+ )
235
+ available_tokens = (
236
+ max_tokens - system_prompt_tokens - existing_tokens - 500
237
+ ) # 500 buffer
221
238
 
222
239
  # Build full history string
223
240
  full_history = self._build_full_history()
@@ -311,49 +328,110 @@ class CodeHistoryTracker:
311
328
  return len(self._history)
312
329
 
313
330
 
314
- # Global tracker instance (per-thread tracking could be added if needed)
315
- _code_history_tracker: Optional[CodeHistoryTracker] = None
331
+ # Global tracker instances per threadId
332
+ _code_history_trackers: Dict[str, CodeHistoryTracker] = {}
333
+ _trackers_lock = threading.Lock()
334
+
335
+
336
+ def get_code_history_tracker(thread_id: Optional[str] = None) -> CodeHistoryTracker:
337
+ """
338
+ Get the CodeHistoryTracker instance for the given thread_id.
339
+
340
+ Args:
341
+ thread_id: Thread ID for session isolation. If None, returns a temporary tracker.
316
342
 
343
+ Returns:
344
+ CodeHistoryTracker instance for the thread
345
+ """
346
+ if thread_id is None:
347
+ logger.warning(
348
+ "get_code_history_tracker called without thread_id - using temporary tracker"
349
+ )
350
+ return CodeHistoryTracker()
317
351
 
318
- def get_code_history_tracker() -> CodeHistoryTracker:
319
- """Get the global CodeHistoryTracker instance."""
320
- global _code_history_tracker
321
- if _code_history_tracker is None:
322
- _code_history_tracker = CodeHistoryTracker()
323
- return _code_history_tracker
352
+ with _trackers_lock:
353
+ if thread_id not in _code_history_trackers:
354
+ _code_history_trackers[thread_id] = CodeHistoryTracker()
355
+ logger.info(f"CodeHistory: Created new tracker for thread_id={thread_id}")
356
+ return _code_history_trackers[thread_id]
324
357
 
325
358
 
326
- def track_jupyter_cell(code: str, output: str) -> None:
359
+ def track_jupyter_cell(code: str, output: str, thread_id: Optional[str] = None) -> None:
327
360
  """Convenience function to track jupyter_cell_tool execution."""
328
- get_code_history_tracker().add_jupyter_cell(code, output)
361
+ get_code_history_tracker(thread_id).add_jupyter_cell(code, output)
329
362
 
330
363
 
331
- def track_write_file(file_path: str, content: str) -> None:
364
+ def track_write_file(
365
+ file_path: str, content: str, thread_id: Optional[str] = None
366
+ ) -> None:
332
367
  """Convenience function to track write_file_tool execution."""
333
- get_code_history_tracker().add_write_file(file_path, content)
368
+ get_code_history_tracker(thread_id).add_write_file(file_path, content)
334
369
 
335
370
 
336
- def track_edit_file(file_path: str, old_content: str, new_content: str) -> None:
371
+ def track_edit_file(
372
+ file_path: str,
373
+ old_content: str,
374
+ new_content: str,
375
+ thread_id: Optional[str] = None,
376
+ ) -> None:
337
377
  """Convenience function to track edit_file_tool execution."""
338
- get_code_history_tracker().add_edit_file(file_path, old_content, new_content)
378
+ get_code_history_tracker(thread_id).add_edit_file(
379
+ file_path, old_content, new_content
380
+ )
339
381
 
340
382
 
341
- def track_multiedit_file(file_path: str, edits: List[Dict[str, str]]) -> None:
383
+ def track_multiedit_file(
384
+ file_path: str,
385
+ edits: List[Dict[str, str]],
386
+ thread_id: Optional[str] = None,
387
+ ) -> None:
342
388
  """Convenience function to track multiedit_file_tool execution."""
343
- get_code_history_tracker().add_multiedit_file(file_path, edits)
389
+ get_code_history_tracker(thread_id).add_multiedit_file(file_path, edits)
344
390
 
345
391
 
346
- def get_context_with_history(existing_context: Optional[str] = None) -> str:
392
+ def get_context_with_history(
393
+ existing_context: Optional[str] = None,
394
+ thread_id: Optional[str] = None,
395
+ ) -> str:
347
396
  """Get context string with code history injected."""
348
- return get_code_history_tracker().get_context_for_subagent(existing_context)
397
+ return get_code_history_tracker(thread_id).get_context_for_subagent(
398
+ existing_context
399
+ )
400
+
349
401
 
402
+ def clear_code_history(thread_id: Optional[str] = None) -> None:
403
+ """
404
+ Clear code history for a specific thread or all threads.
350
405
 
351
- def clear_code_history() -> None:
352
- """Clear all code history."""
353
- get_code_history_tracker().clear()
406
+ Args:
407
+ thread_id: Thread ID to clear. If None, clears all threads.
408
+ """
409
+ if thread_id is None:
410
+ # Clear all trackers
411
+ with _trackers_lock:
412
+ for tid, tracker in _code_history_trackers.items():
413
+ tracker.clear()
414
+ logger.info(f"CodeHistory: Cleared history for thread_id={tid}")
415
+ _code_history_trackers.clear()
416
+ logger.info("CodeHistory: Cleared all thread trackers")
417
+ else:
418
+ # Clear specific thread
419
+ with _trackers_lock:
420
+ if thread_id in _code_history_trackers:
421
+ _code_history_trackers[thread_id].clear()
422
+ del _code_history_trackers[thread_id]
423
+ logger.info(
424
+ f"CodeHistory: Cleared and removed tracker for thread_id={thread_id}"
425
+ )
426
+ else:
427
+ logger.info(f"CodeHistory: No tracker found for thread_id={thread_id}")
354
428
 
355
429
 
356
- def track_tool_execution(tool_name: str, args: Dict[str, Any]) -> None:
430
+ def track_tool_execution(
431
+ tool_name: str,
432
+ args: Dict[str, Any],
433
+ thread_id: Optional[str] = None,
434
+ ) -> None:
357
435
  """
358
436
  Track a tool execution from HITL decision processing.
359
437
 
@@ -363,6 +441,7 @@ def track_tool_execution(tool_name: str, args: Dict[str, Any]) -> None:
363
441
  Args:
364
442
  tool_name: Name of the tool (jupyter_cell_tool, write_file_tool, etc.)
365
443
  args: Tool arguments including execution_result
444
+ thread_id: Thread ID for session isolation
366
445
  """
367
446
  if not args:
368
447
  return
@@ -371,21 +450,26 @@ def track_tool_execution(tool_name: str, args: Dict[str, Any]) -> None:
371
450
  if not execution_result:
372
451
  return
373
452
 
374
- tracker = get_code_history_tracker()
453
+ tracker = get_code_history_tracker(thread_id)
375
454
 
376
455
  if tool_name == "jupyter_cell_tool":
377
456
  code = args.get("code", "")
378
457
  output = execution_result.get("output", "")
379
458
  if code:
380
459
  tracker.add_jupyter_cell(code, output)
381
- logger.info(f"CodeHistory: Tracked jupyter_cell execution (code len={len(code)})")
460
+ logger.info(
461
+ f"CodeHistory: Tracked jupyter_cell execution "
462
+ f"(code len={len(code)}, thread_id={thread_id})"
463
+ )
382
464
 
383
465
  elif tool_name == "write_file_tool":
384
466
  file_path = args.get("path", "")
385
467
  content = args.get("content", "")
386
468
  if file_path:
387
469
  tracker.add_write_file(file_path, content)
388
- logger.info(f"CodeHistory: Tracked write_file to {file_path}")
470
+ logger.info(
471
+ f"CodeHistory: Tracked write_file to {file_path} (thread_id={thread_id})"
472
+ )
389
473
 
390
474
  elif tool_name == "edit_file_tool":
391
475
  file_path = args.get("path", "")
@@ -393,7 +477,9 @@ def track_tool_execution(tool_name: str, args: Dict[str, Any]) -> None:
393
477
  new_string = args.get("new_string", "")
394
478
  if file_path:
395
479
  tracker.add_edit_file(file_path, old_string, new_string)
396
- logger.info(f"CodeHistory: Tracked edit_file to {file_path}")
480
+ logger.info(
481
+ f"CodeHistory: Tracked edit_file to {file_path} (thread_id={thread_id})"
482
+ )
397
483
 
398
484
  elif tool_name == "multiedit_file_tool":
399
485
  file_path = args.get("path", "")
@@ -409,4 +495,7 @@ def track_tool_execution(tool_name: str, args: Dict[str, Any]) -> None:
409
495
  elif isinstance(edit, dict):
410
496
  edits_as_dicts.append(edit)
411
497
  tracker.add_multiedit_file(file_path, edits_as_dicts)
412
- logger.info(f"CodeHistory: Tracked multiedit_file to {file_path} ({len(edits)} edits)")
498
+ logger.info(
499
+ f"CodeHistory: Tracked multiedit_file to {file_path} "
500
+ f"({len(edits)} edits, thread_id={thread_id})"
501
+ )
@@ -0,0 +1,110 @@
1
+ """
2
+ ContentInjectionMiddleware
3
+
4
+ Injects generated_content from LangGraph state into target tool args.
5
+ This eliminates JSON escaping issues when passing code/SQL between agents.
6
+
7
+ Runs BEFORE HumanInTheLoopMiddleware so HITL shows the full injected content.
8
+
9
+ Flow:
10
+ 1. Subagent generates code/SQL → stored in state via Command
11
+ 2. Main Agent calls target tool (e.g., jupyter_cell_tool) without args
12
+ 3. This middleware reads state and injects content into tool args
13
+ 4. HITL middleware sees full content for user approval
14
+
15
+ content_type → tool injection mapping:
16
+ - "python" → jupyter_cell_tool(code=...), write_file_tool(content=...)
17
+ - "sql" → markdown_tool(content="```sql\\n...\\n```")
18
+ """
19
+
20
+ import logging
21
+ from typing import Any, Callable, Union
22
+
23
+ from langchain.agents.middleware import AgentMiddleware
24
+ from langchain_core.messages import ToolMessage
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+
29
+ class ContentInjectionMiddleware(AgentMiddleware):
30
+ """Inject state's generated_content into target tool call args.
31
+
32
+ When a subagent generates code/SQL via task_tool, it's stored in
33
+ LangGraph state (generated_content, generated_content_type, content_description).
34
+ This middleware reads the state and injects the content into the
35
+ appropriate tool's arguments before execution.
36
+
37
+ This ensures:
38
+ 1. Code/SQL bypasses LLM JSON serialization (no escaping issues)
39
+ 2. HITL middleware sees the full injected content for approval
40
+ 3. Main Agent doesn't need to copy code into tool args
41
+
42
+ Usage in agent_factory.py:
43
+ middleware = [
44
+ ContentInjectionMiddleware(), # BEFORE HITL
45
+ ...,
46
+ hitl_middleware, # Sees injected content
47
+ ]
48
+ """
49
+
50
+ def wrap_tool_call(self, request, handler):
51
+ """Intercept tool calls and inject generated content from state.
52
+
53
+ Args:
54
+ request: ToolCallRequest with tool_call, state, runtime
55
+ handler: Next handler in middleware chain
56
+
57
+ Returns:
58
+ ToolMessage or Command from handler
59
+ """
60
+ state = request.state
61
+ if not state:
62
+ return handler(request)
63
+
64
+ content = state.get("generated_content") if isinstance(state, dict) else getattr(state, "generated_content", None)
65
+ content_type = state.get("generated_content_type") if isinstance(state, dict) else getattr(state, "generated_content_type", None)
66
+ desc = state.get("content_description") if isinstance(state, dict) else getattr(state, "content_description", None)
67
+
68
+ if not content or not content_type:
69
+ return handler(request)
70
+
71
+ tool_call = request.tool_call
72
+ tool_name = tool_call["name"]
73
+ args = tool_call.get("args", {})
74
+
75
+ new_args = None
76
+
77
+ # Python code injection
78
+ if content_type == "python":
79
+ if tool_name == "jupyter_cell_tool" and not args.get("code"):
80
+ new_args = {**args, "code": content}
81
+ if desc and not args.get("description"):
82
+ new_args["description"] = desc
83
+ logger.info(
84
+ "[ContentInjection] Injected python code (%d chars) into jupyter_cell_tool",
85
+ len(content),
86
+ )
87
+ elif tool_name == "write_file_tool" and not args.get("content"):
88
+ new_args = {**args, "content": content}
89
+ logger.info(
90
+ "[ContentInjection] Injected python code (%d chars) into write_file_tool",
91
+ len(content),
92
+ )
93
+
94
+ # SQL query injection
95
+ elif content_type == "sql":
96
+ if tool_name == "markdown_tool" and not args.get("content"):
97
+ sql_markdown = f"```sql\n{content}\n```"
98
+ if desc:
99
+ sql_markdown = f"{desc}\n\n{sql_markdown}"
100
+ new_args = {**args, "content": sql_markdown}
101
+ logger.info(
102
+ "[ContentInjection] Injected SQL (%d chars) into markdown_tool",
103
+ len(content),
104
+ )
105
+
106
+ if new_args is not None:
107
+ modified_call = {**tool_call, "args": new_args}
108
+ request = request.override(tool_call=modified_call)
109
+
110
+ return handler(request)