codebuff-agent 2.1.74__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 (217) hide show
  1. codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  2. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
  3. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  4. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
  5. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
  6. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
  7. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
  8. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
  9. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
  10. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
  11. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  12. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  13. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  14. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
  15. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
  16. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
  17. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
  18. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
  19. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  20. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  21. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
  22. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
  23. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  24. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
  25. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
  26. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
  27. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
  28. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  29. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  30. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  31. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  32. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
  33. codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
  34. codebuff_agent-2.1.74.dist-info/METADATA +142 -0
  35. codebuff_agent-2.1.74.dist-info/RECORD +217 -0
  36. codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
  37. codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
  38. codebuff_core/__init__.py +153 -0
  39. codebuff_core/codebuff_config_defaults.json +64 -0
  40. codebuff_core/core/__init__.py +35 -0
  41. codebuff_core/core/code_validator.py +1238 -0
  42. codebuff_core/core/error_classifier.py +579 -0
  43. codebuff_core/core/exceptions.py +49 -0
  44. codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
  45. codebuff_core/directory_policy.py +32 -0
  46. codebuff_core/factory.py +155 -0
  47. codebuff_core/interfaces.py +197 -0
  48. codebuff_core/knowledge/__init__.py +31 -0
  49. codebuff_core/knowledge/chunking.py +375 -0
  50. codebuff_core/knowledge/loader.py +339 -0
  51. codebuff_core/knowledge/watchdog_service.py +354 -0
  52. codebuff_core/legacy_paths.py +69 -0
  53. codebuff_core/llm/__init__.py +13 -0
  54. codebuff_core/llm/service.py +407 -0
  55. codebuff_core/managers/__init__.py +22 -0
  56. codebuff_core/managers/config_manager.py +690 -0
  57. codebuff_core/managers/remote_config.py +673 -0
  58. codebuff_core/managers/remote_skills.py +539 -0
  59. codebuff_core/managers/session_manager.py +255 -0
  60. codebuff_core/middleware/__init__.py +3 -0
  61. codebuff_core/middleware/error_recovery.py +394 -0
  62. codebuff_core/middleware/logging_utils.py +33 -0
  63. codebuff_core/models/__init__.py +111 -0
  64. codebuff_core/models/agent.py +306 -0
  65. codebuff_core/models/chat.py +57 -0
  66. codebuff_core/models/common.py +82 -0
  67. codebuff_core/models/rag.py +300 -0
  68. codebuff_core/platform_rules.py +127 -0
  69. codebuff_core/prompts/__init__.py +44 -0
  70. codebuff_core/prompts/auto_agent_prompts.py +1278 -0
  71. codebuff_core/services/__init__.py +30 -0
  72. codebuff_core/services/agent_service.py +345 -0
  73. codebuff_core/services/analytics_aggregate.py +837 -0
  74. codebuff_core/services/chat_service.py +898 -0
  75. codebuff_core/services/context_condenser.py +306 -0
  76. codebuff_core/services/pii_guardrail.py +393 -0
  77. codebuff_core/services/rag_chat_agent.py +2993 -0
  78. codebuff_core/services/rag_collections.py +2740 -0
  79. codebuff_core/services/rag_manager.py +890 -0
  80. codebuff_core/services/rag_service.py +145 -0
  81. codebuff_core/services/retriever.py +260 -0
  82. codebuff_core/services/vllm_embedding_service.py +290 -0
  83. codebuff_core/tests/__init__.py +1 -0
  84. codebuff_core/tests/conftest.py +81 -0
  85. codebuff_core/tests/test_error_recovery.py +647 -0
  86. codebuff_core/tests/test_factory.py +135 -0
  87. codebuff_core/tests/test_services.py +150 -0
  88. jupyter_ext/__init__.py +266 -0
  89. jupyter_ext/_version.py +4 -0
  90. jupyter_ext/agent/__init__.py +49 -0
  91. jupyter_ext/agent/agent_factory.py +2270 -0
  92. jupyter_ext/agent/ast_tools.py +451 -0
  93. jupyter_ext/agent/autofix_middleware.py +481 -0
  94. jupyter_ext/agent/background_procs.py +372 -0
  95. jupyter_ext/agent/code_history_middleware.py +211 -0
  96. jupyter_ext/agent/codex_autofix.py +160 -0
  97. jupyter_ext/agent/codex_bridge.py +3339 -0
  98. jupyter_ext/agent/codex_hooks.py +320 -0
  99. jupyter_ext/agent/codex_slash.py +685 -0
  100. jupyter_ext/agent/codex_threads.py +114 -0
  101. jupyter_ext/agent/content_injection.py +377 -0
  102. jupyter_ext/agent/continuation_middleware.py +548 -0
  103. jupyter_ext/agent/custom_skills.py +335 -0
  104. jupyter_ext/agent/file_diffs.py +99 -0
  105. jupyter_ext/agent/git_history.py +188 -0
  106. jupyter_ext/agent/guardrail_middleware.py +421 -0
  107. jupyter_ext/agent/json_repair_utils.py +54 -0
  108. jupyter_ext/agent/langchain_tools.py +2673 -0
  109. jupyter_ext/agent/mermaid_fixer.py +289 -0
  110. jupyter_ext/agent/mermaid_middleware.py +135 -0
  111. jupyter_ext/agent/middleware.py +359 -0
  112. jupyter_ext/agent/middleware_prompts.py +53 -0
  113. jupyter_ext/agent/observability.py +334 -0
  114. jupyter_ext/agent/patch_tool_calls.py +57 -0
  115. jupyter_ext/agent/prompts.py +112 -0
  116. jupyter_ext/agent/skill_middleware.py +257 -0
  117. jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
  118. jupyter_ext/agent/skills/anomaly_detection.md +12 -0
  119. jupyter_ext/agent/skills/athena_sql.md +156 -0
  120. jupyter_ext/agent/skills/bdp_table_search.md +33 -0
  121. jupyter_ext/agent/skills/bigquery.md +65 -0
  122. jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
  123. jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
  124. jupyter_ext/agent/skills/classification_evaluation.md +11 -0
  125. jupyter_ext/agent/skills/clustering_analysis.md +12 -0
  126. jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
  127. jupyter_ext/agent/skills/data_analysis.md +236 -0
  128. jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
  129. jupyter_ext/agent/skills/data_loading.md +158 -0
  130. jupyter_ext/agent/skills/data_quality_report.md +13 -0
  131. jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
  132. jupyter_ext/agent/skills/emr_script_development.md +17 -0
  133. jupyter_ext/agent/skills/experiment_tracking.md +13 -0
  134. jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
  135. jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
  136. jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
  137. jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
  138. jupyter_ext/agent/skills/inference.md +392 -0
  139. jupyter_ext/agent/skills/lambda_function_development.md +19 -0
  140. jupyter_ext/agent/skills/model_comparison.md +11 -0
  141. jupyter_ext/agent/skills/model_explainability.md +12 -0
  142. jupyter_ext/agent/skills/model_training.md +318 -0
  143. jupyter_ext/agent/skills/pyspark.md +352 -0
  144. jupyter_ext/agent/skills/python_code_quality.md +35 -0
  145. jupyter_ext/agent/skills/regression_evaluation.md +12 -0
  146. jupyter_ext/agent/skills/spring_java_development.md +35 -0
  147. jupyter_ext/agent/skills/text_preprocessing.md +12 -0
  148. jupyter_ext/agent/skills/time_series_analysis.md +13 -0
  149. jupyter_ext/agent/skills/unit_testing.md +203 -0
  150. jupyter_ext/agent/todo_prompts.py +57 -0
  151. jupyter_ext/agent/tool_report.py +45 -0
  152. jupyter_ext/analytics_ingest.py +599 -0
  153. jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
  154. jupyter_ext/handlers/__init__.py +8 -0
  155. jupyter_ext/handlers/analytics_handler.py +170 -0
  156. jupyter_ext/handlers/benchmark_handler.py +794 -0
  157. jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
  158. jupyter_ext/handlers/rag_admin_handler.py +689 -0
  159. jupyter_ext/handlers/websocket.py +497 -0
  160. jupyter_ext/http_handlers.py +8744 -0
  161. jupyter_ext/idle_monitor.py +346 -0
  162. jupyter_ext/labextension/package.json +157 -0
  163. jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
  164. jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
  165. jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
  166. jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
  167. jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
  168. jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
  169. jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
  170. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
  171. jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
  172. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
  173. jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
  174. jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
  175. jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
  176. jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
  177. jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
  178. jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
  179. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
  180. jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
  181. jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
  182. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
  183. jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
  184. jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
  185. jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
  186. jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
  187. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
  188. jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
  189. jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
  190. jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
  191. jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
  192. jupyter_ext/labextension/static/style.js +4 -0
  193. jupyter_ext/labextension/static/third-party-licenses.json +724 -0
  194. jupyter_ext/log_paths.py +113 -0
  195. jupyter_ext/resource_usage.py +519 -0
  196. jupyter_ext/secrets_bootstrap.py +116 -0
  197. jupyter_ext/team_access.py +105 -0
  198. jupyter_ext/tests/__init__.py +1 -0
  199. jupyter_ext/tests/test_context.py +234 -0
  200. jupyter_ext/tests/test_factory.py +563 -0
  201. jupyter_ext/tests/test_file_finder.py +139 -0
  202. jupyter_ext/tests/test_handlers.py +58 -0
  203. jupyter_ext/tests/test_mcp_selftest.py +204 -0
  204. jupyter_ext/tests/test_mermaid_fixer.py +272 -0
  205. jupyter_ext/tests/test_resource_tiers.py +298 -0
  206. jupyter_ext/tools/__init__.py +15 -0
  207. jupyter_ext/tools/base.py +90 -0
  208. jupyter_ext/tools/context.py +268 -0
  209. jupyter_ext/tools/dir_complete.py +86 -0
  210. jupyter_ext/tools/file_finder.py +303 -0
  211. jupyter_ext/tools/file_tools.py +244 -0
  212. jupyter_ext/tools/jupyter_tools.py +296 -0
  213. jupyter_ext/tools/registry.py +159 -0
  214. jupyter_ext/tools/search_tools.py +230 -0
  215. jupyter_ext/tools/security_tools.py +657 -0
  216. jupyter_ext/tools/shell_tools.py +109 -0
  217. jupyter_ext/tools/user_tools.py +132 -0
@@ -0,0 +1,372 @@
1
+ """pod 에서 끝나지 않는 명령을 띄우고 거둔다 (이슈 #130).
2
+
3
+ `execute_command` 는 명령이 끝날 때까지 기다린다. 개발 서버(`python -m http.server`,
4
+ `npm run dev`, `./mvnw spring-boot:run`)는 끝나지 않으므로 무출력 한도에 걸려 죽거나
5
+ 턴을 붙잡는다. 여기서는 그런 명령을 **백그라운드로** 띄우고, 목록·로그·종료를 따로
6
+ 제공한다.
7
+
8
+ - 출력은 파이프가 아니라 **로그 파일**로 받는다. 파이프는 아무도 안 읽으면 버퍼가
9
+ 차서 서버가 멈춘다.
10
+ - 셸마다 프로세스 그룹을 따로 둔다(`start_new_session`). 종료는 그룹 단위라 셸이 띄운
11
+ 자식(java·node)까지 같이 끊긴다.
12
+ - 생존과 LISTEN 포트는 `psutil` 로 본다. 포트는 패널의 "열기" 링크가 쓴다.
13
+ - 레지스트리는 jupyter 서버 프로세스 하나에 하나이고 **대화와 무관한 전역**이다. pod 은
14
+ 사용자 1명 = 서버 1개라 전역이 곧 "내 것" 이다. 대화(thread_id)는 중지·새로고침마다
15
+ 버려져서(프론트가 저장하지 않는다) 소유 단위로 쓰면 못 찾는 프로세스가 생긴다.
16
+ `owner` 는 표시용으로만 남긴다.
17
+ - 죽이는 때는 셋이다: `kill_process` 도구·패널 중지 버튼, 서버 종료(`atexit`),
18
+ 서버가 `kill -9` 로 죽어 `atexit` 이 안 돈 경우 다음 기동의 `reap_orphans`.
19
+ 턴 취소·모델 변경·유휴 회수·대화 리셋에서는 죽이지 않는다 — 사용자가 브라우저에서
20
+ 쓰는 서버가 그 사이에 사라지면 기능이 무의미하다.
21
+ - **생존은 셸이 아니라 그룹으로 본다.** 셸(그룹 리더)이 먼저 끝나도 `cmd &`·`nohup` 이
22
+ 남긴 자식이 그룹에 살아 포트를 물고 있을 수 있다. 커널은 그룹에 멤버가 있는 동안 그
23
+ 번호를 새 프로세스에 주지 않으므로, 리더가 없는데 그룹이 살아 있으면 그 그룹은 우리
24
+ 것이다. 반대로 리더를 회수했는데 같은 pid 의 프로세스가 보이면 번호가 재사용된 남의
25
+ 것이라 신호를 보내지 않는다. `setsid`·데몬화로 그룹을 벗어난 프로세스는 못 잡는다.
26
+ - 레지스트리 파일은 서버가 아니라 `CODEBUFF_HOME` 단위다. 같은 홈을 쓰는 서버를 둘 띄우면
27
+ 나중 서버의 `reap_orphans` 가 앞 서버의 프로세스를 거둔다 — pod 은 서버가 하나라 해당
28
+ 없고, 개발 기계의 워크트리는 홈이 갈린다.
29
+ """
30
+
31
+ from __future__ import annotations
32
+
33
+ import atexit
34
+ import itertools
35
+ import json
36
+ import logging
37
+ import os
38
+ import shutil
39
+ import signal
40
+ import subprocess
41
+ import tempfile
42
+ import threading
43
+ import time
44
+ from typing import Any, Dict, List, Optional
45
+
46
+ import psutil
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+ # 종료 신호를 보낸 뒤 SIGKILL 까지 기다리는 시간(초)
51
+ _KILL_GRACE_S = 3.0
52
+ # 로그를 돌려줄 때의 줄 수 기본값과 상한
53
+ _TAIL_DEFAULT = 200
54
+ _TAIL_MAX = 2000
55
+ # 로그 끝부분만 읽는다 — 서버 로그는 하루면 수백 MB 가 된다
56
+ _TAIL_READ_BYTES = 256 * 1024
57
+ # 끝난 프로세스를 목록에 남기는 개수. 넘으면 오래된 것부터 로그와 함께 지운다
58
+ _MAX_FINISHED = 20
59
+
60
+
61
+ class _Proc:
62
+ __slots__ = (
63
+ "id",
64
+ "popen",
65
+ "command",
66
+ "cwd",
67
+ "owner",
68
+ "log",
69
+ "started_at",
70
+ "create_time",
71
+ )
72
+
73
+ def __init__(
74
+ self, id_: str, popen: Any, command: str, cwd: str, owner: str, log: str
75
+ ):
76
+ self.id = id_
77
+ self.popen = popen
78
+ self.command = command
79
+ self.cwd = cwd
80
+ self.owner = owner
81
+ self.log = log
82
+ self.started_at = time.time()
83
+ # pid 재사용을 가르는 값 — 재기동 뒤 `reap_orphans` 가 남의 프로세스를 죽이지 않게
84
+ try:
85
+ self.create_time: Optional[float] = psutil.Process(popen.pid).create_time()
86
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
87
+ self.create_time = None
88
+
89
+
90
+ _procs: Dict[str, _Proc] = {}
91
+ _lock = threading.Lock()
92
+ _ids = itertools.count(1)
93
+ _log_dir: Optional[str] = None
94
+
95
+
96
+ def _registry_path() -> str:
97
+ home = os.environ.get("CODEBUFF_HOME") or os.path.expanduser("~/.codebuff")
98
+ return os.path.join(home, "background_procs.json")
99
+
100
+
101
+ def _group_alive(pgid: int) -> bool:
102
+ """그룹에 프로세스가 하나라도 남아 있는가 (신호 0 은 보내지 않고 존재만 묻는다)."""
103
+ try:
104
+ os.killpg(pgid, 0)
105
+ except ProcessLookupError:
106
+ return False
107
+ except PermissionError:
108
+ return True
109
+ return True
110
+
111
+
112
+ def _owns_group(proc: _Proc) -> bool:
113
+ """그 프로세스 그룹이 아직 우리 것이고 살아 있는가."""
114
+ if proc.popen.poll() is None:
115
+ return True
116
+ pid = proc.popen.pid
117
+ try:
118
+ psutil.Process(pid)
119
+ except psutil.NoSuchProcess:
120
+ # 리더는 회수됐고 번호도 비어 있다 — 그룹이 살아 있으면 남은 자식이다
121
+ return _group_alive(pid)
122
+ except psutil.AccessDenied:
123
+ return False
124
+ # 리더를 이미 회수했는데 같은 번호가 보인다 — 재사용된 남의 프로세스다
125
+ return False
126
+
127
+
128
+ def _save() -> None:
129
+ """살아 있는 프로세스를 파일에 적는다. 서버가 `kill -9` 로 죽으면 `atexit` 이
130
+ 안 돌고, 자식은 세션을 따로 두어 살아남는다 — 다음 기동이 이 파일로 거둔다."""
131
+ rows = [
132
+ {
133
+ "id": p.id,
134
+ "pid": p.popen.pid,
135
+ "create_time": p.create_time,
136
+ "command": p.command,
137
+ }
138
+ for p in _procs.values()
139
+ if p.create_time is not None and _owns_group(p)
140
+ ]
141
+ path = _registry_path()
142
+ try:
143
+ os.makedirs(os.path.dirname(path), exist_ok=True)
144
+ tmp = path + ".tmp"
145
+ with open(tmp, "w", encoding="utf-8") as f:
146
+ json.dump(rows, f, ensure_ascii=False)
147
+ os.replace(tmp, path)
148
+ except OSError as e:
149
+ # 파일이 없으면 서버가 `kill -9` 로 죽었을 때 다음 기동이 못 거둔다
150
+ logger.warning("[Background] 레지스트리를 못 썼습니다 (%s): %s", path, e)
151
+
152
+
153
+ def reap_orphans() -> int:
154
+ """지난 서버가 남긴 백그라운드 프로세스를 거둔다. 확장 로드 때 한 번 부른다.
155
+
156
+ pid 만 보면 재사용된 남의 프로세스를 죽일 수 있어 생성 시각까지 맞는 것만 죽인다.
157
+ 리더가 없으면 그룹이 살아 있을 때만 죽인다 — 멤버가 있는 동안 그 번호는 재사용되지
158
+ 않는다.
159
+ """
160
+ try:
161
+ with open(_registry_path(), encoding="utf-8") as f:
162
+ rows = json.load(f)
163
+ except (OSError, ValueError):
164
+ return 0
165
+ reaped = 0
166
+ for row in rows if isinstance(rows, list) else []:
167
+ try:
168
+ pid = int(row["pid"])
169
+ created = float(row["create_time"])
170
+ except (KeyError, TypeError, ValueError):
171
+ continue
172
+ try:
173
+ ours = abs(psutil.Process(pid).create_time() - created) < 1.0
174
+ except psutil.NoSuchProcess:
175
+ ours = _group_alive(pid)
176
+ except psutil.Error:
177
+ continue
178
+ if ours and _signal_group(pid, signal.SIGKILL):
179
+ reaped += 1
180
+ with _lock:
181
+ _save()
182
+ return reaped
183
+
184
+
185
+ def _logs() -> str:
186
+ global _log_dir
187
+ if _log_dir is None or not os.path.isdir(_log_dir):
188
+ _log_dir = tempfile.mkdtemp(prefix="codebuff-bg-")
189
+ return _log_dir
190
+
191
+
192
+ def start(
193
+ command: str,
194
+ cwd: str,
195
+ env: Optional[Dict[str, str]] = None,
196
+ owner: str = "",
197
+ ) -> Dict[str, Any]:
198
+ """명령을 백그라운드로 띄우고 곧바로 돌아온다."""
199
+ if not command:
200
+ return {"success": False, "error": "명령이 비어 있습니다."}
201
+ id_ = f"bg{next(_ids)}"
202
+ log = os.path.join(_logs(), f"{id_}.log")
203
+ with open(log, "wb") as out:
204
+ popen = subprocess.Popen(
205
+ command,
206
+ shell=True,
207
+ cwd=cwd if os.path.isdir(cwd) else None,
208
+ stdin=subprocess.DEVNULL,
209
+ stdout=out,
210
+ stderr=subprocess.STDOUT,
211
+ start_new_session=True,
212
+ env=env,
213
+ )
214
+ proc = _Proc(id_, popen, command, cwd, owner, log)
215
+ with _lock:
216
+ _procs[id_] = proc
217
+ _trim_finished()
218
+ _save()
219
+ return {"success": True, "process_id": id_, **snapshot(proc)}
220
+
221
+
222
+ def _family(pid: int) -> List[Any]:
223
+ """리더와 자손. 리더가 끝났으면 같은 그룹에 남은 프로세스."""
224
+ try:
225
+ root = psutil.Process(pid)
226
+ return [root] + root.children(recursive=True)
227
+ except psutil.AccessDenied:
228
+ return []
229
+ except psutil.NoSuchProcess:
230
+ pass
231
+ members = []
232
+ for p in psutil.process_iter():
233
+ try:
234
+ if os.getpgid(p.pid) == pid:
235
+ members.append(p)
236
+ except (OSError, psutil.Error):
237
+ continue
238
+ return members
239
+
240
+
241
+ def _ports(pid: int) -> List[int]:
242
+ """그 그룹이 LISTEN 중인 TCP 포트."""
243
+ ports = set()
244
+ for p in _family(pid):
245
+ try:
246
+ # psutil 6 에서 `connections` 가 `net_connections` 로 바뀌었다
247
+ conns = (getattr(p, "net_connections", None) or p.connections)(kind="tcp")
248
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
249
+ continue
250
+ for c in conns:
251
+ if c.status == psutil.CONN_LISTEN and c.laddr:
252
+ ports.add(c.laddr.port)
253
+ return sorted(ports)
254
+
255
+
256
+ def snapshot(proc: _Proc) -> Dict[str, Any]:
257
+ code = proc.popen.poll()
258
+ running = _owns_group(proc)
259
+ return {
260
+ "id": proc.id,
261
+ "pid": proc.popen.pid,
262
+ "command": proc.command,
263
+ "cwd": proc.cwd,
264
+ "owner": proc.owner,
265
+ "state": "running" if running else "exited",
266
+ "exit_code": None if running else code,
267
+ "ports": _ports(proc.popen.pid) if running else [],
268
+ "started_at": proc.started_at,
269
+ "elapsed_s": round(time.time() - proc.started_at, 1),
270
+ }
271
+
272
+
273
+ def list_processes() -> Dict[str, Any]:
274
+ with _lock:
275
+ procs = list(_procs.values())
276
+ return {"success": True, "processes": [snapshot(p) for p in procs]}
277
+
278
+
279
+ def read_output(process_id: str, tail: Any = None) -> Dict[str, Any]:
280
+ proc = _procs.get(process_id)
281
+ if proc is None:
282
+ return {"success": False, "error": f"없는 프로세스: {process_id}"}
283
+ try:
284
+ lines = int(tail or _TAIL_DEFAULT)
285
+ except (TypeError, ValueError):
286
+ lines = _TAIL_DEFAULT
287
+ lines = max(1, min(lines, _TAIL_MAX))
288
+ try:
289
+ with open(proc.log, "rb") as f:
290
+ f.seek(0, os.SEEK_END)
291
+ size = f.tell()
292
+ f.seek(max(0, size - _TAIL_READ_BYTES))
293
+ text = f.read().decode("utf-8", errors="replace")
294
+ except OSError as e:
295
+ return {"success": False, "error": f"로그를 못 읽었습니다: {e}"}
296
+ kept = text.splitlines()[-lines:]
297
+ return {"success": True, **snapshot(proc), "output": "\n".join(kept)}
298
+
299
+
300
+ def _signal_group(pid: int, sig: int) -> bool:
301
+ try:
302
+ os.killpg(pid, sig)
303
+ except (ProcessLookupError, PermissionError):
304
+ return False
305
+ return True
306
+
307
+
308
+ def _wait_group(proc: _Proc, timeout: float) -> bool:
309
+ """그룹이 빌 때까지 기다린다. 리더는 회수해야 좀비로 그룹에 남지 않는다."""
310
+ deadline = time.time() + timeout
311
+ while True:
312
+ try:
313
+ proc.popen.wait(timeout=0.1)
314
+ except subprocess.TimeoutExpired:
315
+ pass
316
+ if not _group_alive(proc.popen.pid):
317
+ return True
318
+ if time.time() >= deadline:
319
+ return False
320
+
321
+
322
+ def _stop(proc: _Proc) -> None:
323
+ """그룹 전체에 SIGTERM, 유예 뒤 남았으면 SIGKILL.
324
+
325
+ 셸(그룹 리더)이 먼저 끝나도 그룹에 자식이 남을 수 있어 리더가 아니라 그룹이 빌
326
+ 때까지 기다린다. 이미 끝났거나 번호가 재사용됐으면 아무것도 보내지 않는다.
327
+ """
328
+ if not _owns_group(proc):
329
+ return
330
+ pid = proc.popen.pid
331
+ _signal_group(pid, signal.SIGTERM)
332
+ if not _wait_group(proc, _KILL_GRACE_S):
333
+ _signal_group(pid, signal.SIGKILL)
334
+ _wait_group(proc, _KILL_GRACE_S)
335
+
336
+
337
+ def kill(process_id: str) -> Dict[str, Any]:
338
+ proc = _procs.get(process_id)
339
+ if proc is None:
340
+ return {"success": False, "error": f"없는 프로세스: {process_id}"}
341
+ _stop(proc)
342
+ with _lock:
343
+ _save()
344
+ return {"success": True, **snapshot(proc)}
345
+
346
+
347
+ def kill_all() -> None:
348
+ with _lock:
349
+ procs = list(_procs.values())
350
+ if not procs:
351
+ # 띄운 적이 없으면 파일도 건드리지 않는다 (`atexit` 은 모든 서버에서 돈다)
352
+ return
353
+ for p in procs:
354
+ _stop(p)
355
+ with _lock:
356
+ _save()
357
+ if _log_dir is not None:
358
+ shutil.rmtree(_log_dir, ignore_errors=True)
359
+
360
+
361
+ def _trim_finished() -> None:
362
+ finished = [p for p in _procs.values() if not _owns_group(p)]
363
+ for p in finished[: max(0, len(finished) - _MAX_FINISHED)]:
364
+ _procs.pop(p.id, None)
365
+ try:
366
+ os.unlink(p.log)
367
+ except OSError:
368
+ pass
369
+
370
+
371
+ # jupyter 서버가 내려갈 때 띄운 서버가 남지 않게 한다
372
+ atexit.register(kill_all)
@@ -0,0 +1,211 @@
1
+ """CodeHistoryTracker + CodeHistoryMiddleware.
2
+
3
+ 코드 실행 이력을 추적하고, 서브에이전트(python_expert)에 이전 코드 컨텍스트를 주입.
4
+
5
+ 데이터 흐름:
6
+ HITL edit decision (execution_result)
7
+ -> http_handlers.py: track_tool_execution()
8
+ -> CodeHistoryTracker: 이력 저장
9
+ -> Main Agent -> task(python_expert)
10
+ -> CodeHistoryMiddleware.awrap_tool_call: description에 코드 히스토리 append
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import logging
16
+ from dataclasses import dataclass
17
+ from typing import Any, Optional
18
+
19
+ from langchain.agents.middleware.types import AgentMiddleware
20
+
21
+ logger = logging.getLogger(__name__)
22
+
23
+ # 토큰 추정: 1 토큰 ~= 4 chars
24
+ _CHARS_PER_TOKEN = 4
25
+ _MAX_CONTEXT_TOKENS = 50000
26
+ _MAX_CONTEXT_CHARS = _MAX_CONTEXT_TOKENS * _CHARS_PER_TOKEN
27
+ _RECENT_FULL_COUNT = 3 # 최근 N개는 전체 코드+출력 유지
28
+
29
+
30
+ @dataclass
31
+ class CodeHistoryEntry:
32
+ """단일 코드 실행 이력."""
33
+
34
+ tool_name: str # jupyter_cell, file_write
35
+ code: Optional[str] = None
36
+ output: Optional[str] = None
37
+ file_path: Optional[str] = None
38
+
39
+ def to_context_string(self) -> str:
40
+ """전체 코드+출력 포맷."""
41
+ parts = [f"[{self.tool_name}]"]
42
+ if self.file_path:
43
+ parts.append(f"File: {self.file_path}")
44
+ if self.code:
45
+ parts.append(f"```python\n{self.code}\n```")
46
+ if self.output:
47
+ parts.append(f"Output:\n{self.output[:2000]}")
48
+ return "\n".join(parts)
49
+
50
+ def to_summary_string(self) -> str:
51
+ """한 줄 요약."""
52
+ if self.tool_name == "file_write":
53
+ return f"[file_write] {self.file_path or 'unknown'}"
54
+ code_preview = (self.code or "")[:80].replace("\n", " ")
55
+ output_preview = (self.output or "")[:60].replace("\n", " ")
56
+ return f"[{self.tool_name}] {code_preview} -> {output_preview}"
57
+
58
+
59
+ class CodeHistoryTracker:
60
+ """스레드별 코드 실행 이력 관리."""
61
+
62
+ def __init__(self) -> None:
63
+ self._history: list[CodeHistoryEntry] = []
64
+
65
+ def add_jupyter_cell(self, code: str, output: Optional[str] = None) -> None:
66
+ self._history.append(
67
+ CodeHistoryEntry(tool_name="jupyter_cell", code=code, output=output)
68
+ )
69
+
70
+ def add_write_file(self, file_path: str, content: Optional[str] = None) -> None:
71
+ self._history.append(
72
+ CodeHistoryEntry(tool_name="file_write", code=content, file_path=file_path)
73
+ )
74
+
75
+ @property
76
+ def entries(self) -> list[CodeHistoryEntry]:
77
+ return list(self._history)
78
+
79
+ def get_context_for_subagent(
80
+ self,
81
+ existing_context: str = "",
82
+ max_tokens: int = _MAX_CONTEXT_TOKENS,
83
+ ) -> str:
84
+ """서브에이전트에 주입할 코드 히스토리 컨텍스트 생성.
85
+
86
+ 최근 _RECENT_FULL_COUNT개는 전체 코드+출력,
87
+ 그 이전은 한 줄 요약으로 토큰 관리.
88
+ """
89
+ if not self._history:
90
+ return existing_context
91
+
92
+ max_chars = max_tokens * _CHARS_PER_TOKEN
93
+ existing_chars = len(existing_context)
94
+ budget = max_chars - existing_chars
95
+ if budget <= 200:
96
+ return existing_context
97
+
98
+ history_parts: list[str] = []
99
+ total = len(self._history)
100
+
101
+ # 오래된 항목: 요약
102
+ if total > _RECENT_FULL_COUNT:
103
+ for entry in self._history[: total - _RECENT_FULL_COUNT]:
104
+ history_parts.append(entry.to_summary_string())
105
+
106
+ # 최근 항목: 전체
107
+ recent_start = max(0, total - _RECENT_FULL_COUNT)
108
+ for entry in self._history[recent_start:]:
109
+ history_parts.append(entry.to_context_string())
110
+
111
+ history_text = "\n\n".join(history_parts)
112
+
113
+ # 예산 초과 시 잘라내기
114
+ if len(history_text) > budget:
115
+ history_text = history_text[-(budget - 50) :]
116
+ history_text = "...(truncated)\n" + history_text
117
+
118
+ section = f"\n\n## Previous Code Execution History\n{history_text}\n"
119
+
120
+ if existing_context:
121
+ return existing_context + section
122
+ return section.strip()
123
+
124
+
125
+ # ---------------------------------------------------------------------------
126
+ # Global tracker registry (per thread_id)
127
+ # ---------------------------------------------------------------------------
128
+
129
+ _code_history_trackers: dict[str, CodeHistoryTracker] = {}
130
+
131
+
132
+ def get_code_history_tracker(thread_id: str) -> CodeHistoryTracker:
133
+ """thread_id별 CodeHistoryTracker 반환 (없으면 새로 생성)."""
134
+ if thread_id not in _code_history_trackers:
135
+ _code_history_trackers[thread_id] = CodeHistoryTracker()
136
+ return _code_history_trackers[thread_id]
137
+
138
+
139
+ def track_tool_execution(
140
+ tool_name: str, args: dict, thread_id: str
141
+ ) -> None:
142
+ """HITL edit decision에서 호출: 코드 실행 이력 기록."""
143
+ tracker = get_code_history_tracker(thread_id)
144
+
145
+ if tool_name == "jupyter_cell":
146
+ code = args.get("code", "")
147
+ exec_result = args.get("execution_result", {})
148
+ output = None
149
+ if isinstance(exec_result, dict):
150
+ output = exec_result.get("output", exec_result.get("text", ""))
151
+ tracker.add_jupyter_cell(code, output)
152
+ logger.debug(
153
+ "CodeHistory: tracked jupyter_cell (code_len=%d, has_output=%s)",
154
+ len(code),
155
+ bool(output),
156
+ )
157
+ elif tool_name == "file_write":
158
+ file_path = args.get("path", "")
159
+ content = args.get("content", "")
160
+ tracker.add_write_file(file_path, content)
161
+ logger.debug("CodeHistory: tracked file_write (path=%s)", file_path)
162
+
163
+
164
+ # ---------------------------------------------------------------------------
165
+ # CodeHistoryMiddleware
166
+ # ---------------------------------------------------------------------------
167
+
168
+
169
+ class CodeHistoryMiddleware(AgentMiddleware):
170
+ """task 도구 호출 인터셉트하여 python_expert에 코드 히스토리 주입."""
171
+
172
+ async def awrap_tool_call(self, request, handler):
173
+ tool_call = request.tool_call
174
+ tool_name = tool_call.get("name", "")
175
+
176
+ if tool_name == "task":
177
+ args = tool_call.get("args", {})
178
+ subagent_type = args.get("subagent_type", args.get("agent_name", ""))
179
+
180
+ if subagent_type in ("python_expert", "python_developer"):
181
+ # thread_id 추출 시도
182
+ thread_id = self._extract_thread_id(request)
183
+ if thread_id:
184
+ tracker = get_code_history_tracker(thread_id)
185
+ if tracker.entries:
186
+ description = args.get("description", "")
187
+ enhanced = tracker.get_context_for_subagent(description)
188
+ new_args = {**args, "description": enhanced}
189
+ new_tool_call = {**tool_call, "args": new_args}
190
+ request = request.override(tool_call=new_tool_call)
191
+ logger.debug(
192
+ "CodeHistory: injected %d entries into %s task",
193
+ len(tracker.entries),
194
+ subagent_type,
195
+ )
196
+
197
+ return await handler(request)
198
+
199
+ def _extract_thread_id(self, request: Any) -> str:
200
+ """request에서 thread_id 추출."""
201
+ # runtime config에서 추출 시도
202
+ if hasattr(request, "config"):
203
+ config = request.config
204
+ if isinstance(config, dict):
205
+ return config.get("configurable", {}).get("thread_id", "")
206
+ # state에서 추출 시도
207
+ if hasattr(request, "state"):
208
+ state = request.state
209
+ if isinstance(state, dict):
210
+ return state.get("thread_id", "")
211
+ return ""