338 lines
17 KiB
Markdown
338 lines
17 KiB
Markdown
## 2026-03-27 LM Studio `input required` 오류 수정 계획
|
|
|
|
### 문제 요약
|
|
- LM Studio 서버로 `POST /api/v1/chat` 호출 시 `{"error":{"message":"'input' is required"}}` 400이 발생.
|
|
- 현재 `ai_planner.py`는 `messages` 기반 payload만 전송하고 있어, `input` 기반 스키마를 요구하는 서버와 비호환.
|
|
|
|
### 구현 계획
|
|
1. `ai_planner.py` 요청 payload를 함수로 분리해 `messages` + `input`을 함께 포함하도록 수정한다.
|
|
2. 응답 파싱을 LM Studio/OpenAI 호환 형태(`message.content` 우선, `output_text` 대안)까지 처리한다.
|
|
3. 테스트를 먼저 추가해 payload에 `input`이 포함되는지, 응답 추출이 대체 경로에서 동작하는지 검증한다.
|
|
4. 문서(`README.md`)에 LM Studio 호환 동작을 반영한다.
|
|
|
|
### 검증 계획
|
|
- `pytest tests/test_ai_planner_lmstudio_compat.py -q`
|
|
- 필요 시 전체 관련 테스트 재실행
|
|
|
|
---
|
|
|
|
## 2026-03-27 LM Studio `unrecognized_keys` 400 추가 수정 계획
|
|
|
|
### 문제 요약
|
|
- `input`을 추가한 뒤에도 LM Studio 서버가 `messages`, `format`, `think`, `options` 키를 거부하며 400을 반환.
|
|
- 즉, 서버 모드에 따라 허용 스키마가 다르며 단일 payload로는 호환이 불안정.
|
|
|
|
### 구현 계획
|
|
1. `ai_planner.py`에서 payload를 단일 형태로 고정하지 않고, `legacy chat`와 `input-only` 후보를 순차 시도한다.
|
|
2. 400 응답일 때 에러 메시지를 파싱해 다음 후보 payload로 자동 재시도한다.
|
|
3. README 설명을 “messages+input 동시 전송”에서 “서버 스키마 자동 호환 재시도”로 정정한다.
|
|
|
|
### 검증 계획
|
|
- `python -m py_compile ai_planner.py`
|
|
- LM Studio 로그에서 `unrecognized_keys`가 사라지고 정상 응답으로 전환되는지 확인
|
|
|
|
---
|
|
|
|
## 2026-03-27 LM Studio `output.reasoning` 파싱/잘림 대응 계획
|
|
|
|
### 문제 요약
|
|
- `input-only` 재시도는 성공하지만, 응답이 `output[0].type=reasoning` + `content` 문자열로 돌아와 기존 파서가 텍스트를 추출하지 못함.
|
|
- 또한 서버 컨텍스트(4096)에서 장문 추론으로 출력 토큰이 소진되어 JSON 본문이 잘리는 케이스가 발생.
|
|
|
|
### 구현 계획
|
|
1. `ai_planner.py`의 input-only payload는 긴 시스템 프롬프트 대신 압축 프롬프트를 사용해 토큰 사용량을 줄인다.
|
|
2. 응답 파서를 `output[].content`가 문자열인 경우까지 지원한다.
|
|
3. 텍스트에 JSON이 섞여 있을 때 `{...}` 구간을 복구 파싱하는 보조 로직을 추가한다.
|
|
4. README에 LM Studio input-only 모드와 파싱/폴백 동작을 명시한다.
|
|
|
|
### 검증 계획
|
|
- `python -m py_compile ai_planner.py`
|
|
- 실제 실행 로그에서 `응답에서 텍스트 콘텐츠를 찾지 못했습니다`가 사라지는지 확인
|
|
|
|
---
|
|
|
|
## 채굴 시 mining_state 반복 설정 제거 (우클릭 유지)
|
|
|
|
### 문제
|
|
- `mine_resource` 내부에서 약 0.1초마다 `update_selected_entity` + `mining_state = { mining = true, ... }`를 RCON으로 재전송하고 있었음.
|
|
- 플레이어는 보통 우클릭을 **누른 채 유지**하는데, 스크립트는 매번 상태를 다시 써서 **뗐다 누르는(re-trigger)** 것에 가까운 동작이 됨.
|
|
|
|
### 변경
|
|
- 이동·근처 광물 확인 후, 채굴 루프 **진입 직전에 한 번만** `mining_state` 설정.
|
|
- 루프에서는 `sleep`과 인벤토리/채굴 진행률 **읽기만** 수행. 종료·목표 달성 시에만 `mining = false`.
|
|
|
|
### 범위
|
|
- `action_executor.py` — `mine_resource`
|
|
- `README.md` — 동작 설명 한 줄 보강
|
|
|
|
---
|
|
|
|
## 채굴 실패 시 제외 좌표 반복 버그 수정 계획
|
|
|
|
### 문제 재현/관찰
|
|
- `mine_resource`에서 실패한 타일을 `failed_positions`에 추가한 뒤 Lua에 `exclude` 테이블로 전달하지만,
|
|
다음 시도에서도 동일 좌표(예: `388,2`)로 다시 이동하는 로그가 발생합니다.
|
|
|
|
### 원인 후보
|
|
- Lua에서 제외 판정에 쓰는 좌표 키가 `string.format("%.0f,%.0f", ...)` 기반인 반면,
|
|
Python에서 `exclude["{fx:.0f},{fy:.0f}"]`를 만들 때 반올림/절삭 방식이 Lua와 1:1로 일치하지 않는 케이스가 있을 수 있습니다.
|
|
- 이 경우 `exclude[key]`가 항상 false가 되어, Lua가 “가장 가까운 광석”을 계속 같은 엔티티로 반환할 수 있습니다.
|
|
|
|
### 변경 목표
|
|
1. `failed_positions`를 “Lua 키 생성과 동일한 정수 타일 좌표(tx, ty)”로만 저장합니다.
|
|
2. Lua에서 후보 광석 엔티티를 검사할 때도 정수 타일 좌표를 계산해 키/반환/마이닝 좌표에 일관되게 사용합니다.
|
|
3. 그 결과, 제외한 좌표는 다음 루프에서 절대로 다시 선택되지 않도록 보장합니다.
|
|
|
|
### 구현 범위
|
|
- `action_executor.py`
|
|
- `mine_resource` 내부에서 좌표 처리 로직을 정수 타일 기반으로 통일
|
|
- Lua 반환값을 `tx,ty` 정수 문자열로 변경하고 Python 파싱을 이에 맞춤
|
|
|
|
### README 업데이트 계획
|
|
- 채굴 제외(exclude) 로직이 “정수 타일 키 기반으로 통일”되도록 README의 기술/동작 설명(또는 체크리스트)을 업데이트합니다.
|
|
|
|
---
|
|
|
|
## 자원 패치 중심 오차로 인한 채굴 실패 완화 계획
|
|
|
|
### 문제 관찰
|
|
- `scan_resources()`가 패치의 평균 중심(`center_x/y`)을 추천 좌표로 사용하면서,
|
|
플레이어가 해당 좌표로 이동한 직후 `mine_resource`의 엔티티 탐색 반경 안에서
|
|
실제 자원 엔티티를 찾지 못해 실패하는 케이스가 발생할 수 있음.
|
|
|
|
### 근거(팩토리오 Lua API)
|
|
- `LuaInventory.get_contents()`는 `dictionary[string -> uint]`를 반환하므로,
|
|
인벤토리 판독은 `name -> count` 형태로 처리하는 것이 맞다.
|
|
- 자원 엔티티의 `e.position`은 부동소수(MapPosition) 좌표이므로,
|
|
타일 중심/평균 중심과 실제 엔티티 좌표 간 오차가 생길 수 있다.
|
|
|
|
### 변경 목표
|
|
1. `scan_resources()` 결과에 패치 대표 좌표를 평균 중심뿐 아니라
|
|
플레이어 기준 가장 가까운 실제 엔티티 좌표(앵커 `anchor_x/y` 및 `anchor_tile_x/y`)로 함께 제공
|
|
2. `summarize_for_ai()`에서는 평균 중심 대신 앵커 좌표 기반으로 거리/추천을 계산
|
|
3. `mine_resource` 후보 엔티티 탐색 반경을 더 크게 잡아(행동 레벨) 이동 직후 실패를 흡수
|
|
|
|
### 구현 범위
|
|
- `state_reader.py`
|
|
- `scan_resources()`에 `anchor_x/y` 및 `anchor_tile_x/y` 추가
|
|
- `summarize_for_ai()` 거리 계산을 앵커 우선으로 변경
|
|
- `action_executor.py`
|
|
- `mine_resource`에서 후보 엔티티 탐색 반경을 `80` → `250`으로 확장
|
|
|
|
---
|
|
|
|
## GLM 응답 지연 원인 계측/완화 계획
|
|
|
|
### 문제 관찰
|
|
- `ai_planner.py`에서 GLM 호출이 `[GLM] 생각 중...` 이후 30초~1분 이상 지연되는 현상 발생
|
|
- JSON 파싱 실패로 인한 재시도는 가끔 발생(하지만 느림의 주 원인이 아닐 가능성도 있음)
|
|
|
|
### 1단계(증거 수집)
|
|
- `ai_planner.py`의 `_call_glm()`에 타이밍 로그 추가
|
|
- `total`: 요청 시작~콘텐츠 반환까지 전체 소요
|
|
- `http_read`: HTTP 응답 본문 수신까지 소요
|
|
- `json_parse`: 응답 JSON 파싱 시간
|
|
- `prompt_chars/system_chars/max_tokens`: 입력 크기 동시 기록
|
|
|
|
### 2단계(완화: 로그 확인 후)
|
|
- `json_parse`가 아니라 `http_read/total`이 큰 경우:
|
|
- `max_tokens`를 우선 크게 줄여 생성 길이 상한 조정
|
|
- `SYSTEM_PROMPT`/`state_summary` 길이를 더 강하게 제한
|
|
- `json_parse`가 큰 경우:
|
|
- JSON 파싱/복구 로직 비용 또는 응답 형식 편차 원인 재점검
|
|
|
|
### README 업데이트
|
|
- `README.md`의 `주의사항`/`실행` 섹션에 GLM 지연 계측 로그 출력 방식 및 파라미터 조정 힌트를 반영
|
|
|
|
---
|
|
## Cursor Hooks - Windows `sessionStart` 앱 선택창 원인/해결 계획
|
|
### 문제 재현/관찰
|
|
- Cursor `@cursor.hooks` 로그에서 `sessionStart` 훅 실행 중 `./hooks/session-start`가 stdout JSON을 내지 못하거나, Windows에서 앱 선택창이 뜨는 현상이 관찰됨
|
|
|
|
### 원인 후보
|
|
- Windows에서 확장자 없는 `./hooks/session-start`를 실행할 때 `bash`로 해석되지 않아, OS가 실행 대신 “어떤 앱으로 열지”를 요청할 수 있음
|
|
|
|
### 변경 목표
|
|
1. 프로젝트 훅은 `powershell`로 stdout에 JSON을 출력하도록 고정
|
|
2. superpowers 플러그인의 Cursor 훅은 `bash`로 감싸 실행되도록 구성 변경
|
|
|
|
### 구현/검증 범위
|
|
- `E:/develop/factorio-ai-agent/.cursor/hooks.json`
|
|
- `E:/develop/factorio-ai-agent/.cursor/session-start-hook.ps1`
|
|
- `.../superpowers/.../hooks/hooks-cursor.json`
|
|
- 검증: Cursor 재시작 후 `View -> Output -> Hooks`에서 sessionStart 훅이 정상 파싱되어 `OUTPUT`에 JSON이 나타나는지 확인
|
|
|
|
---
|
|
|
|
## 인벤토리 캐시(메모리) 추가 계획
|
|
|
|
### 목표
|
|
- 파이썬 에이전트만 종료/재실행할 때 `RCON`으로 인벤토리를 읽지 못하거나(`{}` 반환) 빈 값이 나오면,
|
|
직전에 성공적으로 읽은 인벤토리를 기억해서 프롬프트에 반영한다.
|
|
|
|
### 정책(캐시 fallback 조건 변경)
|
|
- `get_inventory()`에서 **JSON 파싱/출력 추출이 성공(parsed_ok=True)** 했으면, 결과가 `{}`라도 캐시로 덮지 않는다.
|
|
- 캐시를 덮어쓰면 실제 진행이 반영되지 않아 같은 행동을 반복하는 루프가 생길 수 있다.
|
|
- `get_inventory()`에서 **파싱이 실패(parsed_ok=False)** 하거나 예외가 발생한 경우에만 `inventory_memory.json` 캐시를 로드한다.
|
|
|
|
### 구현 범위
|
|
- `state_reader.py`
|
|
- `inventory_memory.json` 로드/저장 유틸 추가
|
|
- `get_inventory()` 반환값을 캐시 fallback으로 교체
|
|
- `README.md`
|
|
- 인벤토리 캐시 동작과 파일명(`inventory_memory.json`)을 설명
|
|
|
|
---
|
|
|
|
## 발견된 광맥 좌표 기억(메모리) 추가 계획
|
|
|
|
### 문제 관찰
|
|
- `explore`로 광맥을 발견해도, 재실행/재계획/초기화 상황에서 동일 좌표를 다시 추천받지 못해
|
|
불필요한 탐색이 반복될 수 있음.
|
|
|
|
### 변경 목표
|
|
1. 자원 엔티티(광맥)의 좌표를 `ore_patch_memory.json`에 ore 종류별로 여러 패치 좌표로 저장
|
|
2. 다음 상태 요약에서 “기억된 광맥” 섹션으로 AI에게 제공
|
|
3. LLM이 가능하면 기억된 좌표로 먼저 이동해 `move -> mine_resource`를 수행하도록 유도
|
|
|
|
### 구현 범위
|
|
- `action_executor.py`
|
|
- `explore` 성공 시 발견한 광맥 좌표를 파일에 갱신
|
|
- `state_reader.py`
|
|
- `scan_resources()` 결과로도 광맥 좌표를 `ore_patch_memory.json`에 갱신
|
|
- `summarize_for_ai()`에 “기억된 광맥” 출력 추가
|
|
- `ai_planner.py`
|
|
- SYSTEM_PROMPT에 기억된 광맥 우선 이동 가이드 추가
|
|
|
|
### README 업데이트
|
|
- `ore_patch_memory.json` 파일과 동작 방식 설명 추가
|
|
|
|
---
|
|
|
|
## 에이전트 재시작 시 마지막 행동 기억(메모리) 추가 계획
|
|
|
|
### 문제 관찰
|
|
- 코드를 수정하면 보통 에이전트를 재시작해야 하고,
|
|
이때 `ai_planner.py`의 `feedback_log`/직전 실패 정보가 초기화되어 같은 시행착오가 반복될 수 있음.
|
|
|
|
### 변경 목표
|
|
1. 재시작 시에도 “직전에 실행했던 action”과 결과(success/message)를 파일에 저장
|
|
2. 다음 실행의 상태 요약(`state_reader.summarize_for_ai()`)에 “기억된 마지막 행동” 섹션을 포함
|
|
3. LLM이 마지막 행동이 실패였다면 같은 행동을 즉시 반복하지 않도록 유도
|
|
|
|
### 저장 기준(확정)
|
|
- 성공/실패 상관없이 **가장 마지막으로 실행을 시도한 action 1개**만 저장한다.
|
|
|
|
### 구현 범위
|
|
- `main.py`
|
|
- 행동 실행 직후 action/result를 `agent_last_action_memory.json`에 저장
|
|
- `state_reader.py`
|
|
- 재시작 시 파일을 로드해 상태 요약에 포함
|
|
- `ai_planner.py`
|
|
- SYSTEM_PROMPT에 “마지막 행동 실패 재시도 금지/원인 해결 우선” 가이드 추가
|
|
|
|
### README 업데이트
|
|
- `agent_last_action_memory.json` 존재/동작 설명 추가
|
|
|
|
---
|
|
|
|
## `place_entity` BLOCKED 완화 계획 (인접 타일 탐색)
|
|
|
|
### 문제 관찰
|
|
- 로그에서 `stone-furnace` 배치가 `FAIL 배치 불가`(내부적으로 `BLOCKED`)로 반복됨
|
|
- 또한 `stone-furnace`가 이미 맵에 설치돼 있어도, executor가 먼저 `NO_ITEM`으로 종료해 재사용 탐색이 실행되지 않는 케이스가 관찰됨
|
|
- `mine_resource`가 자원 엔티티 근처(종종 자원 패치 타일)로 이동한 뒤, 같은 좌표에 건물을 배치하려고 하면
|
|
Factorio의 `can_place_entity` 조건에 걸려 막힐 수 있음
|
|
|
|
### 변경 목표
|
|
1. `action_executor.py`의 `place_entity`에서 배치 아이템 유무(`NO_ITEM`) 확인 전에,
|
|
먼저 `surface.find_entities_filtered`로 기존 엔티티(같은 `name`)가 있는지 탐색
|
|
2. (기존) 요청 좌표 주변 `±1 타일(9칸)` 후보에서 기존 엔티티가 있으면 `REUSED`로 성공 처리
|
|
3. (추가) 요청 좌표 중심으로 반경 `3` 타일 내에 같은 엔티티가 있으면 가장 가까운 것을 `REUSED`로 성공 처리
|
|
4. 기존 엔티티가 없을 때만 인벤토리에 `name` 아이템이 있는지 확인하고, `surface.can_place_entity` + `±1 타일` 후보로 실제 배치를 시도
|
|
5. 실제로 배치된/재사용된 좌표를 결과 메시지에 포함해 이후 `stone-furnace` 자동 부트스트랩이 좌표를 정확히 파싱하도록 보장
|
|
|
|
### 구현 범위
|
|
- `action_executor.py`
|
|
- 기존 엔티티 탐색을 `NO_ITEM` 체크보다 먼저 수행
|
|
- 재사용(`REUSED`) 성공 케이스의 메시지/좌표 포맷 확정
|
|
- 반경 3 타일의 넓은 재사용 탐색 추가
|
|
|
|
### README 업데이트
|
|
- `place_entity`가 `BLOCKED`/`NO_ITEM` 상황에서도 기존 엔티티를 찾아 `REUSED`로 재사용하고, `stone-furnace`는 재사용이어도 자동 부트스트랩이 동작하도록 설명 반영
|
|
|
|
---
|
|
|
|
## GLM HTTP read timeout 대응 계획 (예외 포함 재시도)
|
|
|
|
### 문제 관찰
|
|
- 로그에 `TimeoutError: The read operation timed out` 가 발생
|
|
- 현재 `ai_planner.py`는 JSON 파싱 실패만 재시도하고, 네트워크 타임아웃/연결 오류는 별도 재시도 경로가 약함
|
|
|
|
### 변경 목표
|
|
1. `AIPlanner.decide()`의 재시도 예외 범위를 `TimeoutError`, `ConnectionError`, `urllib.error.URLError`까지 확장
|
|
2. 해당 오류가 발생하면 동일한 “plan 응답 받기” 재시도로 복구 시도
|
|
3. 3회 연속 실패 시에는 상태 요약 기반 휴리스틱(광맥 있으면 채굴/이동, 없으면 explore 방향 순환)으로 폴백
|
|
|
|
### 구현 범위
|
|
- `ai_planner.py`
|
|
- `decide()`의 `except` 절 범위 확장 및 경고 로그 보강
|
|
|
|
### README 업데이트
|
|
- GLM read timeout/연결 오류 발생 시 재시도 동작을 `주의사항`에 추가
|
|
|
|
---
|
|
|
|
## GLM 전부 실패 시 explore 무한 루프 완화 (상태 기반 폴백)
|
|
|
|
### 문제 관찰
|
|
- GLM API가 `TimeoutError`/`ConnectionError`로 연속 실패하면 폴백이 항상 `explore`(고정 east)만 선택됨
|
|
- 이미 `주변 자원 패치`·`기억된 광맥`에 철/석탄 좌표가 있는데도 탐색만 반복되어 진행이 멈춤
|
|
|
|
### 변경 목표
|
|
1. `state_reader.summarize_for_ai()` 텍스트에서 플레이어 위치·앵커/기억 광맥 좌표를 정규식으로 추출
|
|
2. 광맥이 있으면 `mine_resource` 우선(거리가 크면 `move` 선행), 없을 때만 `explore` 방향 순환
|
|
3. 기본 HTTP 타임아웃 90초→120초, 재시도 간격 소폭 완화
|
|
|
|
### 구현 범위
|
|
- `ai_planner.py`: `_fallback_plan_from_summary`, `_parse_player_position`, `_parse_ore_anchors`, 환경변수 `GLM_HTTP_TIMEOUT_SECONDS`, `GLM_FALLBACK_MOVE_THRESHOLD`
|
|
|
|
### README 업데이트
|
|
- 폴백 동작 및 환경변수 설명 반영
|
|
|
|
---
|
|
|
|
## GLM 예외 상세 로그 + 연결 검사 스크립트
|
|
|
|
### 목표
|
|
- `TimeoutError`/`ConnectionError` 한 줄만으로는 DNS·SSL·프록시·HTTP 본문 오류를 구분하기 어려움
|
|
- `URLError.reason`·체인 예외·errno를 항상 출력하고, `GLM_DEBUG=1`에서 스택까지 확보
|
|
- `scripts/glm_connection_check.py`로 최소 POST만 수행해 네트워크/API 키를 분리 진단
|
|
|
|
### 구현
|
|
- `ai_planner.describe_glm_exception()`, `_glm_debug_enabled()`
|
|
- `scripts/glm_connection_check.py`
|
|
|
|
### README
|
|
- `GLM API 연결 문제 디버깅` 절 추가
|
|
|
|
---
|
|
## GLM 응답 형식 편차: JSON 객체 외(배열) 처리 강화
|
|
|
|
### 문제 관찰
|
|
- 로그에서 `ValueError: JSON 파싱 실패 ('{' 없음)`이 반복됨
|
|
- `_parse_json()`은 기본적으로 JSON 객체(`{...}`)를 기대하므로, GLM이 최상위에서 JSON 배열(`[...]`)로 응답하면 파싱 실패로 떨어질 수 있음
|
|
|
|
### 변경 목표
|
|
1. `_parse_json()`이 최상위 JSON 배열을 `actions` 리스트로 래핑해 정상 파이프라인으로 이어지게 함
|
|
2. 실패 시 원인 파악을 돕기 위해 `raw`의 첫 비공백 문자를 오류 메시지에 포함
|
|
3. `tests/test_ai_planner_parse_json.py`로 회귀 방지
|
|
|
|
### 구현 범위
|
|
- `ai_planner.py`
|
|
- `_parse_json()`에서 JSON `list`도 허용하고, `{' 없음`일 때 `[...]`를 균형 추출 후 래핑
|
|
- 실패 원인 로깅(첫 비공백 문자)
|
|
- `tests/`
|
|
- JSON 객체/배열 파서 단위 테스트 추가
|
|
|