hdsp-jupyter-extension 2.0.23__py3-none-any.whl → 2.0.26__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/langchain/agent_factory.py +14 -14
- agent_server/langchain/agent_prompts/planner_prompt.py +13 -19
- agent_server/langchain/custom_middleware.py +73 -17
- agent_server/langchain/models/gpt_oss_chat.py +26 -13
- agent_server/langchain/prompts.py +11 -8
- agent_server/langchain/tools/jupyter_tools.py +43 -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 +806 -203
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.26.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.26.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.26.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.b5e4416b4e07ec087aad.js +245 -121
- hdsp_jupyter_extension-2.0.26.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.26.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.67505497667f9c0a763d.js +583 -39
- hdsp_jupyter_extension-2.0.26.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.26.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.0fe2dcbbd176ee0efceb.js +3 -3
- hdsp_jupyter_extension-2.0.23.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.f0127d8744730f2092c1.js.map → hdsp_jupyter_extension-2.0.26.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.0fe2dcbbd176ee0efceb.js.map +1 -1
- {hdsp_jupyter_extension-2.0.23.dist-info → hdsp_jupyter_extension-2.0.26.dist-info}/METADATA +1 -1
- {hdsp_jupyter_extension-2.0.23.dist-info → hdsp_jupyter_extension-2.0.26.dist-info}/RECORD +56 -50
- 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.0fe2dcbbd176ee0efceb.js} +3 -3
- jupyter_ext/labextension/static/{remoteEntry.f0127d8744730f2092c1.js.map → remoteEntry.0fe2dcbbd176ee0efceb.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.26.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.26.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.26.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.26.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.26.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.26.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.26.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.23.data → hdsp_jupyter_extension-2.0.26.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.26.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.26.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.26.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.26.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.26.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.26.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.26.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.26.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.26.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.26.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.26.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.26.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.23.dist-info → hdsp_jupyter_extension-2.0.26.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:
|
|
@@ -733,20 +746,31 @@ async def stream_agent(request: AgentRequest):
|
|
|
733
746
|
logger.info("Agent mode: %s", agent_mode)
|
|
734
747
|
|
|
735
748
|
# Get agent prompts (for multi-agent mode)
|
|
736
|
-
# ROOT CAUSE FOUND: Frontend sends both systemPrompt AND agentPrompts
|
|
737
|
-
# which creates different cache keys and causes MALFORMED_FUNCTION_CALL
|
|
738
|
-
# FIX: Ignore all custom prompts for multi-agent mode until frontend is fixed
|
|
739
749
|
agent_prompts = None
|
|
740
750
|
if agent_mode == "multi":
|
|
741
|
-
#
|
|
751
|
+
# Multi-agent mode: Use agentPrompts for per-agent customization
|
|
752
|
+
# systemPrompt is for single-agent mode only (DEFAULT_SYSTEM_PROMPT)
|
|
742
753
|
if request.llmConfig and request.llmConfig.agent_prompts:
|
|
743
|
-
|
|
744
|
-
"
|
|
754
|
+
agent_prompts = {
|
|
755
|
+
"planner": request.llmConfig.agent_prompts.planner,
|
|
756
|
+
"python_developer": (
|
|
757
|
+
request.llmConfig.agent_prompts.python_developer
|
|
758
|
+
),
|
|
759
|
+
"researcher": request.llmConfig.agent_prompts.researcher,
|
|
760
|
+
"athena_query": request.llmConfig.agent_prompts.athena_query,
|
|
761
|
+
}
|
|
762
|
+
agent_prompts = {k: v for k, v in agent_prompts.items() if v}
|
|
763
|
+
logger.info(
|
|
764
|
+
"Multi-agent mode: Using agentPrompts (%s)",
|
|
765
|
+
list(agent_prompts.keys()),
|
|
745
766
|
)
|
|
746
|
-
#
|
|
767
|
+
# In multi-agent mode, DON'T use systemPrompt as override
|
|
768
|
+
# (systemPrompt = single-agent prompt, not planner prompt)
|
|
769
|
+
# Use agentPrompts.planner instead (handled by agent_factory)
|
|
747
770
|
if system_prompt_override:
|
|
748
|
-
logger.
|
|
749
|
-
"Multi-agent mode: Ignoring systemPrompt override (len=%d)"
|
|
771
|
+
logger.info(
|
|
772
|
+
"Multi-agent mode: Ignoring systemPrompt override (len=%d) - "
|
|
773
|
+
"use agentPrompts.planner instead",
|
|
750
774
|
len(system_prompt_override),
|
|
751
775
|
)
|
|
752
776
|
system_prompt_override = None
|
|
@@ -856,7 +880,11 @@ async def stream_agent(request: AgentRequest):
|
|
|
856
880
|
}
|
|
857
881
|
# Build previous todos context for LLM
|
|
858
882
|
if existing_todos:
|
|
859
|
-
completed_items = [
|
|
883
|
+
completed_items = [
|
|
884
|
+
t.get("content", "")
|
|
885
|
+
for t in existing_todos
|
|
886
|
+
if t.get("status") == "completed"
|
|
887
|
+
]
|
|
860
888
|
if completed_items:
|
|
861
889
|
items_summary = ", ".join(completed_items[:5])
|
|
862
890
|
if len(completed_items) > 5:
|
|
@@ -865,7 +893,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
865
893
|
f"[SYSTEM] 이전 todo list가 완료 혹은 취소되었습니다. 완료된 작업: {items_summary}. "
|
|
866
894
|
f"새 작업을 시작합니다. 이전 todo list에 신규 작업을 append 하지 말고 새로운 todo list를 생성하세요."
|
|
867
895
|
)
|
|
868
|
-
logger.info(
|
|
896
|
+
logger.info(
|
|
897
|
+
"Injecting previous todos context: %s",
|
|
898
|
+
previous_todos_context[:100],
|
|
899
|
+
)
|
|
869
900
|
except Exception as e:
|
|
870
901
|
logger.warning("Could not reset todos in agent state: %s", e)
|
|
871
902
|
|
|
@@ -883,7 +914,6 @@ async def stream_agent(request: AgentRequest):
|
|
|
883
914
|
produced_output = False
|
|
884
915
|
last_finish_reason = None
|
|
885
916
|
last_signature = None
|
|
886
|
-
latest_todos: Optional[List[Dict[str, Any]]] = None
|
|
887
917
|
# Initialize emitted contents set for this thread (clear any stale data)
|
|
888
918
|
emitted_contents: set = set()
|
|
889
919
|
_simple_agent_emitted_contents[thread_id] = emitted_contents
|
|
@@ -895,7 +925,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
895
925
|
|
|
896
926
|
# Initial status: waiting for LLM
|
|
897
927
|
logger.info("SSE: Sending initial debug status 'LLM 응답 대기 중'")
|
|
898
|
-
yield {
|
|
928
|
+
yield {
|
|
929
|
+
"event": "debug",
|
|
930
|
+
"data": json.dumps({"status": "LLM 응답 대기 중", "icon": "thinking"}),
|
|
931
|
+
}
|
|
899
932
|
|
|
900
933
|
# Main streaming loop
|
|
901
934
|
async for step in _async_stream_wrapper(
|
|
@@ -903,9 +936,16 @@ async def stream_agent(request: AgentRequest):
|
|
|
903
936
|
):
|
|
904
937
|
# Check if thread was cancelled by user
|
|
905
938
|
if is_thread_cancelled(thread_id):
|
|
906
|
-
logger.info(
|
|
939
|
+
logger.info(
|
|
940
|
+
f"Thread {thread_id} cancelled by user, stopping stream"
|
|
941
|
+
)
|
|
907
942
|
clear_cancelled_thread(thread_id)
|
|
908
|
-
yield {
|
|
943
|
+
yield {
|
|
944
|
+
"event": "cancelled",
|
|
945
|
+
"data": json.dumps(
|
|
946
|
+
{"message": "작업이 사용자에 의해 중단되었습니다."}
|
|
947
|
+
),
|
|
948
|
+
}
|
|
909
949
|
return
|
|
910
950
|
|
|
911
951
|
if isinstance(step, dict):
|
|
@@ -915,7 +955,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
915
955
|
# DEBUG: Check for __interrupt__ in every step
|
|
916
956
|
if "__interrupt__" in step:
|
|
917
957
|
logger.info("[DEBUG-INTERRUPT] Found __interrupt__ in step!")
|
|
918
|
-
logger.info(
|
|
958
|
+
logger.info(
|
|
959
|
+
"[DEBUG-INTERRUPT] interrupt value: %s",
|
|
960
|
+
str(step["__interrupt__"])[:500],
|
|
961
|
+
)
|
|
919
962
|
|
|
920
963
|
# IMPORTANT: Process todos and messages BEFORE checking for interrupt
|
|
921
964
|
# This ensures todos/debug events are emitted even in interrupt steps
|
|
@@ -924,12 +967,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
924
967
|
if isinstance(step, dict) and "todos" in step:
|
|
925
968
|
todos = step["todos"]
|
|
926
969
|
if todos:
|
|
927
|
-
latest_todos = todos
|
|
928
970
|
yield {"event": "todos", "data": json.dumps({"todos": todos})}
|
|
929
971
|
elif isinstance(step, dict):
|
|
930
972
|
todos = _extract_todos(step)
|
|
931
973
|
if todos:
|
|
932
|
-
latest_todos = todos
|
|
933
974
|
yield {"event": "todos", "data": json.dumps({"todos": todos})}
|
|
934
975
|
|
|
935
976
|
# Process messages (no continue statements to ensure interrupt check always runs)
|
|
@@ -979,8 +1020,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
979
1020
|
)
|
|
980
1021
|
todos = _extract_todos(last_message.content)
|
|
981
1022
|
if todos:
|
|
982
|
-
|
|
983
|
-
|
|
1023
|
+
yield {
|
|
1024
|
+
"event": "todos",
|
|
1025
|
+
"data": json.dumps({"todos": todos}),
|
|
1026
|
+
}
|
|
984
1027
|
# Check if all todos are completed - auto terminate only if summary exists
|
|
985
1028
|
all_completed = all(
|
|
986
1029
|
t.get("status") == "completed" for t in todos
|
|
@@ -989,16 +1032,31 @@ async def stream_agent(request: AgentRequest):
|
|
|
989
1032
|
# Check if summary JSON exists in the CURRENT step's AIMessage
|
|
990
1033
|
# (not in history, to avoid false positives from previous tasks)
|
|
991
1034
|
summary_exists = False
|
|
992
|
-
step_messages =
|
|
1035
|
+
step_messages = (
|
|
1036
|
+
step.get("messages", [])
|
|
1037
|
+
if isinstance(step, dict)
|
|
1038
|
+
else []
|
|
1039
|
+
)
|
|
993
1040
|
# Only check the AIMessage that called write_todos (should be right before this ToolMessage)
|
|
994
|
-
for recent_msg in step_messages[
|
|
1041
|
+
for recent_msg in step_messages[
|
|
1042
|
+
-3:
|
|
1043
|
+
]: # Check only the most recent few messages
|
|
995
1044
|
if isinstance(recent_msg, AIMessage):
|
|
996
|
-
recent_content =
|
|
1045
|
+
recent_content = (
|
|
1046
|
+
getattr(recent_msg, "content", "") or ""
|
|
1047
|
+
)
|
|
997
1048
|
if isinstance(recent_content, list):
|
|
998
|
-
recent_content = " ".join(
|
|
999
|
-
|
|
1049
|
+
recent_content = " ".join(
|
|
1050
|
+
str(p) for p in recent_content
|
|
1051
|
+
)
|
|
1052
|
+
if (
|
|
1053
|
+
'"summary"' in recent_content
|
|
1054
|
+
and '"next_items"' in recent_content
|
|
1055
|
+
):
|
|
1000
1056
|
summary_exists = True
|
|
1001
|
-
logger.info(
|
|
1057
|
+
logger.info(
|
|
1058
|
+
"Found summary in current AIMessage content"
|
|
1059
|
+
)
|
|
1002
1060
|
break
|
|
1003
1061
|
|
|
1004
1062
|
if summary_exists:
|
|
@@ -1010,24 +1068,54 @@ async def stream_agent(request: AgentRequest):
|
|
|
1010
1068
|
# Find and emit the AIMessage content with summary
|
|
1011
1069
|
for recent_msg in step_messages[-3:]:
|
|
1012
1070
|
if isinstance(recent_msg, AIMessage):
|
|
1013
|
-
step_content =
|
|
1071
|
+
step_content = (
|
|
1072
|
+
getattr(recent_msg, "content", "")
|
|
1073
|
+
or ""
|
|
1074
|
+
)
|
|
1014
1075
|
if isinstance(step_content, list):
|
|
1015
|
-
step_content = " ".join(
|
|
1016
|
-
|
|
1076
|
+
step_content = " ".join(
|
|
1077
|
+
str(p) for p in step_content
|
|
1078
|
+
)
|
|
1079
|
+
if (
|
|
1080
|
+
'"summary"' in step_content
|
|
1081
|
+
and '"next_items"' in step_content
|
|
1082
|
+
):
|
|
1017
1083
|
content_hash = hash(step_content)
|
|
1018
|
-
if
|
|
1019
|
-
|
|
1020
|
-
|
|
1084
|
+
if (
|
|
1085
|
+
content_hash
|
|
1086
|
+
not in emitted_contents
|
|
1087
|
+
):
|
|
1088
|
+
emitted_contents.add(
|
|
1089
|
+
content_hash
|
|
1090
|
+
)
|
|
1091
|
+
repaired_content = _repair_summary_json_content(
|
|
1092
|
+
step_content
|
|
1093
|
+
)
|
|
1021
1094
|
logger.info(
|
|
1022
1095
|
"Step auto-terminate: EMITTING summary content (len=%d): %s",
|
|
1023
1096
|
len(repaired_content),
|
|
1024
1097
|
repaired_content[:100],
|
|
1025
1098
|
)
|
|
1026
1099
|
produced_output = True
|
|
1027
|
-
yield {
|
|
1100
|
+
yield {
|
|
1101
|
+
"event": "token",
|
|
1102
|
+
"data": json.dumps(
|
|
1103
|
+
{
|
|
1104
|
+
"content": repaired_content
|
|
1105
|
+
}
|
|
1106
|
+
),
|
|
1107
|
+
}
|
|
1028
1108
|
break
|
|
1029
|
-
yield {
|
|
1030
|
-
|
|
1109
|
+
yield {
|
|
1110
|
+
"event": "debug_clear",
|
|
1111
|
+
"data": json.dumps({}),
|
|
1112
|
+
}
|
|
1113
|
+
yield {
|
|
1114
|
+
"event": "done",
|
|
1115
|
+
"data": json.dumps(
|
|
1116
|
+
{"reason": "all_todos_completed"}
|
|
1117
|
+
),
|
|
1118
|
+
}
|
|
1031
1119
|
return # Exit the generator
|
|
1032
1120
|
else:
|
|
1033
1121
|
logger.warning(
|
|
@@ -1056,49 +1144,37 @@ async def stream_agent(request: AgentRequest):
|
|
|
1056
1144
|
|
|
1057
1145
|
# Handle AIMessage
|
|
1058
1146
|
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)
|
|
1147
|
+
# LLM Response - structured JSON format
|
|
1148
|
+
print(LOG_RESPONSE_START, flush=True)
|
|
1149
|
+
response_data = {
|
|
1150
|
+
"type": "AIMessage",
|
|
1151
|
+
"content": last_message.content or "",
|
|
1152
|
+
"tool_calls": last_message.tool_calls
|
|
1071
1153
|
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",
|
|
1154
|
+
else [],
|
|
1155
|
+
"additional_kwargs": getattr(
|
|
1156
|
+
last_message, "additional_kwargs", {}
|
|
1157
|
+
)
|
|
1158
|
+
or {},
|
|
1159
|
+
"response_metadata": getattr(
|
|
1160
|
+
last_message, "response_metadata", {}
|
|
1161
|
+
)
|
|
1162
|
+
or {},
|
|
1163
|
+
"usage_metadata": getattr(
|
|
1164
|
+
last_message, "usage_metadata", {}
|
|
1165
|
+
)
|
|
1166
|
+
or {},
|
|
1167
|
+
}
|
|
1168
|
+
print(
|
|
1092
1169
|
json.dumps(
|
|
1093
|
-
|
|
1170
|
+
response_data,
|
|
1171
|
+
indent=2,
|
|
1094
1172
|
ensure_ascii=False,
|
|
1173
|
+
default=str,
|
|
1095
1174
|
),
|
|
1175
|
+
flush=True,
|
|
1096
1176
|
)
|
|
1097
|
-
|
|
1098
|
-
print("=" * 96, flush=True)
|
|
1099
|
-
print(" ✅ LLM RESPONSE END", flush=True)
|
|
1100
|
-
print("=" * 96, flush=True)
|
|
1101
|
-
print("🔵" * 48 + "\n", flush=True)
|
|
1177
|
+
print(LOG_RESPONSE_END, flush=True)
|
|
1102
1178
|
last_finish_reason = (
|
|
1103
1179
|
getattr(last_message, "response_metadata", {}) or {}
|
|
1104
1180
|
).get("finish_reason")
|
|
@@ -1157,10 +1233,22 @@ async def stream_agent(request: AgentRequest):
|
|
|
1157
1233
|
logger.warning(
|
|
1158
1234
|
"MALFORMED_FUNCTION_CALL with empty response - sending error to client"
|
|
1159
1235
|
)
|
|
1160
|
-
yield {
|
|
1236
|
+
yield {
|
|
1237
|
+
"event": "token",
|
|
1238
|
+
"data": json.dumps(
|
|
1239
|
+
{
|
|
1161
1240
|
"content": "\n\n[경고] LLM이 잘못된 응답을 반환했습니다. 다시 시도해주세요.\n"
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1241
|
+
}
|
|
1242
|
+
),
|
|
1243
|
+
}
|
|
1244
|
+
yield {
|
|
1245
|
+
"event": "debug",
|
|
1246
|
+
"data": json.dumps(
|
|
1247
|
+
{
|
|
1248
|
+
"status": "[경고] MALFORMED_FUNCTION_CALL 에러"
|
|
1249
|
+
}
|
|
1250
|
+
),
|
|
1251
|
+
}
|
|
1164
1252
|
produced_output = True
|
|
1165
1253
|
# Continue to let agent retry on next iteration
|
|
1166
1254
|
|
|
@@ -1189,8 +1277,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
1189
1277
|
"SSE: Emitting todos event from AIMessage tool_calls: %d items",
|
|
1190
1278
|
len(todos),
|
|
1191
1279
|
)
|
|
1192
|
-
|
|
1193
|
-
|
|
1280
|
+
yield {
|
|
1281
|
+
"event": "todos",
|
|
1282
|
+
"data": json.dumps({"todos": todos}),
|
|
1283
|
+
}
|
|
1194
1284
|
# Check if all todos are completed - terminate early
|
|
1195
1285
|
all_completed = all(
|
|
1196
1286
|
t.get("status") == "completed" for t in todos
|
|
@@ -1202,7 +1292,7 @@ async def stream_agent(request: AgentRequest):
|
|
|
1202
1292
|
"다음단계",
|
|
1203
1293
|
"다음 단계",
|
|
1204
1294
|
]
|
|
1205
|
-
|
|
1295
|
+
any(
|
|
1206
1296
|
any(
|
|
1207
1297
|
kw in t.get("content", "")
|
|
1208
1298
|
for kw in summary_keywords
|
|
@@ -1232,7 +1322,9 @@ async def stream_agent(request: AgentRequest):
|
|
|
1232
1322
|
"## 다음 단계",
|
|
1233
1323
|
]
|
|
1234
1324
|
)
|
|
1235
|
-
has_summary =
|
|
1325
|
+
has_summary = (
|
|
1326
|
+
has_summary_json or has_markdown_summary
|
|
1327
|
+
)
|
|
1236
1328
|
|
|
1237
1329
|
# Only check current AIMessage for summary (not history, to avoid false positives)
|
|
1238
1330
|
if not has_summary:
|
|
@@ -1247,20 +1339,41 @@ async def stream_agent(request: AgentRequest):
|
|
|
1247
1339
|
)
|
|
1248
1340
|
# IMPORTANT: Emit the summary content BEFORE terminating
|
|
1249
1341
|
# so the UI can display the summary JSON
|
|
1250
|
-
if msg_content and isinstance(
|
|
1342
|
+
if msg_content and isinstance(
|
|
1343
|
+
msg_content, str
|
|
1344
|
+
):
|
|
1251
1345
|
content_hash = hash(msg_content)
|
|
1252
1346
|
if content_hash not in emitted_contents:
|
|
1253
1347
|
emitted_contents.add(content_hash)
|
|
1254
|
-
repaired_content =
|
|
1348
|
+
repaired_content = (
|
|
1349
|
+
_repair_summary_json_content(
|
|
1350
|
+
msg_content
|
|
1351
|
+
)
|
|
1352
|
+
)
|
|
1255
1353
|
logger.info(
|
|
1256
1354
|
"Auto-terminate: EMITTING summary content (len=%d): %s",
|
|
1257
1355
|
len(repaired_content),
|
|
1258
1356
|
repaired_content[:100],
|
|
1259
1357
|
)
|
|
1260
1358
|
produced_output = True
|
|
1261
|
-
yield {
|
|
1262
|
-
|
|
1263
|
-
|
|
1359
|
+
yield {
|
|
1360
|
+
"event": "token",
|
|
1361
|
+
"data": json.dumps(
|
|
1362
|
+
{
|
|
1363
|
+
"content": repaired_content
|
|
1364
|
+
}
|
|
1365
|
+
),
|
|
1366
|
+
}
|
|
1367
|
+
yield {
|
|
1368
|
+
"event": "debug_clear",
|
|
1369
|
+
"data": json.dumps({}),
|
|
1370
|
+
}
|
|
1371
|
+
yield {
|
|
1372
|
+
"event": "done",
|
|
1373
|
+
"data": json.dumps(
|
|
1374
|
+
{"reason": "all_todos_completed"}
|
|
1375
|
+
),
|
|
1376
|
+
}
|
|
1264
1377
|
return # Exit before executing more tool calls
|
|
1265
1378
|
for tool_call in tool_calls:
|
|
1266
1379
|
tool_name = tool_call.get("name", "unknown")
|
|
@@ -1275,7 +1388,10 @@ async def stream_agent(request: AgentRequest):
|
|
|
1275
1388
|
"SSE: Emitting debug event for tool: %s",
|
|
1276
1389
|
tool_name,
|
|
1277
1390
|
)
|
|
1278
|
-
yield {
|
|
1391
|
+
yield {
|
|
1392
|
+
"event": "debug",
|
|
1393
|
+
"data": json.dumps(status_msg),
|
|
1394
|
+
}
|
|
1279
1395
|
|
|
1280
1396
|
# Send tool_call event with details for frontend to execute
|
|
1281
1397
|
if tool_name in (
|
|
@@ -1283,37 +1399,55 @@ async def stream_agent(request: AgentRequest):
|
|
|
1283
1399
|
"jupyter_cell",
|
|
1284
1400
|
):
|
|
1285
1401
|
produced_output = True
|
|
1286
|
-
yield {
|
|
1402
|
+
yield {
|
|
1403
|
+
"event": "tool_call",
|
|
1404
|
+
"data": json.dumps(
|
|
1405
|
+
{
|
|
1287
1406
|
"tool": "jupyter_cell",
|
|
1288
1407
|
"code": tool_args.get("code", ""),
|
|
1289
1408
|
"description": tool_args.get(
|
|
1290
1409
|
"description", ""
|
|
1291
1410
|
),
|
|
1292
|
-
}
|
|
1411
|
+
}
|
|
1412
|
+
),
|
|
1413
|
+
}
|
|
1293
1414
|
elif tool_name in ("markdown_tool", "markdown"):
|
|
1294
1415
|
produced_output = True
|
|
1295
|
-
yield {
|
|
1416
|
+
yield {
|
|
1417
|
+
"event": "tool_call",
|
|
1418
|
+
"data": json.dumps(
|
|
1419
|
+
{
|
|
1296
1420
|
"tool": "markdown",
|
|
1297
1421
|
"content": tool_args.get(
|
|
1298
1422
|
"content", ""
|
|
1299
1423
|
),
|
|
1300
|
-
}
|
|
1424
|
+
}
|
|
1425
|
+
),
|
|
1426
|
+
}
|
|
1301
1427
|
elif tool_name == "execute_command_tool":
|
|
1302
1428
|
produced_output = True
|
|
1303
|
-
yield {
|
|
1429
|
+
yield {
|
|
1430
|
+
"event": "tool_call",
|
|
1431
|
+
"data": json.dumps(
|
|
1432
|
+
{
|
|
1304
1433
|
"tool": "execute_command_tool",
|
|
1305
1434
|
"command": tool_args.get(
|
|
1306
1435
|
"command", ""
|
|
1307
1436
|
),
|
|
1308
1437
|
"timeout": tool_args.get("timeout"),
|
|
1309
|
-
}
|
|
1438
|
+
}
|
|
1439
|
+
),
|
|
1440
|
+
}
|
|
1310
1441
|
elif tool_name in (
|
|
1311
1442
|
"search_notebook_cells_tool",
|
|
1312
1443
|
"search_notebook_cells",
|
|
1313
1444
|
):
|
|
1314
1445
|
# Search notebook cells - emit tool_call for client-side execution
|
|
1315
1446
|
produced_output = True
|
|
1316
|
-
yield {
|
|
1447
|
+
yield {
|
|
1448
|
+
"event": "tool_call",
|
|
1449
|
+
"data": json.dumps(
|
|
1450
|
+
{
|
|
1317
1451
|
"tool": "search_notebook_cells",
|
|
1318
1452
|
"pattern": tool_args.get(
|
|
1319
1453
|
"pattern", ""
|
|
@@ -1330,7 +1464,25 @@ async def stream_agent(request: AgentRequest):
|
|
|
1330
1464
|
"case_sensitive": tool_args.get(
|
|
1331
1465
|
"case_sensitive", False
|
|
1332
1466
|
),
|
|
1333
|
-
}
|
|
1467
|
+
}
|
|
1468
|
+
),
|
|
1469
|
+
}
|
|
1470
|
+
elif tool_name in (
|
|
1471
|
+
"final_summary_tool",
|
|
1472
|
+
"final_summary",
|
|
1473
|
+
):
|
|
1474
|
+
# Final summary - emit summary event for frontend
|
|
1475
|
+
produced_output = True
|
|
1476
|
+
summary_data = {
|
|
1477
|
+
"summary": tool_args.get("summary", ""),
|
|
1478
|
+
"next_items": tool_args.get(
|
|
1479
|
+
"next_items", []
|
|
1480
|
+
),
|
|
1481
|
+
}
|
|
1482
|
+
yield {
|
|
1483
|
+
"event": "summary",
|
|
1484
|
+
"data": json.dumps(summary_data),
|
|
1485
|
+
}
|
|
1334
1486
|
|
|
1335
1487
|
# Only display content if it's not empty and not a JSON tool response
|
|
1336
1488
|
if (
|
|
@@ -1374,6 +1526,31 @@ async def stream_agent(request: AgentRequest):
|
|
|
1374
1526
|
and content_stripped.startswith("{")
|
|
1375
1527
|
)
|
|
1376
1528
|
)
|
|
1529
|
+
|
|
1530
|
+
# Check if this is summary/next_items with tool_calls (premature summary)
|
|
1531
|
+
has_summary_pattern = (
|
|
1532
|
+
'"summary"' in content or "'summary'" in content
|
|
1533
|
+
) and (
|
|
1534
|
+
'"next_items"' in content
|
|
1535
|
+
or "'next_items'" in content
|
|
1536
|
+
)
|
|
1537
|
+
tool_calls = (
|
|
1538
|
+
getattr(last_message, "tool_calls", []) or []
|
|
1539
|
+
)
|
|
1540
|
+
# Check if any tool_call is NOT write_todos (work still in progress)
|
|
1541
|
+
has_non_todo_tool_calls = any(
|
|
1542
|
+
tc.get("name")
|
|
1543
|
+
not in ("write_todos", "write_todos_tool")
|
|
1544
|
+
for tc in tool_calls
|
|
1545
|
+
)
|
|
1546
|
+
# Skip summary if non-write_todos tool_calls exist (work still in progress)
|
|
1547
|
+
if has_summary_pattern and has_non_todo_tool_calls:
|
|
1548
|
+
logger.info(
|
|
1549
|
+
"Initial: SKIPPING premature summary (has non-todo tool_calls): %s",
|
|
1550
|
+
content[:100],
|
|
1551
|
+
)
|
|
1552
|
+
content = None # Skip this content
|
|
1553
|
+
|
|
1377
1554
|
if (
|
|
1378
1555
|
content
|
|
1379
1556
|
and isinstance(content, str)
|
|
@@ -1399,7 +1576,12 @@ async def stream_agent(request: AgentRequest):
|
|
|
1399
1576
|
repaired_content[:100],
|
|
1400
1577
|
)
|
|
1401
1578
|
produced_output = True
|
|
1402
|
-
yield {
|
|
1579
|
+
yield {
|
|
1580
|
+
"event": "token",
|
|
1581
|
+
"data": json.dumps(
|
|
1582
|
+
{"content": repaired_content}
|
|
1583
|
+
),
|
|
1584
|
+
}
|
|
1403
1585
|
|
|
1404
1586
|
# Drain and emit any subagent events (tool calls from subagents)
|
|
1405
1587
|
for subagent_event in get_subagent_debug_events():
|
|
@@ -1418,7 +1600,12 @@ async def stream_agent(request: AgentRequest):
|
|
|
1418
1600
|
else interrupt
|
|
1419
1601
|
)
|
|
1420
1602
|
|
|
1421
|
-
yield {
|
|
1603
|
+
yield {
|
|
1604
|
+
"event": "debug",
|
|
1605
|
+
"data": json.dumps(
|
|
1606
|
+
{"status": "사용자 승인 대기 중", "icon": "pause"}
|
|
1607
|
+
),
|
|
1608
|
+
}
|
|
1422
1609
|
|
|
1423
1610
|
# Process regular HITL interrupts (non-subagent)
|
|
1424
1611
|
for interrupt in interrupts:
|
|
@@ -1430,7 +1617,9 @@ async def stream_agent(request: AgentRequest):
|
|
|
1430
1617
|
|
|
1431
1618
|
# Extract action requests
|
|
1432
1619
|
action_requests = interrupt_value.get("action_requests", [])
|
|
1433
|
-
logger.info(
|
|
1620
|
+
logger.info(
|
|
1621
|
+
f"[INTERRUPT] action_requests count: {len(action_requests)}, first: {str(action_requests[0])[:200] if action_requests else 'none'}"
|
|
1622
|
+
)
|
|
1434
1623
|
normalized_actions = [
|
|
1435
1624
|
_normalize_action_request(a) for a in action_requests
|
|
1436
1625
|
]
|
|
@@ -1443,14 +1632,16 @@ async def stream_agent(request: AgentRequest):
|
|
|
1443
1632
|
for idx, action in enumerate(normalized_actions):
|
|
1444
1633
|
yield {
|
|
1445
1634
|
"event": "interrupt",
|
|
1446
|
-
"data": json.dumps(
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1635
|
+
"data": json.dumps(
|
|
1636
|
+
{
|
|
1637
|
+
"thread_id": thread_id,
|
|
1638
|
+
"action": action.get("name", "unknown"),
|
|
1639
|
+
"args": action.get("arguments", {}),
|
|
1640
|
+
"description": action.get("description", ""),
|
|
1641
|
+
"action_index": idx,
|
|
1642
|
+
"total_actions": total_actions,
|
|
1643
|
+
}
|
|
1644
|
+
),
|
|
1454
1645
|
}
|
|
1455
1646
|
|
|
1456
1647
|
# Save last signature for resume to avoid duplicate content
|
|
@@ -1541,9 +1732,14 @@ async def stream_agent(request: AgentRequest):
|
|
|
1541
1732
|
)
|
|
1542
1733
|
except asyncio.TimeoutError:
|
|
1543
1734
|
logger.error("SimpleAgent fallback timed out after 30s")
|
|
1544
|
-
yield {
|
|
1735
|
+
yield {
|
|
1736
|
+
"event": "token",
|
|
1737
|
+
"data": json.dumps(
|
|
1738
|
+
{
|
|
1545
1739
|
"content": "모델이 도구 호출을 생성하지 못했습니다. 다시 시도해주세요."
|
|
1546
|
-
}
|
|
1740
|
+
}
|
|
1741
|
+
),
|
|
1742
|
+
}
|
|
1547
1743
|
produced_output = True
|
|
1548
1744
|
fallback_response = None
|
|
1549
1745
|
except Exception as fallback_error:
|
|
@@ -1552,9 +1748,47 @@ async def stream_agent(request: AgentRequest):
|
|
|
1552
1748
|
fallback_error,
|
|
1553
1749
|
exc_info=True,
|
|
1554
1750
|
)
|
|
1555
|
-
yield {
|
|
1751
|
+
yield {
|
|
1752
|
+
"event": "token",
|
|
1753
|
+
"data": json.dumps(
|
|
1754
|
+
{"content": f"오류가 발생했습니다: {str(fallback_error)}"}
|
|
1755
|
+
),
|
|
1756
|
+
}
|
|
1556
1757
|
produced_output = True
|
|
1557
1758
|
fallback_response = None
|
|
1759
|
+
if isinstance(fallback_response, AIMessage):
|
|
1760
|
+
# LLM Response - structured JSON format
|
|
1761
|
+
print(LOG_RESPONSE_START, flush=True)
|
|
1762
|
+
response_data = {
|
|
1763
|
+
"type": "AIMessage",
|
|
1764
|
+
"content": fallback_response.content or "",
|
|
1765
|
+
"tool_calls": fallback_response.tool_calls
|
|
1766
|
+
if hasattr(fallback_response, "tool_calls")
|
|
1767
|
+
else [],
|
|
1768
|
+
"additional_kwargs": getattr(
|
|
1769
|
+
fallback_response, "additional_kwargs", {}
|
|
1770
|
+
)
|
|
1771
|
+
or {},
|
|
1772
|
+
"response_metadata": getattr(
|
|
1773
|
+
fallback_response, "response_metadata", {}
|
|
1774
|
+
)
|
|
1775
|
+
or {},
|
|
1776
|
+
"usage_metadata": getattr(
|
|
1777
|
+
fallback_response, "usage_metadata", {}
|
|
1778
|
+
)
|
|
1779
|
+
or {},
|
|
1780
|
+
}
|
|
1781
|
+
print(
|
|
1782
|
+
json.dumps(
|
|
1783
|
+
response_data,
|
|
1784
|
+
indent=2,
|
|
1785
|
+
ensure_ascii=False,
|
|
1786
|
+
default=str,
|
|
1787
|
+
),
|
|
1788
|
+
flush=True,
|
|
1789
|
+
)
|
|
1790
|
+
print(LOG_RESPONSE_END, flush=True)
|
|
1791
|
+
|
|
1558
1792
|
if isinstance(fallback_response, AIMessage) and getattr(
|
|
1559
1793
|
fallback_response, "tool_calls", None
|
|
1560
1794
|
):
|
|
@@ -1566,27 +1800,68 @@ async def stream_agent(request: AgentRequest):
|
|
|
1566
1800
|
|
|
1567
1801
|
if tool_name in ("jupyter_cell_tool", "jupyter_cell"):
|
|
1568
1802
|
produced_output = True
|
|
1569
|
-
yield {
|
|
1570
|
-
|
|
1803
|
+
yield {
|
|
1804
|
+
"event": "debug",
|
|
1805
|
+
"data": json.dumps(
|
|
1806
|
+
_get_tool_status_message(tool_name, tool_args)
|
|
1807
|
+
),
|
|
1808
|
+
}
|
|
1809
|
+
yield {
|
|
1810
|
+
"event": "tool_call",
|
|
1811
|
+
"data": json.dumps(
|
|
1812
|
+
{
|
|
1571
1813
|
"tool": "jupyter_cell",
|
|
1572
1814
|
"code": tool_args.get("code", ""),
|
|
1573
1815
|
"description": tool_args.get("description", ""),
|
|
1574
|
-
}
|
|
1816
|
+
}
|
|
1817
|
+
),
|
|
1818
|
+
}
|
|
1575
1819
|
elif tool_name in ("markdown_tool", "markdown"):
|
|
1576
1820
|
produced_output = True
|
|
1577
|
-
yield {
|
|
1578
|
-
|
|
1821
|
+
yield {
|
|
1822
|
+
"event": "debug",
|
|
1823
|
+
"data": json.dumps(
|
|
1824
|
+
_get_tool_status_message(tool_name, tool_args)
|
|
1825
|
+
),
|
|
1826
|
+
}
|
|
1827
|
+
yield {
|
|
1828
|
+
"event": "tool_call",
|
|
1829
|
+
"data": json.dumps(
|
|
1830
|
+
{
|
|
1579
1831
|
"tool": "markdown",
|
|
1580
1832
|
"content": tool_args.get("content", ""),
|
|
1581
|
-
}
|
|
1833
|
+
}
|
|
1834
|
+
),
|
|
1835
|
+
}
|
|
1582
1836
|
elif tool_name == "execute_command_tool":
|
|
1583
1837
|
produced_output = True
|
|
1584
|
-
yield {
|
|
1585
|
-
|
|
1838
|
+
yield {
|
|
1839
|
+
"event": "debug",
|
|
1840
|
+
"data": json.dumps(
|
|
1841
|
+
_get_tool_status_message(tool_name, tool_args)
|
|
1842
|
+
),
|
|
1843
|
+
}
|
|
1844
|
+
yield {
|
|
1845
|
+
"event": "tool_call",
|
|
1846
|
+
"data": json.dumps(
|
|
1847
|
+
{
|
|
1586
1848
|
"tool": "execute_command_tool",
|
|
1587
1849
|
"command": tool_args.get("command", ""),
|
|
1588
1850
|
"timeout": tool_args.get("timeout"),
|
|
1589
|
-
}
|
|
1851
|
+
}
|
|
1852
|
+
),
|
|
1853
|
+
}
|
|
1854
|
+
elif tool_name in ("final_summary_tool", "final_summary"):
|
|
1855
|
+
# Final summary - emit summary event for frontend
|
|
1856
|
+
produced_output = True
|
|
1857
|
+
summary_data = {
|
|
1858
|
+
"summary": tool_args.get("summary", ""),
|
|
1859
|
+
"next_items": tool_args.get("next_items", []),
|
|
1860
|
+
}
|
|
1861
|
+
yield {
|
|
1862
|
+
"event": "summary",
|
|
1863
|
+
"data": json.dumps(summary_data),
|
|
1864
|
+
}
|
|
1590
1865
|
elif tool_name == "read_file_tool":
|
|
1591
1866
|
# For file operations, generate code with the LLM
|
|
1592
1867
|
logger.info(
|
|
@@ -1618,20 +1893,32 @@ async def stream_agent(request: AgentRequest):
|
|
|
1618
1893
|
)
|
|
1619
1894
|
|
|
1620
1895
|
if not code:
|
|
1621
|
-
yield {
|
|
1896
|
+
yield {
|
|
1897
|
+
"event": "token",
|
|
1898
|
+
"data": json.dumps(
|
|
1899
|
+
{
|
|
1622
1900
|
"content": "도구 실행을 위한 코드를 생성하지 못했습니다. 다시 시도해주세요."
|
|
1623
|
-
}
|
|
1901
|
+
}
|
|
1902
|
+
),
|
|
1903
|
+
}
|
|
1624
1904
|
produced_output = True
|
|
1625
1905
|
continue
|
|
1626
1906
|
|
|
1627
|
-
yield {
|
|
1907
|
+
yield {
|
|
1908
|
+
"event": "debug",
|
|
1909
|
+
"data": json.dumps(
|
|
1910
|
+
{"status": "[변환] Jupyter Cell로 변환 중"}
|
|
1911
|
+
),
|
|
1912
|
+
}
|
|
1628
1913
|
yield {
|
|
1629
1914
|
"event": "tool_call",
|
|
1630
|
-
"data": json.dumps(
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1915
|
+
"data": json.dumps(
|
|
1916
|
+
{
|
|
1917
|
+
"tool": "jupyter_cell",
|
|
1918
|
+
"code": code,
|
|
1919
|
+
"description": f"Converted from {tool_name}",
|
|
1920
|
+
}
|
|
1921
|
+
),
|
|
1635
1922
|
}
|
|
1636
1923
|
else:
|
|
1637
1924
|
# Unknown tool - skip and show message
|
|
@@ -1640,9 +1927,11 @@ async def stream_agent(request: AgentRequest):
|
|
|
1640
1927
|
)
|
|
1641
1928
|
yield {
|
|
1642
1929
|
"event": "token",
|
|
1643
|
-
"data": json.dumps(
|
|
1644
|
-
|
|
1645
|
-
|
|
1930
|
+
"data": json.dumps(
|
|
1931
|
+
{
|
|
1932
|
+
"content": f"알 수 없는 도구 '{tool_name}'입니다. jupyter_cell_tool을 사용해주세요."
|
|
1933
|
+
}
|
|
1934
|
+
),
|
|
1646
1935
|
}
|
|
1647
1936
|
produced_output = True
|
|
1648
1937
|
elif (
|
|
@@ -1654,25 +1943,41 @@ async def stream_agent(request: AgentRequest):
|
|
|
1654
1943
|
repaired_content = _repair_summary_json_content(
|
|
1655
1944
|
fallback_response.content
|
|
1656
1945
|
)
|
|
1657
|
-
yield {
|
|
1946
|
+
yield {
|
|
1947
|
+
"event": "token",
|
|
1948
|
+
"data": json.dumps({"content": repaired_content}),
|
|
1949
|
+
}
|
|
1658
1950
|
elif fallback_response is not None and not produced_output:
|
|
1659
|
-
yield {
|
|
1951
|
+
yield {
|
|
1952
|
+
"event": "token",
|
|
1953
|
+
"data": json.dumps(
|
|
1954
|
+
{
|
|
1660
1955
|
"content": "모델이 도구 호출을 생성하지 못했습니다. 다시 시도해주세요."
|
|
1661
|
-
}
|
|
1956
|
+
}
|
|
1957
|
+
),
|
|
1958
|
+
}
|
|
1662
1959
|
produced_output = True
|
|
1663
1960
|
|
|
1664
1961
|
# Clear debug status before completion
|
|
1665
1962
|
yield {"event": "debug_clear", "data": json.dumps({})}
|
|
1666
1963
|
|
|
1667
1964
|
# No interrupt - execution completed
|
|
1668
|
-
yield {
|
|
1965
|
+
yield {
|
|
1966
|
+
"event": "complete",
|
|
1967
|
+
"data": json.dumps({"success": True, "thread_id": thread_id}),
|
|
1968
|
+
}
|
|
1669
1969
|
|
|
1670
1970
|
except Exception as e:
|
|
1671
1971
|
logger.error(f"Stream error: {e}", exc_info=True)
|
|
1672
|
-
yield {
|
|
1972
|
+
yield {
|
|
1973
|
+
"event": "error",
|
|
1974
|
+
"data": json.dumps(
|
|
1975
|
+
{
|
|
1673
1976
|
"error": str(e),
|
|
1674
1977
|
"error_type": type(e).__name__,
|
|
1675
|
-
}
|
|
1978
|
+
}
|
|
1979
|
+
),
|
|
1980
|
+
}
|
|
1676
1981
|
|
|
1677
1982
|
return EventSourceResponse(event_generator())
|
|
1678
1983
|
|
|
@@ -1726,11 +2031,16 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1726
2031
|
"Server may have restarted or session expired.",
|
|
1727
2032
|
request.threadId,
|
|
1728
2033
|
)
|
|
1729
|
-
yield {
|
|
2034
|
+
yield {
|
|
2035
|
+
"event": "error",
|
|
2036
|
+
"data": json.dumps(
|
|
2037
|
+
{
|
|
1730
2038
|
"error": "Session expired or not found",
|
|
1731
2039
|
"code": "CHECKPOINT_NOT_FOUND",
|
|
1732
2040
|
"message": "이전 세션을 찾을 수 없습니다. 서버가 재시작되었거나 세션이 만료되었습니다. 새로운 대화를 시작해주세요.",
|
|
1733
|
-
}
|
|
2041
|
+
}
|
|
2042
|
+
),
|
|
2043
|
+
}
|
|
1734
2044
|
return
|
|
1735
2045
|
|
|
1736
2046
|
checkpointer = _simple_agent_checkpointers.get(request.threadId)
|
|
@@ -1740,14 +2050,28 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1740
2050
|
logger.info("Resume: Agent mode: %s", agent_mode)
|
|
1741
2051
|
|
|
1742
2052
|
# Get agent prompts (for multi-agent mode)
|
|
1743
|
-
# ROOT CAUSE: Frontend sends both systemPrompt AND agentPrompts
|
|
1744
|
-
# FIX: Ignore all custom prompts for multi-agent mode
|
|
1745
2053
|
agent_prompts = None
|
|
1746
2054
|
if agent_mode == "multi":
|
|
1747
2055
|
if request.llmConfig and request.llmConfig.agent_prompts:
|
|
1748
|
-
|
|
2056
|
+
agent_prompts = {
|
|
2057
|
+
"planner": request.llmConfig.agent_prompts.planner,
|
|
2058
|
+
"python_developer": (
|
|
2059
|
+
request.llmConfig.agent_prompts.python_developer
|
|
2060
|
+
),
|
|
2061
|
+
"researcher": request.llmConfig.agent_prompts.researcher,
|
|
2062
|
+
"athena_query": request.llmConfig.agent_prompts.athena_query,
|
|
2063
|
+
}
|
|
2064
|
+
agent_prompts = {k: v for k, v in agent_prompts.items() if v}
|
|
2065
|
+
logger.info(
|
|
2066
|
+
"Resume: Multi-agent mode - using agentPrompts (%s)",
|
|
2067
|
+
list(agent_prompts.keys()),
|
|
2068
|
+
)
|
|
2069
|
+
# In multi-agent mode, DON'T use systemPrompt as override
|
|
1749
2070
|
if system_prompt_override:
|
|
1750
|
-
logger.
|
|
2071
|
+
logger.info(
|
|
2072
|
+
"Resume: Multi-agent mode - ignoring systemPrompt (len=%d)",
|
|
2073
|
+
len(system_prompt_override),
|
|
2074
|
+
)
|
|
1751
2075
|
system_prompt_override = None
|
|
1752
2076
|
|
|
1753
2077
|
agent_cache_key = _get_agent_cache_key(
|
|
@@ -1822,7 +2146,12 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1822
2146
|
)
|
|
1823
2147
|
# Track code execution for history (injected into subagent context)
|
|
1824
2148
|
tool_name = edited_action.get("name", "")
|
|
1825
|
-
if tool_name in (
|
|
2149
|
+
if tool_name in (
|
|
2150
|
+
"jupyter_cell_tool",
|
|
2151
|
+
"write_file_tool",
|
|
2152
|
+
"edit_file_tool",
|
|
2153
|
+
"multiedit_file_tool",
|
|
2154
|
+
):
|
|
1826
2155
|
track_tool_execution(tool_name, args)
|
|
1827
2156
|
langgraph_decisions.append(
|
|
1828
2157
|
{
|
|
@@ -1840,7 +2169,10 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1840
2169
|
)
|
|
1841
2170
|
|
|
1842
2171
|
# Resume execution
|
|
1843
|
-
yield {
|
|
2172
|
+
yield {
|
|
2173
|
+
"event": "debug",
|
|
2174
|
+
"data": json.dumps({"status": "실행 재개 중", "icon": "play"}),
|
|
2175
|
+
}
|
|
1844
2176
|
|
|
1845
2177
|
_simple_agent_pending_actions.pop(request.threadId, None)
|
|
1846
2178
|
|
|
@@ -1866,7 +2198,10 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1866
2198
|
)
|
|
1867
2199
|
|
|
1868
2200
|
# Status: waiting for LLM response
|
|
1869
|
-
yield {
|
|
2201
|
+
yield {
|
|
2202
|
+
"event": "debug",
|
|
2203
|
+
"data": json.dumps({"status": "LLM 응답 대기 중", "icon": "thinking"}),
|
|
2204
|
+
}
|
|
1870
2205
|
|
|
1871
2206
|
step_count = 0
|
|
1872
2207
|
|
|
@@ -1878,9 +2213,16 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1878
2213
|
):
|
|
1879
2214
|
# Check if thread was cancelled by user
|
|
1880
2215
|
if is_thread_cancelled(request.threadId):
|
|
1881
|
-
logger.info(
|
|
2216
|
+
logger.info(
|
|
2217
|
+
f"Thread {request.threadId} cancelled by user, stopping resume stream"
|
|
2218
|
+
)
|
|
1882
2219
|
clear_cancelled_thread(request.threadId)
|
|
1883
|
-
yield {
|
|
2220
|
+
yield {
|
|
2221
|
+
"event": "cancelled",
|
|
2222
|
+
"data": json.dumps(
|
|
2223
|
+
{"message": "작업이 사용자에 의해 중단되었습니다."}
|
|
2224
|
+
),
|
|
2225
|
+
}
|
|
1884
2226
|
return
|
|
1885
2227
|
|
|
1886
2228
|
step_count += 1
|
|
@@ -1982,7 +2324,10 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1982
2324
|
todos = _extract_todos(last_message.content)
|
|
1983
2325
|
if todos:
|
|
1984
2326
|
latest_todos = todos
|
|
1985
|
-
yield {
|
|
2327
|
+
yield {
|
|
2328
|
+
"event": "todos",
|
|
2329
|
+
"data": json.dumps({"todos": todos}),
|
|
2330
|
+
}
|
|
1986
2331
|
# Check if all todos are completed - auto terminate only if summary exists
|
|
1987
2332
|
all_completed = all(
|
|
1988
2333
|
t.get("status") == "completed" for t in todos
|
|
@@ -1991,16 +2336,31 @@ async def resume_agent(request: ResumeRequest):
|
|
|
1991
2336
|
# Check if summary JSON exists in the CURRENT step's AIMessage
|
|
1992
2337
|
# (not in history, to avoid false positives from previous tasks)
|
|
1993
2338
|
summary_exists = False
|
|
1994
|
-
step_messages =
|
|
2339
|
+
step_messages = (
|
|
2340
|
+
step.get("messages", [])
|
|
2341
|
+
if isinstance(step, dict)
|
|
2342
|
+
else []
|
|
2343
|
+
)
|
|
1995
2344
|
# Only check the AIMessage that called write_todos (should be right before this ToolMessage)
|
|
1996
|
-
for recent_msg in step_messages[
|
|
2345
|
+
for recent_msg in step_messages[
|
|
2346
|
+
-3:
|
|
2347
|
+
]: # Check only the most recent few messages
|
|
1997
2348
|
if isinstance(recent_msg, AIMessage):
|
|
1998
|
-
recent_content =
|
|
2349
|
+
recent_content = (
|
|
2350
|
+
getattr(recent_msg, "content", "") or ""
|
|
2351
|
+
)
|
|
1999
2352
|
if isinstance(recent_content, list):
|
|
2000
|
-
recent_content = " ".join(
|
|
2001
|
-
|
|
2353
|
+
recent_content = " ".join(
|
|
2354
|
+
str(p) for p in recent_content
|
|
2355
|
+
)
|
|
2356
|
+
if (
|
|
2357
|
+
'"summary"' in recent_content
|
|
2358
|
+
and '"next_items"' in recent_content
|
|
2359
|
+
):
|
|
2002
2360
|
summary_exists = True
|
|
2003
|
-
logger.info(
|
|
2361
|
+
logger.info(
|
|
2362
|
+
"Resume: Found summary in current AIMessage content"
|
|
2363
|
+
)
|
|
2004
2364
|
break
|
|
2005
2365
|
|
|
2006
2366
|
if summary_exists:
|
|
@@ -2012,23 +2372,53 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2012
2372
|
# Find and emit the AIMessage content with summary
|
|
2013
2373
|
for recent_msg in step_messages[-3:]:
|
|
2014
2374
|
if isinstance(recent_msg, AIMessage):
|
|
2015
|
-
step_content =
|
|
2375
|
+
step_content = (
|
|
2376
|
+
getattr(recent_msg, "content", "")
|
|
2377
|
+
or ""
|
|
2378
|
+
)
|
|
2016
2379
|
if isinstance(step_content, list):
|
|
2017
|
-
step_content = " ".join(
|
|
2018
|
-
|
|
2380
|
+
step_content = " ".join(
|
|
2381
|
+
str(p) for p in step_content
|
|
2382
|
+
)
|
|
2383
|
+
if (
|
|
2384
|
+
'"summary"' in step_content
|
|
2385
|
+
and '"next_items"' in step_content
|
|
2386
|
+
):
|
|
2019
2387
|
content_hash = hash(step_content)
|
|
2020
|
-
if
|
|
2021
|
-
|
|
2022
|
-
|
|
2388
|
+
if (
|
|
2389
|
+
content_hash
|
|
2390
|
+
not in emitted_contents
|
|
2391
|
+
):
|
|
2392
|
+
emitted_contents.add(
|
|
2393
|
+
content_hash
|
|
2394
|
+
)
|
|
2395
|
+
repaired_content = _repair_summary_json_content(
|
|
2396
|
+
step_content
|
|
2397
|
+
)
|
|
2023
2398
|
logger.info(
|
|
2024
2399
|
"Resume step auto-terminate: EMITTING summary content (len=%d): %s",
|
|
2025
2400
|
len(repaired_content),
|
|
2026
2401
|
repaired_content[:100],
|
|
2027
2402
|
)
|
|
2028
|
-
yield {
|
|
2403
|
+
yield {
|
|
2404
|
+
"event": "token",
|
|
2405
|
+
"data": json.dumps(
|
|
2406
|
+
{
|
|
2407
|
+
"content": repaired_content
|
|
2408
|
+
}
|
|
2409
|
+
),
|
|
2410
|
+
}
|
|
2029
2411
|
break
|
|
2030
|
-
yield {
|
|
2031
|
-
|
|
2412
|
+
yield {
|
|
2413
|
+
"event": "debug_clear",
|
|
2414
|
+
"data": json.dumps({}),
|
|
2415
|
+
}
|
|
2416
|
+
yield {
|
|
2417
|
+
"event": "done",
|
|
2418
|
+
"data": json.dumps(
|
|
2419
|
+
{"reason": "all_todos_completed"}
|
|
2420
|
+
),
|
|
2421
|
+
}
|
|
2032
2422
|
return # Exit the generator
|
|
2033
2423
|
else:
|
|
2034
2424
|
logger.warning(
|
|
@@ -2056,7 +2446,39 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2056
2446
|
# ToolMessage processing continues (no final_answer_tool)
|
|
2057
2447
|
|
|
2058
2448
|
# Handle AIMessage (use elif to avoid processing after ToolMessage)
|
|
2059
|
-
elif
|
|
2449
|
+
elif isinstance(last_message, AIMessage):
|
|
2450
|
+
# LLM Response - structured JSON format
|
|
2451
|
+
print(LOG_RESPONSE_START, flush=True)
|
|
2452
|
+
response_data = {
|
|
2453
|
+
"type": "AIMessage",
|
|
2454
|
+
"content": last_message.content or "",
|
|
2455
|
+
"tool_calls": last_message.tool_calls
|
|
2456
|
+
if hasattr(last_message, "tool_calls")
|
|
2457
|
+
else [],
|
|
2458
|
+
"additional_kwargs": getattr(
|
|
2459
|
+
last_message, "additional_kwargs", {}
|
|
2460
|
+
)
|
|
2461
|
+
or {},
|
|
2462
|
+
"response_metadata": getattr(
|
|
2463
|
+
last_message, "response_metadata", {}
|
|
2464
|
+
)
|
|
2465
|
+
or {},
|
|
2466
|
+
"usage_metadata": getattr(
|
|
2467
|
+
last_message, "usage_metadata", {}
|
|
2468
|
+
)
|
|
2469
|
+
or {},
|
|
2470
|
+
}
|
|
2471
|
+
print(
|
|
2472
|
+
json.dumps(
|
|
2473
|
+
response_data,
|
|
2474
|
+
indent=2,
|
|
2475
|
+
ensure_ascii=False,
|
|
2476
|
+
default=str,
|
|
2477
|
+
),
|
|
2478
|
+
flush=True,
|
|
2479
|
+
)
|
|
2480
|
+
print(LOG_RESPONSE_END, flush=True)
|
|
2481
|
+
|
|
2060
2482
|
content = last_message.content
|
|
2061
2483
|
|
|
2062
2484
|
# Handle list content (e.g., multimodal responses)
|
|
@@ -2090,6 +2512,28 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2090
2512
|
and content_stripped.startswith("{")
|
|
2091
2513
|
)
|
|
2092
2514
|
)
|
|
2515
|
+
|
|
2516
|
+
# Check if this is summary/next_items with tool_calls (premature summary)
|
|
2517
|
+
has_summary_pattern = (
|
|
2518
|
+
'"summary"' in content or "'summary'" in content
|
|
2519
|
+
) and (
|
|
2520
|
+
'"next_items"' in content or "'next_items'" in content
|
|
2521
|
+
)
|
|
2522
|
+
tool_calls = getattr(last_message, "tool_calls", []) or []
|
|
2523
|
+
# Check if any tool_call is NOT write_todos (work still in progress)
|
|
2524
|
+
has_non_todo_tool_calls = any(
|
|
2525
|
+
tc.get("name")
|
|
2526
|
+
not in ("write_todos", "write_todos_tool")
|
|
2527
|
+
for tc in tool_calls
|
|
2528
|
+
)
|
|
2529
|
+
# Skip summary if non-write_todos tool_calls exist (work still in progress)
|
|
2530
|
+
if has_summary_pattern and has_non_todo_tool_calls:
|
|
2531
|
+
logger.info(
|
|
2532
|
+
"Resume: SKIPPING premature summary (has non-todo tool_calls): %s",
|
|
2533
|
+
content[:100],
|
|
2534
|
+
)
|
|
2535
|
+
content = None # Skip this content
|
|
2536
|
+
|
|
2093
2537
|
if (
|
|
2094
2538
|
content
|
|
2095
2539
|
and isinstance(content, str)
|
|
@@ -2120,7 +2564,12 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2120
2564
|
len(repaired_content),
|
|
2121
2565
|
log_preview,
|
|
2122
2566
|
)
|
|
2123
|
-
yield {
|
|
2567
|
+
yield {
|
|
2568
|
+
"event": "token",
|
|
2569
|
+
"data": json.dumps(
|
|
2570
|
+
{"content": repaired_content}
|
|
2571
|
+
),
|
|
2572
|
+
}
|
|
2124
2573
|
|
|
2125
2574
|
if (
|
|
2126
2575
|
hasattr(last_message, "tool_calls")
|
|
@@ -2147,7 +2596,10 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2147
2596
|
todos = _emit_todos_from_tool_calls(new_tool_calls)
|
|
2148
2597
|
if todos:
|
|
2149
2598
|
latest_todos = todos
|
|
2150
|
-
yield {
|
|
2599
|
+
yield {
|
|
2600
|
+
"event": "todos",
|
|
2601
|
+
"data": json.dumps({"todos": todos}),
|
|
2602
|
+
}
|
|
2151
2603
|
# Check if all todos are completed - terminate early
|
|
2152
2604
|
all_completed = all(
|
|
2153
2605
|
t.get("status") == "completed" for t in todos
|
|
@@ -2159,7 +2611,7 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2159
2611
|
"다음단계",
|
|
2160
2612
|
"다음 단계",
|
|
2161
2613
|
]
|
|
2162
|
-
|
|
2614
|
+
any(
|
|
2163
2615
|
any(
|
|
2164
2616
|
kw in t.get("content", "")
|
|
2165
2617
|
for kw in summary_keywords
|
|
@@ -2189,7 +2641,9 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2189
2641
|
"## 다음 단계",
|
|
2190
2642
|
]
|
|
2191
2643
|
)
|
|
2192
|
-
has_summary =
|
|
2644
|
+
has_summary = (
|
|
2645
|
+
has_summary_json or has_markdown_summary
|
|
2646
|
+
)
|
|
2193
2647
|
|
|
2194
2648
|
# Only check current AIMessage for summary (not history, to avoid false positives)
|
|
2195
2649
|
if not has_summary:
|
|
@@ -2204,19 +2658,40 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2204
2658
|
)
|
|
2205
2659
|
# IMPORTANT: Emit the summary content BEFORE terminating
|
|
2206
2660
|
# so the UI can display the summary JSON
|
|
2207
|
-
if msg_content and isinstance(
|
|
2661
|
+
if msg_content and isinstance(
|
|
2662
|
+
msg_content, str
|
|
2663
|
+
):
|
|
2208
2664
|
content_hash = hash(msg_content)
|
|
2209
2665
|
if content_hash not in emitted_contents:
|
|
2210
2666
|
emitted_contents.add(content_hash)
|
|
2211
|
-
repaired_content =
|
|
2667
|
+
repaired_content = (
|
|
2668
|
+
_repair_summary_json_content(
|
|
2669
|
+
msg_content
|
|
2670
|
+
)
|
|
2671
|
+
)
|
|
2212
2672
|
logger.info(
|
|
2213
2673
|
"Resume auto-terminate: EMITTING summary content (len=%d): %s",
|
|
2214
2674
|
len(repaired_content),
|
|
2215
2675
|
repaired_content[:100],
|
|
2216
2676
|
)
|
|
2217
|
-
yield {
|
|
2218
|
-
|
|
2219
|
-
|
|
2677
|
+
yield {
|
|
2678
|
+
"event": "token",
|
|
2679
|
+
"data": json.dumps(
|
|
2680
|
+
{
|
|
2681
|
+
"content": repaired_content
|
|
2682
|
+
}
|
|
2683
|
+
),
|
|
2684
|
+
}
|
|
2685
|
+
yield {
|
|
2686
|
+
"event": "debug_clear",
|
|
2687
|
+
"data": json.dumps({}),
|
|
2688
|
+
}
|
|
2689
|
+
yield {
|
|
2690
|
+
"event": "done",
|
|
2691
|
+
"data": json.dumps(
|
|
2692
|
+
{"reason": "all_todos_completed"}
|
|
2693
|
+
),
|
|
2694
|
+
}
|
|
2220
2695
|
return # Exit before executing more tool calls
|
|
2221
2696
|
|
|
2222
2697
|
# Process tool calls
|
|
@@ -2236,40 +2711,61 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2236
2711
|
tool_name, tool_args
|
|
2237
2712
|
)
|
|
2238
2713
|
|
|
2239
|
-
yield {
|
|
2714
|
+
yield {
|
|
2715
|
+
"event": "debug",
|
|
2716
|
+
"data": json.dumps(status_msg),
|
|
2717
|
+
}
|
|
2240
2718
|
|
|
2241
2719
|
if tool_name in (
|
|
2242
2720
|
"jupyter_cell_tool",
|
|
2243
2721
|
"jupyter_cell",
|
|
2244
2722
|
):
|
|
2245
|
-
yield {
|
|
2723
|
+
yield {
|
|
2724
|
+
"event": "tool_call",
|
|
2725
|
+
"data": json.dumps(
|
|
2726
|
+
{
|
|
2246
2727
|
"tool": "jupyter_cell",
|
|
2247
2728
|
"code": tool_args.get("code", ""),
|
|
2248
2729
|
"description": tool_args.get(
|
|
2249
2730
|
"description", ""
|
|
2250
2731
|
),
|
|
2251
|
-
}
|
|
2732
|
+
}
|
|
2733
|
+
),
|
|
2734
|
+
}
|
|
2252
2735
|
elif tool_name in ("markdown_tool", "markdown"):
|
|
2253
|
-
yield {
|
|
2736
|
+
yield {
|
|
2737
|
+
"event": "tool_call",
|
|
2738
|
+
"data": json.dumps(
|
|
2739
|
+
{
|
|
2254
2740
|
"tool": "markdown",
|
|
2255
2741
|
"content": tool_args.get(
|
|
2256
2742
|
"content", ""
|
|
2257
2743
|
),
|
|
2258
|
-
}
|
|
2744
|
+
}
|
|
2745
|
+
),
|
|
2746
|
+
}
|
|
2259
2747
|
elif tool_name == "execute_command_tool":
|
|
2260
|
-
yield {
|
|
2748
|
+
yield {
|
|
2749
|
+
"event": "tool_call",
|
|
2750
|
+
"data": json.dumps(
|
|
2751
|
+
{
|
|
2261
2752
|
"tool": "execute_command_tool",
|
|
2262
2753
|
"command": tool_args.get(
|
|
2263
2754
|
"command", ""
|
|
2264
2755
|
),
|
|
2265
2756
|
"timeout": tool_args.get("timeout"),
|
|
2266
|
-
}
|
|
2757
|
+
}
|
|
2758
|
+
),
|
|
2759
|
+
}
|
|
2267
2760
|
elif tool_name in (
|
|
2268
2761
|
"search_notebook_cells_tool",
|
|
2269
2762
|
"search_notebook_cells",
|
|
2270
2763
|
):
|
|
2271
2764
|
# Search notebook cells - emit tool_call for client-side execution
|
|
2272
|
-
yield {
|
|
2765
|
+
yield {
|
|
2766
|
+
"event": "tool_call",
|
|
2767
|
+
"data": json.dumps(
|
|
2768
|
+
{
|
|
2273
2769
|
"tool": "search_notebook_cells",
|
|
2274
2770
|
"pattern": tool_args.get(
|
|
2275
2771
|
"pattern", ""
|
|
@@ -2286,7 +2782,24 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2286
2782
|
"case_sensitive": tool_args.get(
|
|
2287
2783
|
"case_sensitive", False
|
|
2288
2784
|
),
|
|
2289
|
-
}
|
|
2785
|
+
}
|
|
2786
|
+
),
|
|
2787
|
+
}
|
|
2788
|
+
elif tool_name in (
|
|
2789
|
+
"final_summary_tool",
|
|
2790
|
+
"final_summary",
|
|
2791
|
+
):
|
|
2792
|
+
# Final summary - emit summary event for frontend
|
|
2793
|
+
summary_data = {
|
|
2794
|
+
"summary": tool_args.get("summary", ""),
|
|
2795
|
+
"next_items": tool_args.get(
|
|
2796
|
+
"next_items", []
|
|
2797
|
+
),
|
|
2798
|
+
}
|
|
2799
|
+
yield {
|
|
2800
|
+
"event": "summary",
|
|
2801
|
+
"data": json.dumps(summary_data),
|
|
2802
|
+
}
|
|
2290
2803
|
|
|
2291
2804
|
# Drain and emit any subagent events (tool calls from subagents)
|
|
2292
2805
|
for subagent_event in get_subagent_debug_events():
|
|
@@ -2297,7 +2810,12 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2297
2810
|
if isinstance(step, dict) and "__interrupt__" in step:
|
|
2298
2811
|
interrupts = step["__interrupt__"]
|
|
2299
2812
|
|
|
2300
|
-
yield {
|
|
2813
|
+
yield {
|
|
2814
|
+
"event": "debug",
|
|
2815
|
+
"data": json.dumps(
|
|
2816
|
+
{"status": "사용자 승인 대기 중", "icon": "pause"}
|
|
2817
|
+
),
|
|
2818
|
+
}
|
|
2301
2819
|
|
|
2302
2820
|
for interrupt in interrupts:
|
|
2303
2821
|
interrupt_value = (
|
|
@@ -2306,7 +2824,9 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2306
2824
|
else interrupt
|
|
2307
2825
|
)
|
|
2308
2826
|
action_requests = interrupt_value.get("action_requests", [])
|
|
2309
|
-
logger.info(
|
|
2827
|
+
logger.info(
|
|
2828
|
+
f"[RESUME INTERRUPT] action_requests count: {len(action_requests)}, first: {str(action_requests[0])[:200] if action_requests else 'none'}"
|
|
2829
|
+
)
|
|
2310
2830
|
normalized_actions = [
|
|
2311
2831
|
_normalize_action_request(a) for a in action_requests
|
|
2312
2832
|
]
|
|
@@ -2319,14 +2839,16 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2319
2839
|
for idx, action in enumerate(normalized_actions):
|
|
2320
2840
|
yield {
|
|
2321
2841
|
"event": "interrupt",
|
|
2322
|
-
"data": json.dumps(
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2842
|
+
"data": json.dumps(
|
|
2843
|
+
{
|
|
2844
|
+
"thread_id": request.threadId,
|
|
2845
|
+
"action": action.get("name", "unknown"),
|
|
2846
|
+
"args": action.get("arguments", {}),
|
|
2847
|
+
"description": action.get("description", ""),
|
|
2848
|
+
"action_index": idx,
|
|
2849
|
+
"total_actions": total_actions,
|
|
2850
|
+
}
|
|
2851
|
+
),
|
|
2330
2852
|
}
|
|
2331
2853
|
|
|
2332
2854
|
# Save last signature for next resume to avoid duplicate content
|
|
@@ -2359,7 +2881,10 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2359
2881
|
last_signature,
|
|
2360
2882
|
latest_todos,
|
|
2361
2883
|
)
|
|
2362
|
-
yield {
|
|
2884
|
+
yield {
|
|
2885
|
+
"event": "complete",
|
|
2886
|
+
"data": json.dumps({"success": True, "thread_id": request.threadId}),
|
|
2887
|
+
}
|
|
2363
2888
|
|
|
2364
2889
|
except Exception as e:
|
|
2365
2890
|
error_msg = str(e)
|
|
@@ -2370,17 +2895,27 @@ async def resume_agent(request: ResumeRequest):
|
|
|
2370
2895
|
logger.warning(
|
|
2371
2896
|
"Detected 'contents is not specified' error - likely session state loss"
|
|
2372
2897
|
)
|
|
2373
|
-
yield {
|
|
2898
|
+
yield {
|
|
2899
|
+
"event": "error",
|
|
2900
|
+
"data": json.dumps(
|
|
2901
|
+
{
|
|
2374
2902
|
"error": "Session state lost",
|
|
2375
2903
|
"code": "CONTENTS_NOT_SPECIFIED",
|
|
2376
2904
|
"error_type": type(e).__name__,
|
|
2377
2905
|
"message": "세션 상태가 손실되었습니다. 서버가 재시작되었거나 세션이 만료되었습니다. 새로운 대화를 시작해주세요.",
|
|
2378
|
-
}
|
|
2906
|
+
}
|
|
2907
|
+
),
|
|
2908
|
+
}
|
|
2379
2909
|
else:
|
|
2380
|
-
yield {
|
|
2910
|
+
yield {
|
|
2911
|
+
"event": "error",
|
|
2912
|
+
"data": json.dumps(
|
|
2913
|
+
{
|
|
2381
2914
|
"error": error_msg,
|
|
2382
2915
|
"error_type": type(e).__name__,
|
|
2383
|
-
}
|
|
2916
|
+
}
|
|
2917
|
+
),
|
|
2918
|
+
}
|
|
2384
2919
|
|
|
2385
2920
|
return EventSourceResponse(event_generator())
|
|
2386
2921
|
|
|
@@ -2437,6 +2972,7 @@ async def health_check() -> Dict[str, Any]:
|
|
|
2437
2972
|
|
|
2438
2973
|
class CancelRequest(BaseModel):
|
|
2439
2974
|
"""Request to cancel a running agent thread"""
|
|
2975
|
+
|
|
2440
2976
|
thread_id: str
|
|
2441
2977
|
|
|
2442
2978
|
|
|
@@ -2463,3 +2999,70 @@ async def clear_agent_cache() -> Dict[str, Any]:
|
|
|
2463
2999
|
"cleared": count,
|
|
2464
3000
|
"message": f"Cleared {count} cached agent instances",
|
|
2465
3001
|
}
|
|
3002
|
+
|
|
3003
|
+
|
|
3004
|
+
class ResetRequest(BaseModel):
|
|
3005
|
+
"""Request to reset a thread (clear session and recreate agent)"""
|
|
3006
|
+
|
|
3007
|
+
thread_id: str
|
|
3008
|
+
|
|
3009
|
+
|
|
3010
|
+
@router.post("/reset")
|
|
3011
|
+
async def reset_agent_thread(request: ResetRequest) -> Dict[str, Any]:
|
|
3012
|
+
"""
|
|
3013
|
+
Reset an agent thread by clearing all session state.
|
|
3014
|
+
|
|
3015
|
+
This will:
|
|
3016
|
+
- Clear the checkpointer (conversation history)
|
|
3017
|
+
- Clear pending actions
|
|
3018
|
+
- Clear emitted contents
|
|
3019
|
+
- Clear last signatures
|
|
3020
|
+
- Remove from cancelled threads set
|
|
3021
|
+
|
|
3022
|
+
The agent instance itself is not cleared (it's shared across threads),
|
|
3023
|
+
but the thread state is completely reset.
|
|
3024
|
+
"""
|
|
3025
|
+
thread_id = request.thread_id
|
|
3026
|
+
|
|
3027
|
+
# Track what was cleared
|
|
3028
|
+
cleared = []
|
|
3029
|
+
|
|
3030
|
+
# Clear checkpointer (conversation history)
|
|
3031
|
+
if thread_id in _simple_agent_checkpointers:
|
|
3032
|
+
del _simple_agent_checkpointers[thread_id]
|
|
3033
|
+
cleared.append("checkpointer")
|
|
3034
|
+
|
|
3035
|
+
# Clear pending actions
|
|
3036
|
+
if thread_id in _simple_agent_pending_actions:
|
|
3037
|
+
del _simple_agent_pending_actions[thread_id]
|
|
3038
|
+
cleared.append("pending_actions")
|
|
3039
|
+
|
|
3040
|
+
# Clear last signatures
|
|
3041
|
+
if thread_id in _simple_agent_last_signatures:
|
|
3042
|
+
del _simple_agent_last_signatures[thread_id]
|
|
3043
|
+
cleared.append("last_signatures")
|
|
3044
|
+
|
|
3045
|
+
# Clear emitted contents
|
|
3046
|
+
if thread_id in _simple_agent_emitted_contents:
|
|
3047
|
+
del _simple_agent_emitted_contents[thread_id]
|
|
3048
|
+
cleared.append("emitted_contents")
|
|
3049
|
+
|
|
3050
|
+
# Remove from cancelled threads
|
|
3051
|
+
if thread_id in _cancelled_threads:
|
|
3052
|
+
_cancelled_threads.discard(thread_id)
|
|
3053
|
+
cleared.append("cancelled_flag")
|
|
3054
|
+
|
|
3055
|
+
logger.info(
|
|
3056
|
+
"Reset thread %s: cleared %s",
|
|
3057
|
+
thread_id,
|
|
3058
|
+
", ".join(cleared) if cleared else "nothing (thread not found)",
|
|
3059
|
+
)
|
|
3060
|
+
|
|
3061
|
+
return {
|
|
3062
|
+
"status": "ok",
|
|
3063
|
+
"thread_id": thread_id,
|
|
3064
|
+
"cleared": cleared,
|
|
3065
|
+
"message": f"Thread {thread_id} has been reset"
|
|
3066
|
+
if cleared
|
|
3067
|
+
else f"Thread {thread_id} had no state to clear",
|
|
3068
|
+
}
|