hdsp-jupyter-extension 2.0.23__py3-none-any.whl → 2.0.25__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.
- agent_server/context_providers/__init__.py +22 -0
- agent_server/context_providers/actions.py +45 -0
- agent_server/context_providers/base.py +231 -0
- agent_server/context_providers/file.py +316 -0
- agent_server/context_providers/processor.py +150 -0
- agent_server/main.py +2 -1
- agent_server/routers/chat.py +61 -10
- agent_server/routers/context.py +168 -0
- agent_server/routers/langchain_agent.py +609 -182
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
- hdsp_jupyter_extension-2.0.23.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.96745acc14125453fba8.js → hdsp_jupyter_extension-2.0.25.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js +245 -121
- hdsp_jupyter_extension-2.0.25.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js.map +1 -0
- jupyter_ext/labextension/static/lib_index_js.2d5ea542350862f7c531.js → hdsp_jupyter_extension-2.0.25.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js +583 -39
- hdsp_jupyter_extension-2.0.25.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js.map +1 -0
- hdsp_jupyter_extension-2.0.23.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.f0127d8744730f2092c1.js → hdsp_jupyter_extension-2.0.25.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.ffc2b4bc8e6cb300e1e1.js +3 -3
- jupyter_ext/labextension/static/remoteEntry.f0127d8744730f2092c1.js.map → hdsp_jupyter_extension-2.0.25.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.ffc2b4bc8e6cb300e1e1.js.map +1 -1
- {hdsp_jupyter_extension-2.0.23.dist-info → hdsp_jupyter_extension-2.0.25.dist-info}/METADATA +1 -1
- {hdsp_jupyter_extension-2.0.23.dist-info → hdsp_jupyter_extension-2.0.25.dist-info}/RECORD +50 -44
- jupyter_ext/_version.py +1 -1
- jupyter_ext/handlers.py +29 -0
- jupyter_ext/labextension/build_log.json +1 -1
- jupyter_ext/labextension/package.json +2 -2
- jupyter_ext/labextension/static/{frontend_styles_index_js.96745acc14125453fba8.js → frontend_styles_index_js.b5e4416b4e07ec087aad.js} +245 -121
- jupyter_ext/labextension/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js.map +1 -0
- hdsp_jupyter_extension-2.0.23.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.2d5ea542350862f7c531.js → jupyter_ext/labextension/static/lib_index_js.67505497667f9c0a763d.js +583 -39
- jupyter_ext/labextension/static/lib_index_js.67505497667f9c0a763d.js.map +1 -0
- jupyter_ext/labextension/static/{remoteEntry.f0127d8744730f2092c1.js → remoteEntry.ffc2b4bc8e6cb300e1e1.js} +3 -3
- hdsp_jupyter_extension-2.0.23.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.f0127d8744730f2092c1.js.map → jupyter_ext/labextension/static/remoteEntry.ffc2b4bc8e6cb300e1e1.js.map +1 -1
- hdsp_jupyter_extension-2.0.23.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.96745acc14125453fba8.js.map +0 -1
- hdsp_jupyter_extension-2.0.23.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.2d5ea542350862f7c531.js.map +0 -1
- jupyter_ext/labextension/static/frontend_styles_index_js.96745acc14125453fba8.js.map +0 -1
- jupyter_ext/labextension/static/lib_index_js.2d5ea542350862f7c531.js.map +0 -1
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.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
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.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
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.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
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.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
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.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
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.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
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +0 -0
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.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
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +0 -0
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.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
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.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
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +0 -0
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.25.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -0
- {hdsp_jupyter_extension-2.0.23.dist-info → hdsp_jupyter_extension-2.0.25.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.23.dist-info → hdsp_jupyter_extension-2.0.25.dist-info}/licenses/LICENSE +0 -0
|
@@ -24,6 +24,10 @@ from agent_server.langchain.agent import (
|
|
|
24
24
|
create_agent_system,
|
|
25
25
|
)
|
|
26
26
|
from agent_server.langchain.llm_factory import create_llm
|
|
27
|
+
from agent_server.langchain.logging_utils import (
|
|
28
|
+
LOG_RESPONSE_END,
|
|
29
|
+
LOG_RESPONSE_START,
|
|
30
|
+
)
|
|
27
31
|
from agent_server.langchain.middleware.code_history_middleware import (
|
|
28
32
|
track_tool_execution,
|
|
29
33
|
)
|
|
@@ -65,10 +69,12 @@ def get_subagent_debug_events():
|
|
|
65
69
|
events = drain_subagent_events()
|
|
66
70
|
sse_events = []
|
|
67
71
|
for event in events:
|
|
68
|
-
sse_events.append(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
+
sse_events.append(
|
|
73
|
+
{
|
|
74
|
+
"event": "debug",
|
|
75
|
+
"data": json.dumps(event.to_status_dict()),
|
|
76
|
+
}
|
|
77
|
+
)
|
|
72
78
|
return sse_events
|
|
73
79
|
|
|
74
80
|
|
|
@@ -94,7 +100,7 @@ def _get_tool_status_message(
|
|
|
94
100
|
nb_path = tool_args.get("notebook_path", "all notebooks")
|
|
95
101
|
return {
|
|
96
102
|
"status": f"노트북 검색: '{pattern}' in {nb_path or 'all notebooks'}",
|
|
97
|
-
"icon": "search"
|
|
103
|
+
"icon": "search",
|
|
98
104
|
}
|
|
99
105
|
elif tool_name_normalized in ("task", "task_tool"):
|
|
100
106
|
# Show subagent delegation details with expand support
|
|
@@ -402,17 +408,22 @@ def _normalize_action_request(action: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
402
408
|
or {}
|
|
403
409
|
)
|
|
404
410
|
# Try to get description from action first, then from args (for jupyter_cell_tool etc)
|
|
405
|
-
description = action.get("description", "") or (
|
|
411
|
+
description = action.get("description", "") or (
|
|
412
|
+
args.get("description", "") if isinstance(args, dict) else ""
|
|
413
|
+
)
|
|
406
414
|
|
|
407
415
|
# Auto-inject description for jupyter_cell_tool from python_developer's response
|
|
408
416
|
# Only inject into args.description, keep top-level description as HITL default
|
|
409
417
|
if name == "jupyter_cell_tool":
|
|
410
|
-
logger.info(
|
|
418
|
+
logger.info(
|
|
419
|
+
f"[HITL] jupyter_cell_tool detected, current description: '{description[:50] if description else 'None'}'"
|
|
420
|
+
)
|
|
411
421
|
try:
|
|
412
422
|
from agent_server.langchain.middleware.description_injector import (
|
|
413
423
|
clear_pending_description,
|
|
414
424
|
get_pending_description,
|
|
415
425
|
)
|
|
426
|
+
|
|
416
427
|
pending = get_pending_description()
|
|
417
428
|
if pending:
|
|
418
429
|
# Inject into args.description only (for detailed description display)
|
|
@@ -421,7 +432,9 @@ def _normalize_action_request(action: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
421
432
|
args = dict(args)
|
|
422
433
|
args["description"] = pending
|
|
423
434
|
clear_pending_description()
|
|
424
|
-
logger.info(
|
|
435
|
+
logger.info(
|
|
436
|
+
f"[HITL] Auto-injected description into args: {pending[:80]}..."
|
|
437
|
+
)
|
|
425
438
|
else:
|
|
426
439
|
logger.info("[HITL] No pending description from python_developer")
|
|
427
440
|
except Exception as e:
|
|
@@ -856,7 +869,11 @@ async def stream_agent(request: AgentRequest):
|
|
|
856
869
|
}
|
|
857
870
|
# Build previous todos context for LLM
|
|
858
871
|
if existing_todos:
|
|
859
|
-
completed_items = [
|
|
872
|
+
completed_items = [
|
|
873
|
+
t.get("content", "")
|
|
874
|
+
for t in existing_todos
|
|
875
|
+
if t.get("status") == "completed"
|
|
876
|
+
]
|
|
860
877
|
if completed_items:
|
|
861
878
|
items_summary = ", ".join(completed_items[:5])
|
|
862
879
|
if len(completed_items) > 5:
|
|
@@ -865,7 +882,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
865
882
|
f"[SYSTEM] 이전 todo list가 완료 혹은 취소되었습니다. 완료된 작업: {items_summary}. "
|
|
866
883
|
f"새 작업을 시작합니다. 이전 todo list에 신규 작업을 append 하지 말고 새로운 todo list를 생성하세요."
|
|
867
884
|
)
|
|
868
|
-
logger.info(
|
|
885
|
+
logger.info(
|
|
886
|
+
"Injecting previous todos context: %s",
|
|
887
|
+
previous_todos_context[:100],
|
|
888
|
+
)
|
|
869
889
|
except Exception as e:
|
|
870
890
|
logger.warning("Could not reset todos in agent state: %s", e)
|
|
871
891
|
|
|
@@ -895,7 +915,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
895
915
|
|
|
896
916
|
# Initial status: waiting for LLM
|
|
897
917
|
logger.info("SSE: Sending initial debug status 'LLM 응답 대기 중'")
|
|
898
|
-
yield {
|
|
918
|
+
yield {
|
|
919
|
+
"event": "debug",
|
|
920
|
+
"data": json.dumps({"status": "LLM 응답 대기 중", "icon": "thinking"}),
|
|
921
|
+
}
|
|
899
922
|
|
|
900
923
|
# Main streaming loop
|
|
901
924
|
async for step in _async_stream_wrapper(
|
|
@@ -903,9 +926,16 @@ async def stream_agent(request: AgentRequest):
|
|
|
903
926
|
):
|
|
904
927
|
# Check if thread was cancelled by user
|
|
905
928
|
if is_thread_cancelled(thread_id):
|
|
906
|
-
logger.info(
|
|
929
|
+
logger.info(
|
|
930
|
+
f"Thread {thread_id} cancelled by user, stopping stream"
|
|
931
|
+
)
|
|
907
932
|
clear_cancelled_thread(thread_id)
|
|
908
|
-
yield {
|
|
933
|
+
yield {
|
|
934
|
+
"event": "cancelled",
|
|
935
|
+
"data": json.dumps(
|
|
936
|
+
{"message": "작업이 사용자에 의해 중단되었습니다."}
|
|
937
|
+
),
|
|
938
|
+
}
|
|
909
939
|
return
|
|
910
940
|
|
|
911
941
|
if isinstance(step, dict):
|
|
@@ -915,7 +945,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
915
945
|
# DEBUG: Check for __interrupt__ in every step
|
|
916
946
|
if "__interrupt__" in step:
|
|
917
947
|
logger.info("[DEBUG-INTERRUPT] Found __interrupt__ in step!")
|
|
918
|
-
logger.info(
|
|
948
|
+
logger.info(
|
|
949
|
+
"[DEBUG-INTERRUPT] interrupt value: %s",
|
|
950
|
+
str(step["__interrupt__"])[:500],
|
|
951
|
+
)
|
|
919
952
|
|
|
920
953
|
# IMPORTANT: Process todos and messages BEFORE checking for interrupt
|
|
921
954
|
# This ensures todos/debug events are emitted even in interrupt steps
|
|
@@ -980,7 +1013,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
980
1013
|
todos = _extract_todos(last_message.content)
|
|
981
1014
|
if todos:
|
|
982
1015
|
latest_todos = todos
|
|
983
|
-
yield {
|
|
1016
|
+
yield {
|
|
1017
|
+
"event": "todos",
|
|
1018
|
+
"data": json.dumps({"todos": todos}),
|
|
1019
|
+
}
|
|
984
1020
|
# Check if all todos are completed - auto terminate only if summary exists
|
|
985
1021
|
all_completed = all(
|
|
986
1022
|
t.get("status") == "completed" for t in todos
|
|
@@ -989,16 +1025,31 @@ async def stream_agent(request: AgentRequest):
|
|
|
989
1025
|
# Check if summary JSON exists in the CURRENT step's AIMessage
|
|
990
1026
|
# (not in history, to avoid false positives from previous tasks)
|
|
991
1027
|
summary_exists = False
|
|
992
|
-
step_messages =
|
|
1028
|
+
step_messages = (
|
|
1029
|
+
step.get("messages", [])
|
|
1030
|
+
if isinstance(step, dict)
|
|
1031
|
+
else []
|
|
1032
|
+
)
|
|
993
1033
|
# Only check the AIMessage that called write_todos (should be right before this ToolMessage)
|
|
994
|
-
for recent_msg in step_messages[
|
|
1034
|
+
for recent_msg in step_messages[
|
|
1035
|
+
-3:
|
|
1036
|
+
]: # Check only the most recent few messages
|
|
995
1037
|
if isinstance(recent_msg, AIMessage):
|
|
996
|
-
recent_content =
|
|
1038
|
+
recent_content = (
|
|
1039
|
+
getattr(recent_msg, "content", "") or ""
|
|
1040
|
+
)
|
|
997
1041
|
if isinstance(recent_content, list):
|
|
998
|
-
recent_content = " ".join(
|
|
999
|
-
|
|
1042
|
+
recent_content = " ".join(
|
|
1043
|
+
str(p) for p in recent_content
|
|
1044
|
+
)
|
|
1045
|
+
if (
|
|
1046
|
+
'"summary"' in recent_content
|
|
1047
|
+
and '"next_items"' in recent_content
|
|
1048
|
+
):
|
|
1000
1049
|
summary_exists = True
|
|
1001
|
-
logger.info(
|
|
1050
|
+
logger.info(
|
|
1051
|
+
"Found summary in current AIMessage content"
|
|
1052
|
+
)
|
|
1002
1053
|
break
|
|
1003
1054
|
|
|
1004
1055
|
if summary_exists:
|
|
@@ -1010,24 +1061,54 @@ async def stream_agent(request: AgentRequest):
|
|
|
1010
1061
|
# Find and emit the AIMessage content with summary
|
|
1011
1062
|
for recent_msg in step_messages[-3:]:
|
|
1012
1063
|
if isinstance(recent_msg, AIMessage):
|
|
1013
|
-
step_content =
|
|
1064
|
+
step_content = (
|
|
1065
|
+
getattr(recent_msg, "content", "")
|
|
1066
|
+
or ""
|
|
1067
|
+
)
|
|
1014
1068
|
if isinstance(step_content, list):
|
|
1015
|
-
step_content = " ".join(
|
|
1016
|
-
|
|
1069
|
+
step_content = " ".join(
|
|
1070
|
+
str(p) for p in step_content
|
|
1071
|
+
)
|
|
1072
|
+
if (
|
|
1073
|
+
'"summary"' in step_content
|
|
1074
|
+
and '"next_items"' in step_content
|
|
1075
|
+
):
|
|
1017
1076
|
content_hash = hash(step_content)
|
|
1018
|
-
if
|
|
1019
|
-
|
|
1020
|
-
|
|
1077
|
+
if (
|
|
1078
|
+
content_hash
|
|
1079
|
+
not in emitted_contents
|
|
1080
|
+
):
|
|
1081
|
+
emitted_contents.add(
|
|
1082
|
+
content_hash
|
|
1083
|
+
)
|
|
1084
|
+
repaired_content = _repair_summary_json_content(
|
|
1085
|
+
step_content
|
|
1086
|
+
)
|
|
1021
1087
|
logger.info(
|
|
1022
1088
|
"Step auto-terminate: EMITTING summary content (len=%d): %s",
|
|
1023
1089
|
len(repaired_content),
|
|
1024
1090
|
repaired_content[:100],
|
|
1025
1091
|
)
|
|
1026
1092
|
produced_output = True
|
|
1027
|
-
yield {
|
|
1093
|
+
yield {
|
|
1094
|
+
"event": "token",
|
|
1095
|
+
"data": json.dumps(
|
|
1096
|
+
{
|
|
1097
|
+
"content": repaired_content
|
|
1098
|
+
}
|
|
1099
|
+
),
|
|
1100
|
+
}
|
|
1028
1101
|
break
|
|
1029
|
-
yield {
|
|
1030
|
-
|
|
1102
|
+
yield {
|
|
1103
|
+
"event": "debug_clear",
|
|
1104
|
+
"data": json.dumps({}),
|
|
1105
|
+
}
|
|
1106
|
+
yield {
|
|
1107
|
+
"event": "done",
|
|
1108
|
+
"data": json.dumps(
|
|
1109
|
+
{"reason": "all_todos_completed"}
|
|
1110
|
+
),
|
|
1111
|
+
}
|
|
1031
1112
|
return # Exit the generator
|
|
1032
1113
|
else:
|
|
1033
1114
|
logger.warning(
|
|
@@ -1056,49 +1137,37 @@ async def stream_agent(request: AgentRequest):
|
|
|
1056
1137
|
|
|
1057
1138
|
# Handle AIMessage
|
|
1058
1139
|
elif isinstance(last_message, AIMessage):
|
|
1059
|
-
# LLM Response
|
|
1060
|
-
print(
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
"SimpleAgent AIMessage content: %s",
|
|
1066
|
-
last_message.content or "",
|
|
1067
|
-
)
|
|
1068
|
-
logger.info(
|
|
1069
|
-
"SimpleAgent AIMessage tool_calls: %s",
|
|
1070
|
-
json.dumps(last_message.tool_calls, ensure_ascii=False)
|
|
1140
|
+
# LLM Response - structured JSON format
|
|
1141
|
+
print(LOG_RESPONSE_START, flush=True)
|
|
1142
|
+
response_data = {
|
|
1143
|
+
"type": "AIMessage",
|
|
1144
|
+
"content": last_message.content or "",
|
|
1145
|
+
"tool_calls": last_message.tool_calls
|
|
1071
1146
|
if hasattr(last_message, "tool_calls")
|
|
1072
|
-
else
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
ensure_ascii=False,
|
|
1088
|
-
),
|
|
1089
|
-
)
|
|
1090
|
-
logger.info(
|
|
1091
|
-
"SimpleAgent AIMessage usage_metadata: %s",
|
|
1147
|
+
else [],
|
|
1148
|
+
"additional_kwargs": getattr(
|
|
1149
|
+
last_message, "additional_kwargs", {}
|
|
1150
|
+
)
|
|
1151
|
+
or {},
|
|
1152
|
+
"response_metadata": getattr(
|
|
1153
|
+
last_message, "response_metadata", {}
|
|
1154
|
+
)
|
|
1155
|
+
or {},
|
|
1156
|
+
"usage_metadata": getattr(
|
|
1157
|
+
last_message, "usage_metadata", {}
|
|
1158
|
+
)
|
|
1159
|
+
or {},
|
|
1160
|
+
}
|
|
1161
|
+
print(
|
|
1092
1162
|
json.dumps(
|
|
1093
|
-
|
|
1163
|
+
response_data,
|
|
1164
|
+
indent=2,
|
|
1094
1165
|
ensure_ascii=False,
|
|
1166
|
+
default=str,
|
|
1095
1167
|
),
|
|
1168
|
+
flush=True,
|
|
1096
1169
|
)
|
|
1097
|
-
|
|
1098
|
-
print("=" * 96, flush=True)
|
|
1099
|
-
print(" ✅ LLM RESPONSE END", flush=True)
|
|
1100
|
-
print("=" * 96, flush=True)
|
|
1101
|
-
print("🔵" * 48 + "\n", flush=True)
|
|
1170
|
+
print(LOG_RESPONSE_END, flush=True)
|
|
1102
1171
|
last_finish_reason = (
|
|
1103
1172
|
getattr(last_message, "response_metadata", {}) or {}
|
|
1104
1173
|
).get("finish_reason")
|
|
@@ -1157,10 +1226,22 @@ async def stream_agent(request: AgentRequest):
|
|
|
1157
1226
|
logger.warning(
|
|
1158
1227
|
"MALFORMED_FUNCTION_CALL with empty response - sending error to client"
|
|
1159
1228
|
)
|
|
1160
|
-
yield {
|
|
1229
|
+
yield {
|
|
1230
|
+
"event": "token",
|
|
1231
|
+
"data": json.dumps(
|
|
1232
|
+
{
|
|
1161
1233
|
"content": "\n\n[경고] LLM이 잘못된 응답을 반환했습니다. 다시 시도해주세요.\n"
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1234
|
+
}
|
|
1235
|
+
),
|
|
1236
|
+
}
|
|
1237
|
+
yield {
|
|
1238
|
+
"event": "debug",
|
|
1239
|
+
"data": json.dumps(
|
|
1240
|
+
{
|
|
1241
|
+
"status": "[경고] MALFORMED_FUNCTION_CALL 에러"
|
|
1242
|
+
}
|
|
1243
|
+
),
|
|
1244
|
+
}
|
|
1164
1245
|
produced_output = True
|
|
1165
1246
|
# Continue to let agent retry on next iteration
|
|
1166
1247
|
|
|
@@ -1190,7 +1271,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
1190
1271
|
len(todos),
|
|
1191
1272
|
)
|
|
1192
1273
|
latest_todos = todos
|
|
1193
|
-
yield {
|
|
1274
|
+
yield {
|
|
1275
|
+
"event": "todos",
|
|
1276
|
+
"data": json.dumps({"todos": todos}),
|
|
1277
|
+
}
|
|
1194
1278
|
# Check if all todos are completed - terminate early
|
|
1195
1279
|
all_completed = all(
|
|
1196
1280
|
t.get("status") == "completed" for t in todos
|
|
@@ -1232,7 +1316,9 @@ async def stream_agent(request: AgentRequest):
|
|
|
1232
1316
|
"## 다음 단계",
|
|
1233
1317
|
]
|
|
1234
1318
|
)
|
|
1235
|
-
has_summary =
|
|
1319
|
+
has_summary = (
|
|
1320
|
+
has_summary_json or has_markdown_summary
|
|
1321
|
+
)
|
|
1236
1322
|
|
|
1237
1323
|
# Only check current AIMessage for summary (not history, to avoid false positives)
|
|
1238
1324
|
if not has_summary:
|
|
@@ -1247,20 +1333,41 @@ async def stream_agent(request: AgentRequest):
|
|
|
1247
1333
|
)
|
|
1248
1334
|
# IMPORTANT: Emit the summary content BEFORE terminating
|
|
1249
1335
|
# so the UI can display the summary JSON
|
|
1250
|
-
if msg_content and isinstance(
|
|
1336
|
+
if msg_content and isinstance(
|
|
1337
|
+
msg_content, str
|
|
1338
|
+
):
|
|
1251
1339
|
content_hash = hash(msg_content)
|
|
1252
1340
|
if content_hash not in emitted_contents:
|
|
1253
1341
|
emitted_contents.add(content_hash)
|
|
1254
|
-
repaired_content =
|
|
1342
|
+
repaired_content = (
|
|
1343
|
+
_repair_summary_json_content(
|
|
1344
|
+
msg_content
|
|
1345
|
+
)
|
|
1346
|
+
)
|
|
1255
1347
|
logger.info(
|
|
1256
1348
|
"Auto-terminate: EMITTING summary content (len=%d): %s",
|
|
1257
1349
|
len(repaired_content),
|
|
1258
1350
|
repaired_content[:100],
|
|
1259
1351
|
)
|
|
1260
1352
|
produced_output = True
|
|
1261
|
-
yield {
|
|
1262
|
-
|
|
1263
|
-
|
|
1353
|
+
yield {
|
|
1354
|
+
"event": "token",
|
|
1355
|
+
"data": json.dumps(
|
|
1356
|
+
{
|
|
1357
|
+
"content": repaired_content
|
|
1358
|
+
}
|
|
1359
|
+
),
|
|
1360
|
+
}
|
|
1361
|
+
yield {
|
|
1362
|
+
"event": "debug_clear",
|
|
1363
|
+
"data": json.dumps({}),
|
|
1364
|
+
}
|
|
1365
|
+
yield {
|
|
1366
|
+
"event": "done",
|
|
1367
|
+
"data": json.dumps(
|
|
1368
|
+
{"reason": "all_todos_completed"}
|
|
1369
|
+
),
|
|
1370
|
+
}
|
|
1264
1371
|
return # Exit before executing more tool calls
|
|
1265
1372
|
for tool_call in tool_calls:
|
|
1266
1373
|
tool_name = tool_call.get("name", "unknown")
|
|
@@ -1275,7 +1382,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
1275
1382
|
"SSE: Emitting debug event for tool: %s",
|
|
1276
1383
|
tool_name,
|
|
1277
1384
|
)
|
|
1278
|
-
yield {
|
|
1385
|
+
yield {
|
|
1386
|
+
"event": "debug",
|
|
1387
|
+
"data": json.dumps(status_msg),
|
|
1388
|
+
}
|
|
1279
1389
|
|
|
1280
1390
|
# Send tool_call event with details for frontend to execute
|
|
1281
1391
|
if tool_name in (
|
|
@@ -1283,37 +1393,55 @@ async def stream_agent(request: AgentRequest):
|
|
|
1283
1393
|
"jupyter_cell",
|
|
1284
1394
|
):
|
|
1285
1395
|
produced_output = True
|
|
1286
|
-
yield {
|
|
1396
|
+
yield {
|
|
1397
|
+
"event": "tool_call",
|
|
1398
|
+
"data": json.dumps(
|
|
1399
|
+
{
|
|
1287
1400
|
"tool": "jupyter_cell",
|
|
1288
1401
|
"code": tool_args.get("code", ""),
|
|
1289
1402
|
"description": tool_args.get(
|
|
1290
1403
|
"description", ""
|
|
1291
1404
|
),
|
|
1292
|
-
}
|
|
1405
|
+
}
|
|
1406
|
+
),
|
|
1407
|
+
}
|
|
1293
1408
|
elif tool_name in ("markdown_tool", "markdown"):
|
|
1294
1409
|
produced_output = True
|
|
1295
|
-
yield {
|
|
1410
|
+
yield {
|
|
1411
|
+
"event": "tool_call",
|
|
1412
|
+
"data": json.dumps(
|
|
1413
|
+
{
|
|
1296
1414
|
"tool": "markdown",
|
|
1297
1415
|
"content": tool_args.get(
|
|
1298
1416
|
"content", ""
|
|
1299
1417
|
),
|
|
1300
|
-
}
|
|
1418
|
+
}
|
|
1419
|
+
),
|
|
1420
|
+
}
|
|
1301
1421
|
elif tool_name == "execute_command_tool":
|
|
1302
1422
|
produced_output = True
|
|
1303
|
-
yield {
|
|
1423
|
+
yield {
|
|
1424
|
+
"event": "tool_call",
|
|
1425
|
+
"data": json.dumps(
|
|
1426
|
+
{
|
|
1304
1427
|
"tool": "execute_command_tool",
|
|
1305
1428
|
"command": tool_args.get(
|
|
1306
1429
|
"command", ""
|
|
1307
1430
|
),
|
|
1308
1431
|
"timeout": tool_args.get("timeout"),
|
|
1309
|
-
}
|
|
1432
|
+
}
|
|
1433
|
+
),
|
|
1434
|
+
}
|
|
1310
1435
|
elif tool_name in (
|
|
1311
1436
|
"search_notebook_cells_tool",
|
|
1312
1437
|
"search_notebook_cells",
|
|
1313
1438
|
):
|
|
1314
1439
|
# Search notebook cells - emit tool_call for client-side execution
|
|
1315
1440
|
produced_output = True
|
|
1316
|
-
yield {
|
|
1441
|
+
yield {
|
|
1442
|
+
"event": "tool_call",
|
|
1443
|
+
"data": json.dumps(
|
|
1444
|
+
{
|
|
1317
1445
|
"tool": "search_notebook_cells",
|
|
1318
1446
|
"pattern": tool_args.get(
|
|
1319
1447
|
"pattern", ""
|
|
@@ -1330,7 +1458,9 @@ async def stream_agent(request: AgentRequest):
|
|
|
1330
1458
|
"case_sensitive": tool_args.get(
|
|
1331
1459
|
"case_sensitive", False
|
|
1332
1460
|
),
|
|
1333
|
-
}
|
|
1461
|
+
}
|
|
1462
|
+
),
|
|
1463
|
+
}
|
|
1334
1464
|
|
|
1335
1465
|
# Only display content if it's not empty and not a JSON tool response
|
|
1336
1466
|
if (
|
|
@@ -1399,7 +1529,12 @@ async def stream_agent(request: AgentRequest):
|
|
|
1399
1529
|
repaired_content[:100],
|
|
1400
1530
|
)
|
|
1401
1531
|
produced_output = True
|
|
1402
|
-
yield {
|
|
1532
|
+
yield {
|
|
1533
|
+
"event": "token",
|
|
1534
|
+
"data": json.dumps(
|
|
1535
|
+
{"content": repaired_content}
|
|
1536
|
+
),
|
|
1537
|
+
}
|
|
1403
1538
|
|
|
1404
1539
|
# Drain and emit any subagent events (tool calls from subagents)
|
|
1405
1540
|
for subagent_event in get_subagent_debug_events():
|
|
@@ -1418,7 +1553,12 @@ async def stream_agent(request: AgentRequest):
|
|
|
1418
1553
|
else interrupt
|
|
1419
1554
|
)
|
|
1420
1555
|
|
|
1421
|
-
yield {
|
|
1556
|
+
yield {
|
|
1557
|
+
"event": "debug",
|
|
1558
|
+
"data": json.dumps(
|
|
1559
|
+
{"status": "사용자 승인 대기 중", "icon": "pause"}
|
|
1560
|
+
),
|
|
1561
|
+
}
|
|
1422
1562
|
|
|
1423
1563
|
# Process regular HITL interrupts (non-subagent)
|
|
1424
1564
|
for interrupt in interrupts:
|
|
@@ -1430,7 +1570,9 @@ async def stream_agent(request: AgentRequest):
|
|
|
1430
1570
|
|
|
1431
1571
|
# Extract action requests
|
|
1432
1572
|
action_requests = interrupt_value.get("action_requests", [])
|
|
1433
|
-
logger.info(
|
|
1573
|
+
logger.info(
|
|
1574
|
+
f"[INTERRUPT] action_requests count: {len(action_requests)}, first: {str(action_requests[0])[:200] if action_requests else 'none'}"
|
|
1575
|
+
)
|
|
1434
1576
|
normalized_actions = [
|
|
1435
1577
|
_normalize_action_request(a) for a in action_requests
|
|
1436
1578
|
]
|
|
@@ -1443,14 +1585,16 @@ async def stream_agent(request: AgentRequest):
|
|
|
1443
1585
|
for idx, action in enumerate(normalized_actions):
|
|
1444
1586
|
yield {
|
|
1445
1587
|
"event": "interrupt",
|
|
1446
|
-
"data": json.dumps(
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1588
|
+
"data": json.dumps(
|
|
1589
|
+
{
|
|
1590
|
+
"thread_id": thread_id,
|
|
1591
|
+
"action": action.get("name", "unknown"),
|
|
1592
|
+
"args": action.get("arguments", {}),
|
|
1593
|
+
"description": action.get("description", ""),
|
|
1594
|
+
"action_index": idx,
|
|
1595
|
+
"total_actions": total_actions,
|
|
1596
|
+
}
|
|
1597
|
+
),
|
|
1454
1598
|
}
|
|
1455
1599
|
|
|
1456
1600
|
# Save last signature for resume to avoid duplicate content
|
|
@@ -1541,9 +1685,14 @@ async def stream_agent(request: AgentRequest):
|
|
|
1541
1685
|
)
|
|
1542
1686
|
except asyncio.TimeoutError:
|
|
1543
1687
|
logger.error("SimpleAgent fallback timed out after 30s")
|
|
1544
|
-
yield {
|
|
1688
|
+
yield {
|
|
1689
|
+
"event": "token",
|
|
1690
|
+
"data": json.dumps(
|
|
1691
|
+
{
|
|
1545
1692
|
"content": "모델이 도구 호출을 생성하지 못했습니다. 다시 시도해주세요."
|
|
1546
|
-
}
|
|
1693
|
+
}
|
|
1694
|
+
),
|
|
1695
|
+
}
|
|
1547
1696
|
produced_output = True
|
|
1548
1697
|
fallback_response = None
|
|
1549
1698
|
except Exception as fallback_error:
|
|
@@ -1552,7 +1701,12 @@ async def stream_agent(request: AgentRequest):
|
|
|
1552
1701
|
fallback_error,
|
|
1553
1702
|
exc_info=True,
|
|
1554
1703
|
)
|
|
1555
|
-
yield {
|
|
1704
|
+
yield {
|
|
1705
|
+
"event": "token",
|
|
1706
|
+
"data": json.dumps(
|
|
1707
|
+
{"content": f"오류가 발생했습니다: {str(fallback_error)}"}
|
|
1708
|
+
),
|
|
1709
|
+
}
|
|
1556
1710
|
produced_output = True
|
|
1557
1711
|
fallback_response = None
|
|
1558
1712
|
if isinstance(fallback_response, AIMessage) and getattr(
|
|
@@ -1566,27 +1720,57 @@ async def stream_agent(request: AgentRequest):
|
|
|
1566
1720
|
|
|
1567
1721
|
if tool_name in ("jupyter_cell_tool", "jupyter_cell"):
|
|
1568
1722
|
produced_output = True
|
|
1569
|
-
yield {
|
|
1570
|
-
|
|
1723
|
+
yield {
|
|
1724
|
+
"event": "debug",
|
|
1725
|
+
"data": json.dumps(
|
|
1726
|
+
_get_tool_status_message(tool_name, tool_args)
|
|
1727
|
+
),
|
|
1728
|
+
}
|
|
1729
|
+
yield {
|
|
1730
|
+
"event": "tool_call",
|
|
1731
|
+
"data": json.dumps(
|
|
1732
|
+
{
|
|
1571
1733
|
"tool": "jupyter_cell",
|
|
1572
1734
|
"code": tool_args.get("code", ""),
|
|
1573
1735
|
"description": tool_args.get("description", ""),
|
|
1574
|
-
}
|
|
1736
|
+
}
|
|
1737
|
+
),
|
|
1738
|
+
}
|
|
1575
1739
|
elif tool_name in ("markdown_tool", "markdown"):
|
|
1576
1740
|
produced_output = True
|
|
1577
|
-
yield {
|
|
1578
|
-
|
|
1741
|
+
yield {
|
|
1742
|
+
"event": "debug",
|
|
1743
|
+
"data": json.dumps(
|
|
1744
|
+
_get_tool_status_message(tool_name, tool_args)
|
|
1745
|
+
),
|
|
1746
|
+
}
|
|
1747
|
+
yield {
|
|
1748
|
+
"event": "tool_call",
|
|
1749
|
+
"data": json.dumps(
|
|
1750
|
+
{
|
|
1579
1751
|
"tool": "markdown",
|
|
1580
1752
|
"content": tool_args.get("content", ""),
|
|
1581
|
-
}
|
|
1753
|
+
}
|
|
1754
|
+
),
|
|
1755
|
+
}
|
|
1582
1756
|
elif tool_name == "execute_command_tool":
|
|
1583
1757
|
produced_output = True
|
|
1584
|
-
yield {
|
|
1585
|
-
|
|
1758
|
+
yield {
|
|
1759
|
+
"event": "debug",
|
|
1760
|
+
"data": json.dumps(
|
|
1761
|
+
_get_tool_status_message(tool_name, tool_args)
|
|
1762
|
+
),
|
|
1763
|
+
}
|
|
1764
|
+
yield {
|
|
1765
|
+
"event": "tool_call",
|
|
1766
|
+
"data": json.dumps(
|
|
1767
|
+
{
|
|
1586
1768
|
"tool": "execute_command_tool",
|
|
1587
1769
|
"command": tool_args.get("command", ""),
|
|
1588
1770
|
"timeout": tool_args.get("timeout"),
|
|
1589
|
-
}
|
|
1771
|
+
}
|
|
1772
|
+
),
|
|
1773
|
+
}
|
|
1590
1774
|
elif tool_name == "read_file_tool":
|
|
1591
1775
|
# For file operations, generate code with the LLM
|
|
1592
1776
|
logger.info(
|
|
@@ -1618,20 +1802,32 @@ async def stream_agent(request: AgentRequest):
|
|
|
1618
1802
|
)
|
|
1619
1803
|
|
|
1620
1804
|
if not code:
|
|
1621
|
-
yield {
|
|
1805
|
+
yield {
|
|
1806
|
+
"event": "token",
|
|
1807
|
+
"data": json.dumps(
|
|
1808
|
+
{
|
|
1622
1809
|
"content": "도구 실행을 위한 코드를 생성하지 못했습니다. 다시 시도해주세요."
|
|
1623
|
-
}
|
|
1810
|
+
}
|
|
1811
|
+
),
|
|
1812
|
+
}
|
|
1624
1813
|
produced_output = True
|
|
1625
1814
|
continue
|
|
1626
1815
|
|
|
1627
|
-
yield {
|
|
1816
|
+
yield {
|
|
1817
|
+
"event": "debug",
|
|
1818
|
+
"data": json.dumps(
|
|
1819
|
+
{"status": "[변환] Jupyter Cell로 변환 중"}
|
|
1820
|
+
),
|
|
1821
|
+
}
|
|
1628
1822
|
yield {
|
|
1629
1823
|
"event": "tool_call",
|
|
1630
|
-
"data": json.dumps(
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1824
|
+
"data": json.dumps(
|
|
1825
|
+
{
|
|
1826
|
+
"tool": "jupyter_cell",
|
|
1827
|
+
"code": code,
|
|
1828
|
+
"description": f"Converted from {tool_name}",
|
|
1829
|
+
}
|
|
1830
|
+
),
|
|
1635
1831
|
}
|
|
1636
1832
|
else:
|
|
1637
1833
|
# Unknown tool - skip and show message
|
|
@@ -1640,9 +1836,11 @@ async def stream_agent(request: AgentRequest):
|
|
|
1640
1836
|
)
|
|
1641
1837
|
yield {
|
|
1642
1838
|
"event": "token",
|
|
1643
|
-
"data": json.dumps(
|
|
1644
|
-
|
|
1645
|
-
|
|
1839
|
+
"data": json.dumps(
|
|
1840
|
+
{
|
|
1841
|
+
"content": f"알 수 없는 도구 '{tool_name}'입니다. jupyter_cell_tool을 사용해주세요."
|
|
1842
|
+
}
|
|
1843
|
+
),
|
|
1646
1844
|
}
|
|
1647
1845
|
produced_output = True
|
|
1648
1846
|
elif (
|
|
@@ -1654,25 +1852,41 @@ async def stream_agent(request: AgentRequest):
|
|
|
1654
1852
|
repaired_content = _repair_summary_json_content(
|
|
1655
1853
|
fallback_response.content
|
|
1656
1854
|
)
|
|
1657
|
-
yield {
|
|
1855
|
+
yield {
|
|
1856
|
+
"event": "token",
|
|
1857
|
+
"data": json.dumps({"content": repaired_content}),
|
|
1858
|
+
}
|
|
1658
1859
|
elif fallback_response is not None and not produced_output:
|
|
1659
|
-
yield {
|
|
1860
|
+
yield {
|
|
1861
|
+
"event": "token",
|
|
1862
|
+
"data": json.dumps(
|
|
1863
|
+
{
|
|
1660
1864
|
"content": "모델이 도구 호출을 생성하지 못했습니다. 다시 시도해주세요."
|
|
1661
|
-
}
|
|
1865
|
+
}
|
|
1866
|
+
),
|
|
1867
|
+
}
|
|
1662
1868
|
produced_output = True
|
|
1663
1869
|
|
|
1664
1870
|
# Clear debug status before completion
|
|
1665
1871
|
yield {"event": "debug_clear", "data": json.dumps({})}
|
|
1666
1872
|
|
|
1667
1873
|
# No interrupt - execution completed
|
|
1668
|
-
yield {
|
|
1874
|
+
yield {
|
|
1875
|
+
"event": "complete",
|
|
1876
|
+
"data": json.dumps({"success": True, "thread_id": thread_id}),
|
|
1877
|
+
}
|
|
1669
1878
|
|
|
1670
1879
|
except Exception as e:
|
|
1671
1880
|
logger.error(f"Stream error: {e}", exc_info=True)
|
|
1672
|
-
yield {
|
|
1881
|
+
yield {
|
|
1882
|
+
"event": "error",
|
|
1883
|
+
"data": json.dumps(
|
|
1884
|
+
{
|
|
1673
1885
|
"error": str(e),
|
|
1674
1886
|
"error_type": type(e).__name__,
|
|
1675
|
-
}
|
|
1887
|
+
}
|
|
1888
|
+
),
|
|
1889
|
+
}
|
|
1676
1890
|
|
|
1677
1891
|
return EventSourceResponse(event_generator())
|
|
1678
1892
|
|
|
@@ -1726,11 +1940,16 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1726
1940
|
"Server may have restarted or session expired.",
|
|
1727
1941
|
request.threadId,
|
|
1728
1942
|
)
|
|
1729
|
-
yield {
|
|
1943
|
+
yield {
|
|
1944
|
+
"event": "error",
|
|
1945
|
+
"data": json.dumps(
|
|
1946
|
+
{
|
|
1730
1947
|
"error": "Session expired or not found",
|
|
1731
1948
|
"code": "CHECKPOINT_NOT_FOUND",
|
|
1732
1949
|
"message": "이전 세션을 찾을 수 없습니다. 서버가 재시작되었거나 세션이 만료되었습니다. 새로운 대화를 시작해주세요.",
|
|
1733
|
-
}
|
|
1950
|
+
}
|
|
1951
|
+
),
|
|
1952
|
+
}
|
|
1734
1953
|
return
|
|
1735
1954
|
|
|
1736
1955
|
checkpointer = _simple_agent_checkpointers.get(request.threadId)
|
|
@@ -1822,7 +2041,12 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1822
2041
|
)
|
|
1823
2042
|
# Track code execution for history (injected into subagent context)
|
|
1824
2043
|
tool_name = edited_action.get("name", "")
|
|
1825
|
-
if tool_name in (
|
|
2044
|
+
if tool_name in (
|
|
2045
|
+
"jupyter_cell_tool",
|
|
2046
|
+
"write_file_tool",
|
|
2047
|
+
"edit_file_tool",
|
|
2048
|
+
"multiedit_file_tool",
|
|
2049
|
+
):
|
|
1826
2050
|
track_tool_execution(tool_name, args)
|
|
1827
2051
|
langgraph_decisions.append(
|
|
1828
2052
|
{
|
|
@@ -1840,7 +2064,10 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1840
2064
|
)
|
|
1841
2065
|
|
|
1842
2066
|
# Resume execution
|
|
1843
|
-
yield {
|
|
2067
|
+
yield {
|
|
2068
|
+
"event": "debug",
|
|
2069
|
+
"data": json.dumps({"status": "실행 재개 중", "icon": "play"}),
|
|
2070
|
+
}
|
|
1844
2071
|
|
|
1845
2072
|
_simple_agent_pending_actions.pop(request.threadId, None)
|
|
1846
2073
|
|
|
@@ -1866,7 +2093,10 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1866
2093
|
)
|
|
1867
2094
|
|
|
1868
2095
|
# Status: waiting for LLM response
|
|
1869
|
-
yield {
|
|
2096
|
+
yield {
|
|
2097
|
+
"event": "debug",
|
|
2098
|
+
"data": json.dumps({"status": "LLM 응답 대기 중", "icon": "thinking"}),
|
|
2099
|
+
}
|
|
1870
2100
|
|
|
1871
2101
|
step_count = 0
|
|
1872
2102
|
|
|
@@ -1878,9 +2108,16 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1878
2108
|
):
|
|
1879
2109
|
# Check if thread was cancelled by user
|
|
1880
2110
|
if is_thread_cancelled(request.threadId):
|
|
1881
|
-
logger.info(
|
|
2111
|
+
logger.info(
|
|
2112
|
+
f"Thread {request.threadId} cancelled by user, stopping resume stream"
|
|
2113
|
+
)
|
|
1882
2114
|
clear_cancelled_thread(request.threadId)
|
|
1883
|
-
yield {
|
|
2115
|
+
yield {
|
|
2116
|
+
"event": "cancelled",
|
|
2117
|
+
"data": json.dumps(
|
|
2118
|
+
{"message": "작업이 사용자에 의해 중단되었습니다."}
|
|
2119
|
+
),
|
|
2120
|
+
}
|
|
1884
2121
|
return
|
|
1885
2122
|
|
|
1886
2123
|
step_count += 1
|
|
@@ -1982,7 +2219,10 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1982
2219
|
todos = _extract_todos(last_message.content)
|
|
1983
2220
|
if todos:
|
|
1984
2221
|
latest_todos = todos
|
|
1985
|
-
yield {
|
|
2222
|
+
yield {
|
|
2223
|
+
"event": "todos",
|
|
2224
|
+
"data": json.dumps({"todos": todos}),
|
|
2225
|
+
}
|
|
1986
2226
|
# Check if all todos are completed - auto terminate only if summary exists
|
|
1987
2227
|
all_completed = all(
|
|
1988
2228
|
t.get("status") == "completed" for t in todos
|
|
@@ -1991,16 +2231,31 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1991
2231
|
# Check if summary JSON exists in the CURRENT step's AIMessage
|
|
1992
2232
|
# (not in history, to avoid false positives from previous tasks)
|
|
1993
2233
|
summary_exists = False
|
|
1994
|
-
step_messages =
|
|
2234
|
+
step_messages = (
|
|
2235
|
+
step.get("messages", [])
|
|
2236
|
+
if isinstance(step, dict)
|
|
2237
|
+
else []
|
|
2238
|
+
)
|
|
1995
2239
|
# Only check the AIMessage that called write_todos (should be right before this ToolMessage)
|
|
1996
|
-
for recent_msg in step_messages[
|
|
2240
|
+
for recent_msg in step_messages[
|
|
2241
|
+
-3:
|
|
2242
|
+
]: # Check only the most recent few messages
|
|
1997
2243
|
if isinstance(recent_msg, AIMessage):
|
|
1998
|
-
recent_content =
|
|
2244
|
+
recent_content = (
|
|
2245
|
+
getattr(recent_msg, "content", "") or ""
|
|
2246
|
+
)
|
|
1999
2247
|
if isinstance(recent_content, list):
|
|
2000
|
-
recent_content = " ".join(
|
|
2001
|
-
|
|
2248
|
+
recent_content = " ".join(
|
|
2249
|
+
str(p) for p in recent_content
|
|
2250
|
+
)
|
|
2251
|
+
if (
|
|
2252
|
+
'"summary"' in recent_content
|
|
2253
|
+
and '"next_items"' in recent_content
|
|
2254
|
+
):
|
|
2002
2255
|
summary_exists = True
|
|
2003
|
-
logger.info(
|
|
2256
|
+
logger.info(
|
|
2257
|
+
"Resume: Found summary in current AIMessage content"
|
|
2258
|
+
)
|
|
2004
2259
|
break
|
|
2005
2260
|
|
|
2006
2261
|
if summary_exists:
|
|
@@ -2012,23 +2267,53 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2012
2267
|
# Find and emit the AIMessage content with summary
|
|
2013
2268
|
for recent_msg in step_messages[-3:]:
|
|
2014
2269
|
if isinstance(recent_msg, AIMessage):
|
|
2015
|
-
step_content =
|
|
2270
|
+
step_content = (
|
|
2271
|
+
getattr(recent_msg, "content", "")
|
|
2272
|
+
or ""
|
|
2273
|
+
)
|
|
2016
2274
|
if isinstance(step_content, list):
|
|
2017
|
-
step_content = " ".join(
|
|
2018
|
-
|
|
2275
|
+
step_content = " ".join(
|
|
2276
|
+
str(p) for p in step_content
|
|
2277
|
+
)
|
|
2278
|
+
if (
|
|
2279
|
+
'"summary"' in step_content
|
|
2280
|
+
and '"next_items"' in step_content
|
|
2281
|
+
):
|
|
2019
2282
|
content_hash = hash(step_content)
|
|
2020
|
-
if
|
|
2021
|
-
|
|
2022
|
-
|
|
2283
|
+
if (
|
|
2284
|
+
content_hash
|
|
2285
|
+
not in emitted_contents
|
|
2286
|
+
):
|
|
2287
|
+
emitted_contents.add(
|
|
2288
|
+
content_hash
|
|
2289
|
+
)
|
|
2290
|
+
repaired_content = _repair_summary_json_content(
|
|
2291
|
+
step_content
|
|
2292
|
+
)
|
|
2023
2293
|
logger.info(
|
|
2024
2294
|
"Resume step auto-terminate: EMITTING summary content (len=%d): %s",
|
|
2025
2295
|
len(repaired_content),
|
|
2026
2296
|
repaired_content[:100],
|
|
2027
2297
|
)
|
|
2028
|
-
yield {
|
|
2298
|
+
yield {
|
|
2299
|
+
"event": "token",
|
|
2300
|
+
"data": json.dumps(
|
|
2301
|
+
{
|
|
2302
|
+
"content": repaired_content
|
|
2303
|
+
}
|
|
2304
|
+
),
|
|
2305
|
+
}
|
|
2029
2306
|
break
|
|
2030
|
-
yield {
|
|
2031
|
-
|
|
2307
|
+
yield {
|
|
2308
|
+
"event": "debug_clear",
|
|
2309
|
+
"data": json.dumps({}),
|
|
2310
|
+
}
|
|
2311
|
+
yield {
|
|
2312
|
+
"event": "done",
|
|
2313
|
+
"data": json.dumps(
|
|
2314
|
+
{"reason": "all_todos_completed"}
|
|
2315
|
+
),
|
|
2316
|
+
}
|
|
2032
2317
|
return # Exit the generator
|
|
2033
2318
|
else:
|
|
2034
2319
|
logger.warning(
|
|
@@ -2120,7 +2405,12 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2120
2405
|
len(repaired_content),
|
|
2121
2406
|
log_preview,
|
|
2122
2407
|
)
|
|
2123
|
-
yield {
|
|
2408
|
+
yield {
|
|
2409
|
+
"event": "token",
|
|
2410
|
+
"data": json.dumps(
|
|
2411
|
+
{"content": repaired_content}
|
|
2412
|
+
),
|
|
2413
|
+
}
|
|
2124
2414
|
|
|
2125
2415
|
if (
|
|
2126
2416
|
hasattr(last_message, "tool_calls")
|
|
@@ -2147,7 +2437,10 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2147
2437
|
todos = _emit_todos_from_tool_calls(new_tool_calls)
|
|
2148
2438
|
if todos:
|
|
2149
2439
|
latest_todos = todos
|
|
2150
|
-
yield {
|
|
2440
|
+
yield {
|
|
2441
|
+
"event": "todos",
|
|
2442
|
+
"data": json.dumps({"todos": todos}),
|
|
2443
|
+
}
|
|
2151
2444
|
# Check if all todos are completed - terminate early
|
|
2152
2445
|
all_completed = all(
|
|
2153
2446
|
t.get("status") == "completed" for t in todos
|
|
@@ -2189,7 +2482,9 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2189
2482
|
"## 다음 단계",
|
|
2190
2483
|
]
|
|
2191
2484
|
)
|
|
2192
|
-
has_summary =
|
|
2485
|
+
has_summary = (
|
|
2486
|
+
has_summary_json or has_markdown_summary
|
|
2487
|
+
)
|
|
2193
2488
|
|
|
2194
2489
|
# Only check current AIMessage for summary (not history, to avoid false positives)
|
|
2195
2490
|
if not has_summary:
|
|
@@ -2204,19 +2499,40 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2204
2499
|
)
|
|
2205
2500
|
# IMPORTANT: Emit the summary content BEFORE terminating
|
|
2206
2501
|
# so the UI can display the summary JSON
|
|
2207
|
-
if msg_content and isinstance(
|
|
2502
|
+
if msg_content and isinstance(
|
|
2503
|
+
msg_content, str
|
|
2504
|
+
):
|
|
2208
2505
|
content_hash = hash(msg_content)
|
|
2209
2506
|
if content_hash not in emitted_contents:
|
|
2210
2507
|
emitted_contents.add(content_hash)
|
|
2211
|
-
repaired_content =
|
|
2508
|
+
repaired_content = (
|
|
2509
|
+
_repair_summary_json_content(
|
|
2510
|
+
msg_content
|
|
2511
|
+
)
|
|
2512
|
+
)
|
|
2212
2513
|
logger.info(
|
|
2213
2514
|
"Resume auto-terminate: EMITTING summary content (len=%d): %s",
|
|
2214
2515
|
len(repaired_content),
|
|
2215
2516
|
repaired_content[:100],
|
|
2216
2517
|
)
|
|
2217
|
-
yield {
|
|
2218
|
-
|
|
2219
|
-
|
|
2518
|
+
yield {
|
|
2519
|
+
"event": "token",
|
|
2520
|
+
"data": json.dumps(
|
|
2521
|
+
{
|
|
2522
|
+
"content": repaired_content
|
|
2523
|
+
}
|
|
2524
|
+
),
|
|
2525
|
+
}
|
|
2526
|
+
yield {
|
|
2527
|
+
"event": "debug_clear",
|
|
2528
|
+
"data": json.dumps({}),
|
|
2529
|
+
}
|
|
2530
|
+
yield {
|
|
2531
|
+
"event": "done",
|
|
2532
|
+
"data": json.dumps(
|
|
2533
|
+
{"reason": "all_todos_completed"}
|
|
2534
|
+
),
|
|
2535
|
+
}
|
|
2220
2536
|
return # Exit before executing more tool calls
|
|
2221
2537
|
|
|
2222
2538
|
# Process tool calls
|
|
@@ -2236,40 +2552,61 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2236
2552
|
tool_name, tool_args
|
|
2237
2553
|
)
|
|
2238
2554
|
|
|
2239
|
-
yield {
|
|
2555
|
+
yield {
|
|
2556
|
+
"event": "debug",
|
|
2557
|
+
"data": json.dumps(status_msg),
|
|
2558
|
+
}
|
|
2240
2559
|
|
|
2241
2560
|
if tool_name in (
|
|
2242
2561
|
"jupyter_cell_tool",
|
|
2243
2562
|
"jupyter_cell",
|
|
2244
2563
|
):
|
|
2245
|
-
yield {
|
|
2564
|
+
yield {
|
|
2565
|
+
"event": "tool_call",
|
|
2566
|
+
"data": json.dumps(
|
|
2567
|
+
{
|
|
2246
2568
|
"tool": "jupyter_cell",
|
|
2247
2569
|
"code": tool_args.get("code", ""),
|
|
2248
2570
|
"description": tool_args.get(
|
|
2249
2571
|
"description", ""
|
|
2250
2572
|
),
|
|
2251
|
-
}
|
|
2573
|
+
}
|
|
2574
|
+
),
|
|
2575
|
+
}
|
|
2252
2576
|
elif tool_name in ("markdown_tool", "markdown"):
|
|
2253
|
-
yield {
|
|
2577
|
+
yield {
|
|
2578
|
+
"event": "tool_call",
|
|
2579
|
+
"data": json.dumps(
|
|
2580
|
+
{
|
|
2254
2581
|
"tool": "markdown",
|
|
2255
2582
|
"content": tool_args.get(
|
|
2256
2583
|
"content", ""
|
|
2257
2584
|
),
|
|
2258
|
-
}
|
|
2585
|
+
}
|
|
2586
|
+
),
|
|
2587
|
+
}
|
|
2259
2588
|
elif tool_name == "execute_command_tool":
|
|
2260
|
-
yield {
|
|
2589
|
+
yield {
|
|
2590
|
+
"event": "tool_call",
|
|
2591
|
+
"data": json.dumps(
|
|
2592
|
+
{
|
|
2261
2593
|
"tool": "execute_command_tool",
|
|
2262
2594
|
"command": tool_args.get(
|
|
2263
2595
|
"command", ""
|
|
2264
2596
|
),
|
|
2265
2597
|
"timeout": tool_args.get("timeout"),
|
|
2266
|
-
}
|
|
2598
|
+
}
|
|
2599
|
+
),
|
|
2600
|
+
}
|
|
2267
2601
|
elif tool_name in (
|
|
2268
2602
|
"search_notebook_cells_tool",
|
|
2269
2603
|
"search_notebook_cells",
|
|
2270
2604
|
):
|
|
2271
2605
|
# Search notebook cells - emit tool_call for client-side execution
|
|
2272
|
-
yield {
|
|
2606
|
+
yield {
|
|
2607
|
+
"event": "tool_call",
|
|
2608
|
+
"data": json.dumps(
|
|
2609
|
+
{
|
|
2273
2610
|
"tool": "search_notebook_cells",
|
|
2274
2611
|
"pattern": tool_args.get(
|
|
2275
2612
|
"pattern", ""
|
|
@@ -2286,7 +2623,9 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2286
2623
|
"case_sensitive": tool_args.get(
|
|
2287
2624
|
"case_sensitive", False
|
|
2288
2625
|
),
|
|
2289
|
-
}
|
|
2626
|
+
}
|
|
2627
|
+
),
|
|
2628
|
+
}
|
|
2290
2629
|
|
|
2291
2630
|
# Drain and emit any subagent events (tool calls from subagents)
|
|
2292
2631
|
for subagent_event in get_subagent_debug_events():
|
|
@@ -2297,7 +2636,12 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2297
2636
|
if isinstance(step, dict) and "__interrupt__" in step:
|
|
2298
2637
|
interrupts = step["__interrupt__"]
|
|
2299
2638
|
|
|
2300
|
-
yield {
|
|
2639
|
+
yield {
|
|
2640
|
+
"event": "debug",
|
|
2641
|
+
"data": json.dumps(
|
|
2642
|
+
{"status": "사용자 승인 대기 중", "icon": "pause"}
|
|
2643
|
+
),
|
|
2644
|
+
}
|
|
2301
2645
|
|
|
2302
2646
|
for interrupt in interrupts:
|
|
2303
2647
|
interrupt_value = (
|
|
@@ -2306,7 +2650,9 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2306
2650
|
else interrupt
|
|
2307
2651
|
)
|
|
2308
2652
|
action_requests = interrupt_value.get("action_requests", [])
|
|
2309
|
-
logger.info(
|
|
2653
|
+
logger.info(
|
|
2654
|
+
f"[RESUME INTERRUPT] action_requests count: {len(action_requests)}, first: {str(action_requests[0])[:200] if action_requests else 'none'}"
|
|
2655
|
+
)
|
|
2310
2656
|
normalized_actions = [
|
|
2311
2657
|
_normalize_action_request(a) for a in action_requests
|
|
2312
2658
|
]
|
|
@@ -2319,14 +2665,16 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2319
2665
|
for idx, action in enumerate(normalized_actions):
|
|
2320
2666
|
yield {
|
|
2321
2667
|
"event": "interrupt",
|
|
2322
|
-
"data": json.dumps(
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2668
|
+
"data": json.dumps(
|
|
2669
|
+
{
|
|
2670
|
+
"thread_id": request.threadId,
|
|
2671
|
+
"action": action.get("name", "unknown"),
|
|
2672
|
+
"args": action.get("arguments", {}),
|
|
2673
|
+
"description": action.get("description", ""),
|
|
2674
|
+
"action_index": idx,
|
|
2675
|
+
"total_actions": total_actions,
|
|
2676
|
+
}
|
|
2677
|
+
),
|
|
2330
2678
|
}
|
|
2331
2679
|
|
|
2332
2680
|
# Save last signature for next resume to avoid duplicate content
|
|
@@ -2359,7 +2707,10 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2359
2707
|
last_signature,
|
|
2360
2708
|
latest_todos,
|
|
2361
2709
|
)
|
|
2362
|
-
yield {
|
|
2710
|
+
yield {
|
|
2711
|
+
"event": "complete",
|
|
2712
|
+
"data": json.dumps({"success": True, "thread_id": request.threadId}),
|
|
2713
|
+
}
|
|
2363
2714
|
|
|
2364
2715
|
except Exception as e:
|
|
2365
2716
|
error_msg = str(e)
|
|
@@ -2370,17 +2721,27 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2370
2721
|
logger.warning(
|
|
2371
2722
|
"Detected 'contents is not specified' error - likely session state loss"
|
|
2372
2723
|
)
|
|
2373
|
-
yield {
|
|
2724
|
+
yield {
|
|
2725
|
+
"event": "error",
|
|
2726
|
+
"data": json.dumps(
|
|
2727
|
+
{
|
|
2374
2728
|
"error": "Session state lost",
|
|
2375
2729
|
"code": "CONTENTS_NOT_SPECIFIED",
|
|
2376
2730
|
"error_type": type(e).__name__,
|
|
2377
2731
|
"message": "세션 상태가 손실되었습니다. 서버가 재시작되었거나 세션이 만료되었습니다. 새로운 대화를 시작해주세요.",
|
|
2378
|
-
}
|
|
2732
|
+
}
|
|
2733
|
+
),
|
|
2734
|
+
}
|
|
2379
2735
|
else:
|
|
2380
|
-
yield {
|
|
2736
|
+
yield {
|
|
2737
|
+
"event": "error",
|
|
2738
|
+
"data": json.dumps(
|
|
2739
|
+
{
|
|
2381
2740
|
"error": error_msg,
|
|
2382
2741
|
"error_type": type(e).__name__,
|
|
2383
|
-
}
|
|
2742
|
+
}
|
|
2743
|
+
),
|
|
2744
|
+
}
|
|
2384
2745
|
|
|
2385
2746
|
return EventSourceResponse(event_generator())
|
|
2386
2747
|
|
|
@@ -2437,6 +2798,7 @@ async def health_check() -> Dict[str, Any]:
|
|
|
2437
2798
|
|
|
2438
2799
|
class CancelRequest(BaseModel):
|
|
2439
2800
|
"""Request to cancel a running agent thread"""
|
|
2801
|
+
|
|
2440
2802
|
thread_id: str
|
|
2441
2803
|
|
|
2442
2804
|
|
|
@@ -2463,3 +2825,68 @@ async def clear_agent_cache() -> Dict[str, Any]:
|
|
|
2463
2825
|
"cleared": count,
|
|
2464
2826
|
"message": f"Cleared {count} cached agent instances",
|
|
2465
2827
|
}
|
|
2828
|
+
|
|
2829
|
+
|
|
2830
|
+
class ResetRequest(BaseModel):
|
|
2831
|
+
"""Request to reset a thread (clear session and recreate agent)"""
|
|
2832
|
+
|
|
2833
|
+
thread_id: str
|
|
2834
|
+
|
|
2835
|
+
|
|
2836
|
+
@router.post("/reset")
|
|
2837
|
+
async def reset_agent_thread(request: ResetRequest) -> Dict[str, Any]:
|
|
2838
|
+
"""
|
|
2839
|
+
Reset an agent thread by clearing all session state.
|
|
2840
|
+
|
|
2841
|
+
This will:
|
|
2842
|
+
- Clear the checkpointer (conversation history)
|
|
2843
|
+
- Clear pending actions
|
|
2844
|
+
- Clear emitted contents
|
|
2845
|
+
- Clear last signatures
|
|
2846
|
+
- Remove from cancelled threads set
|
|
2847
|
+
|
|
2848
|
+
The agent instance itself is not cleared (it's shared across threads),
|
|
2849
|
+
but the thread state is completely reset.
|
|
2850
|
+
"""
|
|
2851
|
+
thread_id = request.thread_id
|
|
2852
|
+
|
|
2853
|
+
# Track what was cleared
|
|
2854
|
+
cleared = []
|
|
2855
|
+
|
|
2856
|
+
# Clear checkpointer (conversation history)
|
|
2857
|
+
if thread_id in _simple_agent_checkpointers:
|
|
2858
|
+
del _simple_agent_checkpointers[thread_id]
|
|
2859
|
+
cleared.append("checkpointer")
|
|
2860
|
+
|
|
2861
|
+
# Clear pending actions
|
|
2862
|
+
if thread_id in _simple_agent_pending_actions:
|
|
2863
|
+
del _simple_agent_pending_actions[thread_id]
|
|
2864
|
+
cleared.append("pending_actions")
|
|
2865
|
+
|
|
2866
|
+
# Clear last signatures
|
|
2867
|
+
if thread_id in _simple_agent_last_signatures:
|
|
2868
|
+
del _simple_agent_last_signatures[thread_id]
|
|
2869
|
+
cleared.append("last_signatures")
|
|
2870
|
+
|
|
2871
|
+
# Clear emitted contents
|
|
2872
|
+
if thread_id in _simple_agent_emitted_contents:
|
|
2873
|
+
del _simple_agent_emitted_contents[thread_id]
|
|
2874
|
+
cleared.append("emitted_contents")
|
|
2875
|
+
|
|
2876
|
+
# Remove from cancelled threads
|
|
2877
|
+
if thread_id in _cancelled_threads:
|
|
2878
|
+
_cancelled_threads.discard(thread_id)
|
|
2879
|
+
cleared.append("cancelled_flag")
|
|
2880
|
+
|
|
2881
|
+
logger.info(
|
|
2882
|
+
"Reset thread %s: cleared %s",
|
|
2883
|
+
thread_id,
|
|
2884
|
+
", ".join(cleared) if cleared else "nothing (thread not found)",
|
|
2885
|
+
)
|
|
2886
|
+
|
|
2887
|
+
return {
|
|
2888
|
+
"status": "ok",
|
|
2889
|
+
"thread_id": thread_id,
|
|
2890
|
+
"cleared": cleared,
|
|
2891
|
+
"message": f"Thread {thread_id} has been reset" if cleared else f"Thread {thread_id} had no state to clear",
|
|
2892
|
+
}
|