-
Notifications
You must be signed in to change notification settings - Fork 0
Human in the Loop
이 페이지는 "에이전트가 장기 메모리에 뭔가 저장하기 직전에 사람이 개입해서 리뷰/편집/승인하는 흐름" 을 실제 코드 경로로 추적한다. 상위 개요는 Memory and HITL 에 있으며, 여기서는 함수 호출 체인 + 세션 상태 전이 를 중점적으로 다룬다.
장기 메모리에 아무 파일이나 자동 저장하면:
-
오염: 임시 산출물, 실험 스크립트, 실패한 시도가 계속 쌓여서
memory_search결과가 노이즈로 묻힘 -
잘못된 계층 배치: 프로젝트 한정 결정이
domain/knowledge로 들어가면 다른 프로젝트에서도 잘못 재주입됨 - 통제 불가능한 사일런트 기억: 사용자가 "무엇이 기억됐는지" 검토할 시점이 없음
그래서 이 프로젝트는 remember SubAgent 가 후보를 큐레이션 → 사람이 리뷰/편집 → 승인된 것만 저장 하는 3단 구조를 세션 말미에 끼워넣는다. 이 구조를 mid-session Human-in-the-Loop 이라고 부른다. "답변 완료 후 피드백" 이 아니라 Main Agent 가 최종 답변을 내기 직전에 세션을 멈춰 세우는 것이 핵심이다.
사용자 질의
↓
Main Agent stream 시작 → SubAgent 들이 async 로 작업
↓
모든 async task 완료
↓
[1] _maybe_force_remember_subagent() # 강제 remember launch
└→ agent.invoke(HumanMessage("remember subagent 를 지금 띄워라..."))
└→ Main Agent 가 start_async_task(subagent_type="remember") 호출
└→ remember SubAgent 가 JSON 블록 생성하고 completed 상태로 전환
↓
[2] _pause_for_human_review_if_needed() # 세션 pause
└→ _parse_remember_candidates_from_history() 로 JSON → 후보 목록
└→ st.session_state["_pending_human_review"] = {...}
└→ st.rerun() # Streamlit 리렌더 → 스트리밍 루프 탈출
↓
[3] _render_live_remember_review() # HITL 카드 + 폼 렌더링
└→ _render_remember_review_form()
└→ 사용자가 계층/근거/노트 편집 → Approve 클릭
↓
[4] _store_approved_memory_files() # 실제 저장
└→ SQLite durable store + ChromaDB 양쪽에 기록
└→ st.session_state["_human_review_resolution"] = {"approved": True, ...}
└→ st.session_state.pop("_pending_human_review", None)
└→ st.rerun()
↓
[5] _resume_async_monitoring 재진입
└→ human_review_note 를 포함한 followup HumanMessage 생성
└→ agent.invoke(...) 로 Main Agent 최종 aggregation
각 단계가 코드의 어느 파일/라인인지 이제 하나씩 짚는다.
구현: src/coding_agent/webui/_pages/chat.py:2814 _maybe_force_remember_subagent()
세 조건을 모두 만족해야 강제 launch 가 일어남 (chat.py:2815-2823):
-
_workspace_has_artifacts(working_dir)— query workspace 에 실제 산출물이 있어야 함 (빈 대화는 skip) - 현재 tracked_agents 에 실패/취소가 아닌 remember subagent 가 이미 있으면 skip — 중복 launch 방지
- 위 두 조건을 통과하면 이벤트 로그에
🧠 Artifacts detected. Forcing a remember subagent...를 남김
chat.py:2831-2848 에서 Main Agent 에게 직접 HumanMessage 를 주입 한다:
agent.invoke(
{
"messages": [
HumanMessage(
content=(
"The current user turn produced durable workspace artifacts. "
"You must now launch exactly one async `remember` subagent with `start_async_task`. "
"Its job is to inspect the current query workspace, nominate up to 10 files worth long-term memory, "
"assign each file to the correct memory layer (user/profile, project/context, or domain/knowledge), "
"explain WHY each file belongs in that layer, and draft a `suggested_memory_content` for each. "
"It MUST emit its result as a single fenced JSON block named `recommendations` so the "
"Human-in-the-Loop UI can parse it. Do not finalize the user turn yet."
)
)
]
},
config=config,
)즉 Main Agent 는 "remember 를 띄워라" 라는 명시적 지시를 받고, 이 지시를 따르기 위해 start_async_task(subagent_type="remember") 를 호출 한다. 그 호출이 LazyAsyncSubagentsMiddleware 에 가로채지면서 remember 프로세스가 spawn 되고, 실제 작업이 시작된다.
📎 이 "강제 launch" 는 system prompt 가 아니라 런타임 주입이다. Main Agent 가 자발적으로 remember 를 띄울 때까지 기다리지 않고, 조건이 맞으면 WebUI 가 직접 HumanMessage 를 넣어서 "시작해" 라고 지시한다. 이 설계가 HITL 의 결정권을 에이전트가 아니라 런타임(우리)에게 남겨둔다.
구현: src/coding_agent/async_subagent_manager.py:108-147
Remember SubAgent 는 DEFAULT_ASYNC_SUBAGENTS["remember"] 에 정의된 system prompt 를 그대로 사용한다. 핵심 규칙은 다음과 같다 (코드에서 발췌):
You are a memory curation specialist preparing a Human-in-the-Loop review.
Inspect the final project artifacts in the current working directory and
select up to 10 files that are most valuable for future reuse or recall.
For EACH selected file you must decide:
1. Which long-term memory layer it belongs to:
- user/profile : user coding style, language preferences, conventions...
- project/context : project structure, architecture decisions, module boundaries...
- domain/knowledge : business rules, domain facts, API contracts, technical patterns...
2. A clear rationale (1-3 sentences) explaining WHY this file belongs in that layer.
3. A concise `suggested_memory_content` (<=800 chars) — the distilled note.
OUTPUT FORMAT (STRICT):
Emit ONE fenced JSON block as your final message, exactly in this shape:
```json
{
"recommendations": [
{
"path": "<relative path from workdir>",
"recommended_layer": "user/profile|project/context|domain/knowledge",
"rationale": "<why this file belongs in that layer>",
"suggested_memory_content": "<distilled durable note to store>"
}
]
}
Never omit the JSON block — the Human-in-the-Loop UI parses it to pre-fill the review form.
핵심 포인트:
- **최대 10개** — 상한선을 system prompt 에 박아둠
- **strict JSON format** — UI 가 파싱하기 쉬우라고 구조를 고정
- **fallback 지시**: "JSON 블록 뒤에 사람이 읽기 편한 요약도 추가해라" — 파싱 실패해도 사람이 직접 볼 수 있게
- **suggested_memory_content ≤ 800 chars** — 파일 원문 복사가 아니라 **축약 노트** 를 쓰도록 명시
## 4. [2] 후보 파싱
구현: `src/coding_agent/webui/_pages/chat.py:402` `_parse_remember_candidates_from_history()`
### 4.1 입력
- `subagent_history`: 현재 턴의 tracked_agents 리스트 — 각 row 는 type/status/result_summary/live_output 을 담은 dict
- `workdir`: query workspace 경로 (파일 존재 여부 확인용)
- `limit`: 기본 10
### 4.2 처리 단계
**Step 1 — remember row 필터링** (`chat.py:406-412`):
```python
remember_rows = [
row for row in subagent_history
if str(row.get("type", "")).lower() == "remember"
and str(row.get("status", "") or row.get("durable_state", "")).lower() == "completed"
]
type=="remember" 이면서 completed 상태인 row 만 남긴다. 마지막 1개만 사용 (remember_rows[-1]).
Step 2 — JSON 블록 추출 (chat.py:433):
parsed = _extract_remember_json_block(text)_extract_remember_json_block 은 텍스트에서 {"recommendations": [...]} 를 찾는 함수다. recommendations 키가 존재하면 json.loads 로 dict 반환, 실패하면 None.
Step 3 — 정규화 (chat.py:434-454):
- 각 recommendation 의
path는./접두사 제거 -
seenset 으로 중복 제거 -
_normalize_remember_layer로 layer 문자열을 3개 중 하나로 강제 (잘못된 값 →project/context로 떨어짐) -
_augment헬퍼가 파일 존재 여부 + 크기를 계산해서 payload 에 추가
결과 dict 형태:
{
"path": "src/coding_agent/agent.py",
"bytes": 12345,
"source": "remember_subagent",
"reason": "<rationale>",
"recommended_layer": "project/context",
"rationale": "<rationale>",
"suggested_memory_content": "<distilled note>",
}Step 4 — Legacy fallback (chat.py:457-476):
JSON 추출이 실패하면 라인 단위로 정규식 ([A-Za-z0-9_./-]+\.(?:md|txt|py|ts|tsx|js|jsx|json|ya?ml)) 로 경로를 추출한다. 이 경우 layer 정보가 없으므로 기본값 project/context 로 떨어지고, suggested_memory_content 는 빈 문자열. 사용자가 폼에서 직접 편집해야 한다.
왜 fallback 이 필요한가: remember SubAgent 의 모델이 작거나 지시를 어겨서 JSON 형식을 깨뜨릴 수 있다. 그래도 최소한 파일 경로 목록은 뽑아내서 HITL 을 계속 진행할 수 있도록 설계됨.
구현: src/coding_agent/webui/_pages/chat.py:2795 _pause_for_human_review_if_needed()
def _pause_for_human_review_if_needed() -> bool:
if st.session_state.get("_pending_human_review"):
return True # 이미 pause 중
if st.session_state.get("_human_review_resolution") is not None:
return False # 이미 resolution 됨 → 통과
candidates = _parse_remember_candidates_from_history(tracked_agents, working_dir)
if not candidates:
return False # 후보 없음 → 통과
st.session_state["_pending_human_review"] = {
"status": "pending",
"workdir": working_dir,
"candidates": candidates,
}
st.session_state["_hitl_scroll_pending"] = True # 스크롤 플래그
st.session_state["_monitor_async_after_answer"] = True # 재진입 플래그
_sync_live_turn_state(working=True)
st.rerun() # Streamlit 리렌더 → 루프 탈출
return TrueStreamlit 에서 st.rerun() 은 현재 스크립트 실행을 멈추고 처음부터 다시 돌린다. 이 프로젝트에서는 이것을 세션을 얼리는 메커니즘으로 쓴다:
-
_stream_response의 루프 안에서_pause_for_human_review_if_needed()가 호출됨 - 함수가 session state 에
_pending_human_review를 세팅하고st.rerun()호출 - Streamlit 이 페이지 전체를 다시 렌더함
- 리렌더된 페이지는
st.session_state.get("_pending_human_review")가 세팅되어 있는 걸 보고, 스트리밍 루프에 들어가는 대신 HITL 카드를 렌더링 함 (chat.py:4192근처) - 사용자가 폼을 제출할 때까지 계속 그 상태로 멈춰있음
이 방식의 장점:
- LangGraph checkpointer 의 interrupt 메커니즘을 쓰지 않아도 됨 (로컬 split topology 에서 복잡도 낮춤)
- Session state 가 진실의 원천 — 새로고침해도
_pending_human_review가 살아있어서 재개 가능 - 디버그가 쉬움: pause 중인 상태를
st.session_state에서 직접 확인
구현: src/coding_agent/webui/_pages/chat.py:641 _render_live_remember_review() → chat.py:502 _render_remember_review_form()
_render_live_remember_review 는 먼저 HITL 안내 카드를 그린다 (chat.py:648-657):
<div class='hitl-review-card'>
<div class='hitl-review-title'>Action Required · Human In The Loop</div>
<div class='hitl-review-text'>The remember subagent paused this turn for approval.
It grouped nominated files by memory layer and explained why each one matters.
Review the layer, rationale, and proposed memory note per file — edit anything you want —
then approve to persist them. The final Main Agent answer is held until this decision is made.</div>
</div>그 다음 스크롤 앵커 <div id='hitl-remember-review-anchor'></div> 를 박고, _hitl_scroll_pending 플래그가 있으면 JavaScript 로 스크롤을 카드 위치로 강제 이동시킨다 (chat.py:659-674).
_render_remember_review_form 의 chat.py:519-561 부분:
grouped: dict[str, list[dict]] = {layer: [] for layer in REMEMBER_LAYERS}
for row in candidates:
layer = _normalize_remember_layer(row.get("recommended_layer"))
grouped.setdefault(layer, []).append(row)
for layer in REMEMBER_LAYERS:
rows = grouped.get(layer) or []
if not rows:
continue
with st.expander(f"{REMEMBER_LAYER_BADGE[layer]} · {len(rows)} file(s)", expanded=True):
st.caption(REMEMBER_LAYER_GUIDE[layer])
for row in rows:
# 파일명, rationale, suggested memory note, 다운로드 버튼3개 계층별로 expander 를 열고, 각 파일에 대해:
- 파일 경로 (굵게)
- rationale (회색 작은 글씨)
- suggested memory note (회색 배경 박스)
-
Download file버튼 — 원본 파일을 그대로 다운받을 수 있음
이 영역은 읽기 전용 요약 이다. 편집은 밑의 폼에서 한다.
chat.py:568-622 가 실제 st.form(form_key) 블록이다. 3개 파트로 구성:
Part 1 — multi-select: 어떤 파일을 실제로 저장할지 선택 (chat.py:570-581)
selected_paths = st.multiselect(
"Files to store in long-term memory",
options=[row["path"] for row in candidates],
default=default_selection,
format_func=lambda p: ... # layer 배지 + 경로
)Part 2 — 파일별 편집 expander (chat.py:584-622)
각 후보 파일마다 expander 를 만들고 세 가지 편집 필드를 제공:
for row in candidates:
rel = str(row.get("path", ""))
recommended_layer = _normalize_remember_layer(row.get("recommended_layer"))
suggested = str(row.get("suggested_memory_content", "") or "").strip()
saved = default_edits.get(rel, {})
with st.expander(f"✏️ {rel}", expanded=False):
# 1. Layer override (selectbox)
layer_value = st.selectbox(
"Memory layer (override if needed)",
options=list(REMEMBER_LAYERS),
index=list(REMEMBER_LAYERS).index(
_normalize_remember_layer(saved.get("layer", recommended_layer))
),
key=f"{form_key}_layer_{rel}",
)
# 2. Rationale (text_area)
rationale_value = st.text_area(
"Rationale (why store this)",
value=str(saved.get("rationale", row.get("rationale", "") or row.get("reason", ""))),
height=70,
key=f"{form_key}_rationale_{rel}",
)
# 3. Memory note (text_area) — 이게 실제로 저장될 본문
content_value = st.text_area(
"Memory note to store (edit freely — this is what will be saved)",
value=str(saved.get("content", suggested)),
height=140,
key=f"{form_key}_content_{rel}",
help="Leave empty to fall back to a trimmed copy of the file contents. "
"Otherwise this exact text will become the durable memory record.",
)
edits[rel] = {
"layer": layer_value,
"rationale": rationale_value,
"content": content_value,
}Part 3 — Approve / Reject 버튼 (chat.py:624-632)
if show_reject:
approve_col, reject_col = st.columns(2)
with approve_col:
approve = st.form_submit_button("Approve and Continue", use_container_width=True)
with reject_col:
reject = st.form_submit_button("Reject and Continue", use_container_width=True)chat.py:634-638:
if approve:
return {"action": "approve", "selected_paths": list(selected_paths), "edits": edits}
if reject:
return {"action": "reject", "selected_paths": list(selected_paths), "edits": edits}
return None폼 제출 전에는 None 을 반환하므로, 상위 _render_live_remember_review 는 그냥 카드를 계속 보여주는 상태로 머문다.
구현: src/coding_agent/webui/_pages/chat.py:728 _store_approved_memory_files()
_render_live_remember_review 의 chat.py:694-707:
if result["action"] == "approve":
stored_ids = _store_approved_memory_files(
workdir,
result["selected_paths"],
edits=result["edits"],
)
st.session_state["_human_review_resolution"] = {
"approved": True,
"selected_paths": result["selected_paths"],
"edits": result["edits"],
"stored_ids": stored_ids,
}
st.session_state.pop("_pending_human_review", None)
st.rerun()Approve 가 눌리면 바로 _store_approved_memory_files 가 호출되고, 반환된 stored_ids 가 resolution 에 담긴다. 그 다음 _pending_human_review 를 삭제하고 rerun → 세션이 재개된다.
chat.py:728-801. 핵심 흐름:
Step A — 준비 (chat.py:742-745)
root = Path(workdir).expanduser().resolve()
ltm = LongTermMemoryMiddleware(memory_dir=str(settings.memory_dir))
edits = edits or {}
stored_ids: list[str] = []LongTermMemoryMiddleware 를 새로 인스턴스화해서 ltm._state_store (SQLite) 와 ltm.store (Chroma) 양쪽 접근을 얻는다.
Step B — 파일당 10개 한도 루프 (chat.py:747)
for rel_path in selected_paths[:10]:상한선이 여기서도 10으로 강제된다.
Step C — 계층 결정 (chat.py:749-753)
per_file = edits.get(rel_path, {}) if isinstance(edits, dict) else {}
file_layer = _normalize_remember_layer(
per_file.get("layer") or layer or "project/context"
)
category = _remember_layer_to_category(file_layer)우선순위: per-file edit → 전역 layer 파라미터(legacy) → 기본값 project/context. _remember_layer_to_category 는 레이어 문자열을 MemoryCategory enum 으로 매핑 (chat.py:719-725):
def _remember_layer_to_category(layer: str) -> MemoryCategory:
mapping = {
"project/context": MemoryCategory.PROJECT_CONTEXT,
"domain/knowledge": MemoryCategory.DOMAIN_KNOWLEDGE,
"user/profile": MemoryCategory.USER_PREFERENCES,
}
return mapping[layer]Step D — 본문 결정 (chat.py:754-766)
rationale = str(per_file.get("rationale", "") or "").strip()
human_content = str(per_file.get("content", "") or "").strip()
file_text = ""
if path.exists() and path.is_file():
try:
file_text = path.read_text(encoding="utf-8", errors="ignore")
except Exception:
file_text = ""
memory_body = human_content or file_text[:15000]
if not memory_body:
continue우선순위: 사람이 편집한 content → 파일 원문 선두 15000자. 양쪽 모두 비어있으면 이 파일은 스킵.
Step E — Durable payload 포맷 (chat.py:768-776)
durable_sections = [
"[remember_agent]",
f"file: {rel_path}",
f"layer: {file_layer}",
]
if rationale:
durable_sections.append(f"rationale: {rationale}")
durable_sections.extend(["", memory_body])
durable_payload = "\n".join(durable_sections)SQLite 에 저장될 문자열은 메타 헤더 + 본문 구조:
[remember_agent]
file: src/coding_agent/agent.py
layer: project/context
rationale: Main Agent 조립의 핵심 파일
<memory_body>
Step F — 이중 저장 (chat.py:782-800)
tags = ["remember_agent", rel_path]
if human_content:
tags.append("human_edited")
record_id = ltm._state_store.store_memory( # → SQLite
layer=file_layer,
content=durable_payload,
scope_key=root.name,
source="remember_agent_human_approved",
tags=tags,
)
ltm.store.store( # → ChromaDB
memory_body,
category,
{
"source": "remember_agent_human_approved",
"path": rel_path,
"layer": file_layer,
"rationale": rationale,
"human_edited": "1" if human_content else "0",
},
)
stored_ids.append(record_id)핵심 디테일:
-
human_edited태그: 사람이 note 를 편집했는지(SQLite tags) / 편집 여부 bool(Chroma metadata) 양쪽에 기록. 나중에 자동 저장 vs 사람 편집을 구분 가능. - SQLite 에는 durable_payload (메타 포함 전체) 를 저장 — 진실의 원천
- Chroma 에는 memory_body (본문만) 를 저장 — semantic search 할 때 메타 노이즈가 끼지 않음
-
scope_key=root.name— query workspace 디렉터리 이름을 scope 로 사용, 나중에 이 질의에서 저장된 기억만 필터링 가능
chat.py:708-716 에서:
else:
st.session_state["_human_review_resolution"] = {
"approved": False,
"selected_paths": [],
"edits": result["edits"],
"stored_ids": [],
}
st.session_state.pop("_pending_human_review", None)
st.rerun()Reject 는 저장 자체를 하지 않는다 — 단지 resolution 만 남기고 세션이 재개됨. edits 는 그대로 담아서 나중에 감사 로그로 볼 수 있게 남김.
구현: src/coding_agent/webui/_pages/chat.py:3391 (_resume_async_monitoring 내부)
HITL 이 끝나면 st.rerun() 으로 페이지가 다시 로드되고, 이번에는 _pending_human_review 가 없으므로 정상 경로로 진입한다. 그 시점에서:
human_review = st.session_state.pop("_human_review_resolution", None) # 소비
human_review_note = ""
if human_review:
human_review_note = (
"\n\nHuman review resolution:\n"
f"- approved={human_review.get('approved')}\n"
f"- layer={human_review.get('target_layer', '')}\n"
f"- selected_paths={human_review.get('selected_paths', [])}\n"
f"- stored_ids={human_review.get('stored_ids', [])}\n"
)이 human_review_note 는 Main Agent 에게 보낼 aggregation followup HumanMessage 에 합쳐진다 (chat.py:3403-3410):
followup = (
"All async subagent tasks from this user turn should now be finished. "
"Below is the completed SubAgent result ledger gathered by the WebUI runtime. "
"Use it as the primary aggregation source, ...\n\n"
f"{completed_report}{human_review_note}\n\n"
"then produce one final synthesized answer for the user. "
"Do not launch new async tasks unless absolutely required."
)
try:
loop_guard.reset()
result = agent.invoke(
{"messages": [HumanMessage(content=followup)]},
config=config,
)즉, Main Agent 는 "사람이 뭘 승인했고 어떤 record_id 로 저장됐는지" 까지 알고 최종 답변을 생성한다. 이 정보는 답변 본문에 "이러저러한 파일을 장기 메모리에 저장했습니다" 같은 문장으로 자연스럽게 들어간다. 사람은 "저장 승인" 버튼을 한 번만 눌렀지만, 에이전트는 그 결과를 자기 컨텍스트에 재주입받아 응답에 반영한다.
st.session_state 의 두 키가 HITL 상태 머신의 전부다:
| 상태 | _pending_human_review |
_human_review_resolution |
의미 |
|---|---|---|---|
| idle | None |
None |
HITL 관여 없음 (정상 스트리밍) |
| pending | {status, workdir, candidates} |
None |
폼 렌더링 중, 세션 얼려있음 |
| resolved (approve) | None |
{approved:True, stored_ids:[...]} |
저장 완료, Main Agent 재진입 대기 |
| resolved (reject) | None |
{approved:False, stored_ids:[]} |
사용자 거절, Main Agent 재진입 대기 |
| consumed | None |
None |
Main Agent 가 aggregation 에서 resolution 을 소비 |
전이는 chat.py 의 3곳에서만 일어남:
-
idle → pending:
_pause_for_human_review_if_needed()(chat.py:2803-2808) -
pending → resolved:
_render_live_remember_review()의 approve/reject 분기 (chat.py:700-716) -
resolved → consumed:
_resume_async_monitoring의 pop (chat.py:3391)
-
에이전트가 자발적으로 멈추지 않는다 —
_maybe_force_remember_subagent가 런타임에서 "지금 remember 돌려" 라고 직접 주입. 에이전트가 "memory 저장을 건너뛰어도 되나?" 를 판단하지 않음. -
사람의 편집이 진짜로 반영된다 —
_store_approved_memory_files에서human_content or file_text[:15000]우선순위가 바로 그 약속. 비어두면 파일 원문으로 fallback 되므로 사용자 입장에서 부담 없음. -
human_edited태그가 저장소에 박힘 — 나중에 "자동 저장된 기억" vs "사람이 큐레이션한 기억" 을 구분 가능. 이게 있어야 정정(correction) 정책을 층위별로 다르게 할 수 있음. -
Main Agent 가 resolution 을 재주입받음 — aggregation followup 에
human_review_note가 포함되므로 최종 답변이 "사용자가 뭘 승인했는지" 를 반영함. - Streamlit rerun 이 pause 메커니즘으로 쓰임 — LangGraph checkpointer interrupt 없이도 세션을 얼릴 수 있고, 새로고침에도 살아남음.
| 파일:라인 | 심볼 | 역할 |
|---|---|---|
async_subagent_manager.py:108-147 |
DEFAULT_ASYNC_SUBAGENTS["remember"] |
Remember SubAgent system prompt (JSON 포맷 강제) |
webui/_pages/chat.py:402 |
_parse_remember_candidates_from_history |
remember 출력을 파싱해서 후보 목록으로 |
webui/_pages/chat.py:2814 |
_maybe_force_remember_subagent |
조건 맞으면 Main Agent 에게 HumanMessage 주입해서 강제 launch |
webui/_pages/chat.py:2795 |
_pause_for_human_review_if_needed |
session state 세팅 + st.rerun() 으로 세션 얼림 |
webui/_pages/chat.py:641 |
_render_live_remember_review |
HITL 카드 + 스크롤 앵커 + 폼 호출 |
webui/_pages/chat.py:502 |
_render_remember_review_form |
계층별 그룹 요약 + st.form 편집 폼 + Approve/Reject |
webui/_pages/chat.py:728 |
_store_approved_memory_files |
SQLite + Chroma 이중 저장, human_edited 태그 |
webui/_pages/chat.py:3391 |
_resume_async_monitoring 내부 |
resolution 을 followup HumanMessage 에 포함해서 Main Agent 재호출 |
webui/_pages/chat.py:719-725 |
_remember_layer_to_category |
"project/context" 등 문자열 → MemoryCategory enum |
Memory and HITL 와 교차 참조해서 읽으면 "memory 저장의 입구 (HITL) → 저장소 구조 (3계층 + 이중 저장) → 자동 주입 (middleware)" 의 전체 체계가 보인다.
Danny's Coding AI Agent
- Home
- Architecture
- SubAgents
- Memory and HITL
- Resilience and Models
- Installation
- Testing and Development
- File Reference
External