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.
- codebuff_agent-2.1.74.data/data/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/package.json +157 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/150.39fa98117cc8ca590465.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/166.386ff9b2c37ac865fdee.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/216.25cf81131bf7274dabf4.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/353.581bd1cb00746399b62e.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/367.575f9621b63c9a641aa9.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/421.11dac223a2c2da1b09f5.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/481.cc2dec9fa869d66f1407.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/487.0796e931d6e651482c14.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/49.d326f554b6f9ccde6580.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/541.26897ea32845659e36be.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/579.e569d3edd8391fd8fbb3.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/666.d9bfdb23f98489d0a188.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/68.26b6f59c8454adb17f20.js +204 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/694.4ac443814260057e68f7.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/791.61affb8d3480d3d60e0d.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js +2 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/style.js +4 -0
- codebuff_agent-2.1.74.data/data/share/jupyter/labextensions/codebuff/static/third-party-licenses.json +724 -0
- codebuff_agent-2.1.74.dist-info/METADATA +142 -0
- codebuff_agent-2.1.74.dist-info/RECORD +217 -0
- codebuff_agent-2.1.74.dist-info/WHEEL +4 -0
- codebuff_agent-2.1.74.dist-info/licenses/LICENSE +21 -0
- codebuff_core/__init__.py +153 -0
- codebuff_core/codebuff_config_defaults.json +64 -0
- codebuff_core/core/__init__.py +35 -0
- codebuff_core/core/code_validator.py +1238 -0
- codebuff_core/core/error_classifier.py +579 -0
- codebuff_core/core/exceptions.py +49 -0
- codebuff_core/data/tiktoken_cache/9b5ad71b2ce5302211f9c61530b329a4922fc6a4 +100256 -0
- codebuff_core/directory_policy.py +32 -0
- codebuff_core/factory.py +155 -0
- codebuff_core/interfaces.py +197 -0
- codebuff_core/knowledge/__init__.py +31 -0
- codebuff_core/knowledge/chunking.py +375 -0
- codebuff_core/knowledge/loader.py +339 -0
- codebuff_core/knowledge/watchdog_service.py +354 -0
- codebuff_core/legacy_paths.py +69 -0
- codebuff_core/llm/__init__.py +13 -0
- codebuff_core/llm/service.py +407 -0
- codebuff_core/managers/__init__.py +22 -0
- codebuff_core/managers/config_manager.py +690 -0
- codebuff_core/managers/remote_config.py +673 -0
- codebuff_core/managers/remote_skills.py +539 -0
- codebuff_core/managers/session_manager.py +255 -0
- codebuff_core/middleware/__init__.py +3 -0
- codebuff_core/middleware/error_recovery.py +394 -0
- codebuff_core/middleware/logging_utils.py +33 -0
- codebuff_core/models/__init__.py +111 -0
- codebuff_core/models/agent.py +306 -0
- codebuff_core/models/chat.py +57 -0
- codebuff_core/models/common.py +82 -0
- codebuff_core/models/rag.py +300 -0
- codebuff_core/platform_rules.py +127 -0
- codebuff_core/prompts/__init__.py +44 -0
- codebuff_core/prompts/auto_agent_prompts.py +1278 -0
- codebuff_core/services/__init__.py +30 -0
- codebuff_core/services/agent_service.py +345 -0
- codebuff_core/services/analytics_aggregate.py +837 -0
- codebuff_core/services/chat_service.py +898 -0
- codebuff_core/services/context_condenser.py +306 -0
- codebuff_core/services/pii_guardrail.py +393 -0
- codebuff_core/services/rag_chat_agent.py +2993 -0
- codebuff_core/services/rag_collections.py +2740 -0
- codebuff_core/services/rag_manager.py +890 -0
- codebuff_core/services/rag_service.py +145 -0
- codebuff_core/services/retriever.py +260 -0
- codebuff_core/services/vllm_embedding_service.py +290 -0
- codebuff_core/tests/__init__.py +1 -0
- codebuff_core/tests/conftest.py +81 -0
- codebuff_core/tests/test_error_recovery.py +647 -0
- codebuff_core/tests/test_factory.py +135 -0
- codebuff_core/tests/test_services.py +150 -0
- jupyter_ext/__init__.py +266 -0
- jupyter_ext/_version.py +4 -0
- jupyter_ext/agent/__init__.py +49 -0
- jupyter_ext/agent/agent_factory.py +2270 -0
- jupyter_ext/agent/ast_tools.py +451 -0
- jupyter_ext/agent/autofix_middleware.py +481 -0
- jupyter_ext/agent/background_procs.py +372 -0
- jupyter_ext/agent/code_history_middleware.py +211 -0
- jupyter_ext/agent/codex_autofix.py +160 -0
- jupyter_ext/agent/codex_bridge.py +3339 -0
- jupyter_ext/agent/codex_hooks.py +320 -0
- jupyter_ext/agent/codex_slash.py +685 -0
- jupyter_ext/agent/codex_threads.py +114 -0
- jupyter_ext/agent/content_injection.py +377 -0
- jupyter_ext/agent/continuation_middleware.py +548 -0
- jupyter_ext/agent/custom_skills.py +335 -0
- jupyter_ext/agent/file_diffs.py +99 -0
- jupyter_ext/agent/git_history.py +188 -0
- jupyter_ext/agent/guardrail_middleware.py +421 -0
- jupyter_ext/agent/json_repair_utils.py +54 -0
- jupyter_ext/agent/langchain_tools.py +2673 -0
- jupyter_ext/agent/mermaid_fixer.py +289 -0
- jupyter_ext/agent/mermaid_middleware.py +135 -0
- jupyter_ext/agent/middleware.py +359 -0
- jupyter_ext/agent/middleware_prompts.py +53 -0
- jupyter_ext/agent/observability.py +334 -0
- jupyter_ext/agent/patch_tool_calls.py +57 -0
- jupyter_ext/agent/prompts.py +112 -0
- jupyter_ext/agent/skill_middleware.py +257 -0
- jupyter_ext/agent/skills/airflow_dag_development.md +36 -0
- jupyter_ext/agent/skills/anomaly_detection.md +12 -0
- jupyter_ext/agent/skills/athena_sql.md +156 -0
- jupyter_ext/agent/skills/bdp_table_search.md +33 -0
- jupyter_ext/agent/skills/bigquery.md +65 -0
- jupyter_ext/agent/skills/bigquery_oracle.md +231 -0
- jupyter_ext/agent/skills/bigquery_tuning.md +307 -0
- jupyter_ext/agent/skills/classification_evaluation.md +11 -0
- jupyter_ext/agent/skills/clustering_analysis.md +12 -0
- jupyter_ext/agent/skills/comprehensive_eda.md +11 -0
- jupyter_ext/agent/skills/data_analysis.md +236 -0
- jupyter_ext/agent/skills/data_cleaning_pipeline.md +12 -0
- jupyter_ext/agent/skills/data_loading.md +158 -0
- jupyter_ext/agent/skills/data_quality_report.md +13 -0
- jupyter_ext/agent/skills/dimensionality_reduction.md +12 -0
- jupyter_ext/agent/skills/emr_script_development.md +17 -0
- jupyter_ext/agent/skills/experiment_tracking.md +13 -0
- jupyter_ext/agent/skills/feature_importance_analysis.md +10 -0
- jupyter_ext/agent/skills/hyperparameter_tuning.md +12 -0
- jupyter_ext/agent/skills/hypothesis_testing.md +12 -0
- jupyter_ext/agent/skills/imbalanced_data_handling.md +12 -0
- jupyter_ext/agent/skills/inference.md +392 -0
- jupyter_ext/agent/skills/lambda_function_development.md +19 -0
- jupyter_ext/agent/skills/model_comparison.md +11 -0
- jupyter_ext/agent/skills/model_explainability.md +12 -0
- jupyter_ext/agent/skills/model_training.md +318 -0
- jupyter_ext/agent/skills/pyspark.md +352 -0
- jupyter_ext/agent/skills/python_code_quality.md +35 -0
- jupyter_ext/agent/skills/regression_evaluation.md +12 -0
- jupyter_ext/agent/skills/spring_java_development.md +35 -0
- jupyter_ext/agent/skills/text_preprocessing.md +12 -0
- jupyter_ext/agent/skills/time_series_analysis.md +13 -0
- jupyter_ext/agent/skills/unit_testing.md +203 -0
- jupyter_ext/agent/todo_prompts.py +57 -0
- jupyter_ext/agent/tool_report.py +45 -0
- jupyter_ext/analytics_ingest.py +599 -0
- jupyter_ext/etc/jupyter/jupyter_server_config.d/codebuff_agent.json +7 -0
- jupyter_ext/handlers/__init__.py +8 -0
- jupyter_ext/handlers/analytics_handler.py +170 -0
- jupyter_ext/handlers/benchmark_handler.py +794 -0
- jupyter_ext/handlers/eda_benchmark_handler.py +1328 -0
- jupyter_ext/handlers/rag_admin_handler.py +689 -0
- jupyter_ext/handlers/websocket.py +497 -0
- jupyter_ext/http_handlers.py +8744 -0
- jupyter_ext/idle_monitor.py +346 -0
- jupyter_ext/labextension/package.json +157 -0
- jupyter_ext/labextension/static/104.2b8f20f9d1ac2c9db01b.js +1 -0
- jupyter_ext/labextension/static/150.39fa98117cc8ca590465.js +1 -0
- jupyter_ext/labextension/static/166.386ff9b2c37ac865fdee.js +1 -0
- jupyter_ext/labextension/static/216.25cf81131bf7274dabf4.js +1 -0
- jupyter_ext/labextension/static/353.581bd1cb00746399b62e.js +1 -0
- jupyter_ext/labextension/static/367.575f9621b63c9a641aa9.js +1 -0
- jupyter_ext/labextension/static/421.11dac223a2c2da1b09f5.js +1 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js +2 -0
- jupyter_ext/labextension/static/432.8513b4c6cc1d1232d165.js.LICENSE.txt +19 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js +2 -0
- jupyter_ext/labextension/static/439.ba5b5ef70ec6de214b8c.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/481.cc2dec9fa869d66f1407.js +1 -0
- jupyter_ext/labextension/static/487.0796e931d6e651482c14.js +1 -0
- jupyter_ext/labextension/static/49.d326f554b6f9ccde6580.js +1 -0
- jupyter_ext/labextension/static/541.26897ea32845659e36be.js +1 -0
- jupyter_ext/labextension/static/579.e569d3edd8391fd8fbb3.js +1 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js +2 -0
- jupyter_ext/labextension/static/58.eb7cc679f7535cc8a2f1.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/666.d9bfdb23f98489d0a188.js +1 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js +2 -0
- jupyter_ext/labextension/static/677.d266c39dbed33bc388b3.js.LICENSE.txt +8 -0
- jupyter_ext/labextension/static/68.26b6f59c8454adb17f20.js +204 -0
- jupyter_ext/labextension/static/694.4ac443814260057e68f7.js +1 -0
- jupyter_ext/labextension/static/791.61affb8d3480d3d60e0d.js +1 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js +2 -0
- jupyter_ext/labextension/static/848.4d2db09cfbb09374b299.js.LICENSE.txt +9 -0
- jupyter_ext/labextension/static/868.5cfe7e1a348ec5af1b8c.js +1 -0
- jupyter_ext/labextension/static/97.ea559a5b8a9a09f17c6b.js +1 -0
- jupyter_ext/labextension/static/remoteEntry.5292fc9c6df7e7920cc8.js +1 -0
- jupyter_ext/labextension/static/style.js +4 -0
- jupyter_ext/labextension/static/third-party-licenses.json +724 -0
- jupyter_ext/log_paths.py +113 -0
- jupyter_ext/resource_usage.py +519 -0
- jupyter_ext/secrets_bootstrap.py +116 -0
- jupyter_ext/team_access.py +105 -0
- jupyter_ext/tests/__init__.py +1 -0
- jupyter_ext/tests/test_context.py +234 -0
- jupyter_ext/tests/test_factory.py +563 -0
- jupyter_ext/tests/test_file_finder.py +139 -0
- jupyter_ext/tests/test_handlers.py +58 -0
- jupyter_ext/tests/test_mcp_selftest.py +204 -0
- jupyter_ext/tests/test_mermaid_fixer.py +272 -0
- jupyter_ext/tests/test_resource_tiers.py +298 -0
- jupyter_ext/tools/__init__.py +15 -0
- jupyter_ext/tools/base.py +90 -0
- jupyter_ext/tools/context.py +268 -0
- jupyter_ext/tools/dir_complete.py +86 -0
- jupyter_ext/tools/file_finder.py +303 -0
- jupyter_ext/tools/file_tools.py +244 -0
- jupyter_ext/tools/jupyter_tools.py +296 -0
- jupyter_ext/tools/registry.py +159 -0
- jupyter_ext/tools/search_tools.py +230 -0
- jupyter_ext/tools/security_tools.py +657 -0
- jupyter_ext/tools/shell_tools.py +109 -0
- 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 ""
|