Issue 01중국 AI
AC POST
중국 AI 목록
掘金2026년 11월 4일 22:05중국어 → 한국어

AI가 승인 없이 계속 실행되는 문제, 상태기계로 통제한다

한 번의 승인이 세션 전체 쓰기 권한이 되지 않도록 7단계 상태기계와 단일 턴 쓰기 임차로 권한을 회수하는 방식을 제시했다.

중국어 원문을 AI로 번역했습니다. 고유명사와 수치는 원문 표기를 우선하며, 중요한 판단에는 아래 출처 원문을 함께 확인하세요.

「1회 승인」에서 「단일 라운드 쓰기 임대」로: 장시간 작업 Agent에 만료되는 쓰기 권한을 한 겹 더한다.

프로젝트: Agent-LongRun-Framework v3 저장소: github.com/lss48655835…

내가 AI로 코드를 작성할 때 가장 두려운 것은 그것이 못 쓰는 게 아니라, 너무 적극적이라는 점이다.

당신이 20분 동안 그것과 요구사항을 맞추다가, 방금 「먼저 현재 구현을 좀 봐줘」라고 말했을 뿐인데, 그것은 이미 편집기를 열고 인증 계층을 리팩터링해 버렸다. 왜 그러냐고 물으면, 그것은 오히려 억울하다는 듯이 말한다. 「당신이 내가 바로 고쳐주길 바란다고 생각했어요.」

당신은 그것이 말을 듣지 않는다고 욕하지만, 문제는 대개 모델에 있지 않다. 문제는 당신의 Agent에게 「대화」와 「실행」의 경계가 전혀 없다는 데 있다.

그래서 나는 이후 Agent를 다루는 방식을 바꿔, 그것을 제품 관리자와对接하듯 대했다.

Agent의运作은 제품 관리자와对接하는 것과 같다. 대화 상태(읽기 전용): Agent와 입자를 맞춘다—요구사항을 명확히 하고, 해결책을 논의한다. 실행 상태(쓰기 가능): 해결책이 승인된 후의 작업 하달—문을 닫고 코드를 쓰며, 절대 함부로发散하지 않는다.

이 두 문장은 말하기는 자연스럽지만, 코드로 내려놓으면 아주 구체적인 질문 하나에 답해야 한다.

「해결책이 이미 승인되었다」는 사실을 대체 누가 기억하는가? 그리고 누가 언제 회수하는가?

만약 답이 그저 「prompt에 적어둔다」라면, 그것은 공학적 제약이 아니다. 모델 기분이 좋을 때는 지키겠지만, 컨텍스트가 길어지고, 도구가 많아지고, 호스트가 바뀌면 잊어버릴 수 있다. 진짜로 해결해야 할 문제는 이것이다. 「승인하지 않으면 쓸 수 없다」를 모델의 자각에 의존하지 않는 거부 지점으로 만드는 것.

이것이 Agent-LongRun-Framework v3의 출발점이다. 그것은 또 하나의 Agent 프레임워크가 아니며, LLM을 호출하지도 않고, 오케스트레이션 DSL도 없다. 그것은 아주 얇은 런타임 계약 한 겹만을 한다.

- 7개 상태의 작업 상태 머신;

- 대화 라운드에 따라 만료되는 쓰기 임대;

- 프로젝트 루트 디렉터리에 바인딩된 메모리 라우팅.

아래는 내가 왜 이렇게 설계했는지, 그리고 어디가 아직 잘 안 되어 있는지다.

一、세 가지 유형의 고장, 본질은 같은 문제다

장시간 작업 Agent의 고장은 표면적으로는 「모델이 말을 안 듣는다」로 보이지만, 공학적으로 내려놓으면 대개 세 가지 일에 계약이 없는 것이다.

1. 브레이크포인트가 「저장됨」이라고 말하지만, 사실 저장되지 않았다

이것은 아주 은밀한 가짜 영수증이다.

Agent는 컨텍스트가 거의 찰 때 즈음 브레이크포인트 한 토막을 출력한다.

[LongRun Checkpoint] 阶段: 인터페이스适配 下一步: 두 번째 공급자에 연결

사용자는 이 말을 보고 진행 상황이 이미 저장되었다고 생각하며 안심하고 창을 닫는다. 그런데 프로세스가 종료되면 채팅 기록이 유일한 사본이 된다. 다음에 복구할 때에야 비로소 메모리 저장소에 이 브레이크포인트가 전혀 없다는 것을 발견한다.

문제는 「브레이크포인트를 쓰느냐 마느냐」가 아니라, 쓰는 동작과 말하는 동작 중 누가 먼저인가에 있다. 만약 먼저 출력하고 나중에 저장소에 쓰면, 그 중간의 어떤 예외든 존재하는 것처럼 보이지만 실제로는 존재하지 않는 진행 상황을 만들어 낸다.

내 처리 방식은 아주 강경하다. 브레이크포인트는 반드시 먼저 메모리에 성공적으로 기록된 뒤에야 출력이 허용된다. 쓰기에 실패하면 출력하지 않고, 대신 로컬의 pending_checkpoint.json으로 떨어뜨려, 다음 복구 때 「여기에 미완료된 저장이 한 건 있다」는 것을 알게 한다. 브레이크포인트는 영수증이지 로그가 아니다. 영수증은 「저장됨」을 먼저 쓰고 나서 일을 처리할 수 없다.

2. 당신은 한 번만 승인했는데, 그것은 한 라운드 전체를 썼다

이것은 더 흔하고 더 위험한 유형이다.

v2의 enable_writes()는 지속 스위치다. 한 번 호출하면 작업이 writable에 들어가고, 이후 모든 라운드에서 쓸 수 있다. 사용자의 실제 의도는 대개 「이 동작 하나는 해도 된다」일 뿐인데, 코드는 그것을 「이 작업은 이제부터 전부 다 해도 된다」로 이해한다.

그래서 이런 대화가 생긴다.

사용자: A 파일의 타임아웃을 30초로 바꿔줘. Agent: 네, A를 수정했습니다. 사용자: 겸사겸사 B가 왜 오류를 내는지도 좀 봐줘. Agent: 겸사겸사 B, C, D도 리팩터링했어요.

Agent의 관점에서 그것은 어떤 지시도 위반하지 않았다. 쓰기 권한은 아직 있고, 작업은 아직 실행 상태에 있다. 그것은 그저 「1회 승인」을 「지속 인가」로 잘못 받아들였을 뿐이다.

하나의 승인은 얼마나 많은 작업량을 덮어야 하는가? 답은 「작업이 끝날 때까지」가 아니라, 「다음에 사용자 확인이 필요한 경계까지」여야 한다. 이것이 단일 라운드 쓰기 임대가 해결하려는 문제다.

3. 상태는 A에, 기억은 B에

세 번째 유형의 고장은 복구할 때에야 드러난다.

상태 파일은 ~/.longrun/ 에 있고, 메모리 저장소는 또 다른 전역 디렉터리에 있다. 또는 LongRunAgent는 프로젝트 A를 가리키는데, MCPMemoryBackend는 기본값인 프로젝트 B를 사용한다. resume()는 A의 next_step을 읽고, 다시 B의 이력에서 컨텍스트를 찾아, 마지막에는 아무도 알아보지 못하는 진행 상황을 짜깁기한다.

이런 문제는 happy path에서는 나타나지 않는다. 그것은 오직 기계를 바꾸고, 프로젝트를 전환하고, 캐시를 지우고, 새 프로세스를 열 때에만 나타나는데, 바로 그 몇 순간이야말로 당신이 브레이크포인트의 신뢰성을 가장 필요로 하는 때다.

그래서 v3는 「프로젝트 루트」를 유일한 바인딩으로 만든다.

<프로젝트 루트>/.longrun/state.json # 핫 상태 <프로젝트 루트>/memory.db # 콜드 메모리

상태와 기억은 반드시 같은 root를 가리켜야 한다. resume()는 기억을 읽지 못할 수는 있어도, 다른 프로젝트의 기억을 읽어서는 안 된다.

二、Prompt은 계약이 아니다

위의 세 가지 유형의 문제 모두 prompt에 「이렇게 하지 마라」 한 줄을 써넣을 수 있다. 그러나 prompt는 권고이지 거부가 아니다.

모델은 「좋아요」라고 말한 뒤 계속 할 수 있다. 컨텍스트는 잘릴 수 있다. 서로 다른 호스트가 도구 호출을 감싸는 방식도 다르다. 더 중요한 것은, prompt는 당신에게 테스트 가능한 실패 경로를 줄 수 없다는 점이다. 당신은 CI에서 「모델이 이번에는 반드시 거부할 것」이라고 단언할 수 없다.

나는 prompt가 쓸모없다고 말하는 게 아니다. 그것은 의도, 스타일, 우선순위를 표현하기에 적합하고, Agent에게 「우리가 무엇을 원하는지」를 알려주기에 적합하다. 그러나 아래의 것들은 코드가 보장해야 한다.

- 어떤 상태가 어떤 상태로 전이될 수 있는지;

- 쓰기 권한이 언제 무효화되는지;

- 브레이크포인트와 기억 중 누가 먼저 쓰이는지;

- 복구 시 어느 프로젝트의 데이터를 읽는지.

이것들은 런타임 계약이지, 프롬프트 기법이 아니다.

三、네 가지 불변량

v3의 설계는 네 가지 불변량으로 압축할 수 있다. 각각에는 대응하는 코드와 테스트가 있어, 자각에 의존하지 않는다.

I1: 먼저 기억을 조회하고, 그 다음 작업을 만든다

start_task()의 첫 번째 일은 TaskState를 생성하는 것이 아니라, 프로젝트 기억을 검색하는 것이다.

철칙 1: 작업을 시작하면 먼저 기억을 조회한다. terms = _normalize_keywords(keywords) or extract_keywords(task_desc) memories = self._search_for_task(terms) # 검색이 끝난 뒤에야 상태를 생성하고 디스크에 기록한다. state = TaskState(task_id=..., description=task_desc) state.transition(TaskStatus.IN_PROGRESS, "start_task" ) self.store.save(state)

테스트 test_start_task_searches_before_creating_state는 첫 번째 백엔드 호출이 반드시 search여야 한다고 직접 단언하며, "결국 무언가를 조회했는가"를 단언하지 않는다.

기억이 조회되지 않아 비어 있을 수는 있어도, 조회하지 않을 수는 없다. "먼저 작업을 만들고 나중에 기억을 보충"하는 것이 허용되는 순간, 이력 컨텍스트가 없는 새 작업이 생기고 이후의 모든 결정이 잘못된 공백 위에 세워지기 때문이다.

I2: 먼저 기억을 쓰고, 그다음에 중단점을 출력한다

checkpoint()는 "텍스트를 짜맞춘 뒤 출력하는" 것이 아니다. 그 순서는 다음과 같다:

- 상태 사본을 수정하여, 실패 시 메모리 안의 실제 상태를 오염시키지 않도록 한다;

- 현재 쓰기 임차(lease)를 회수한다;

- 중단점 텍스트와 기계 표지를 렌더링한다;

- save_memory() 를 호출한다;

- 4단계가 성공했을 때에만 state.json 을 쓰고 중단점을 반환한다.

핵심 코드는 다음과 같다:

try : record = self.memory.save( type = "session" , title=title, content=content, keywords= f"longrun,checkpoint, {working.task_id} " , ) except Exception as exc: fallback = self._write_fallback(working, kind, reason, summary, content) self._revoke_write_lease(state, "checkpoint write failed" ) raise CheckpointWriteError( "기억 쓰기에 실패하여 중단점을 출력하지 않았습니다(규칙: 반드시 먼저 기억을 쓰고, 그다음에 중단점을 출력해야 합니다)." f" {fallback} 에 임시 저장했으니, 기억 서비스를 복구한 뒤 checkpoint()를 다시 호출하십시오." ) from exc

여기서 주목할 만한 두 가지 세부 사항이 있다.

첫째, 실패할 때 수정하는 것은 state이지 working이 아니다. working은 메모리 안의 사본일 뿐 아직 디스크에 기록되지 않았기 때문이며, 실제로 회수해야 하는 것은 현재 살아 있는 작업 상태다.

둘째, 중단점에는 기계가 읽을 수 있는 표지 한 줄이 있다:

<!-- longrun-state: {"task_id":"T_...","status":"paused","access_mode":"read_only",...} -->

복구할 때 모델이 자연어에서 "지금이 paused인지 in_progress인지"를 추측할 필요 없이, 이 JSON 줄을 직접 파싱하여 상태를 재구성한다. 자연어는 사람이 보기 위한 것이고, 기계 표지는 프로그램이 보기 위한 것이며, 둘 다 같은 중단점 안에 있다.

여기에는 또한 저장소를 넘나드는 순서 문제가 관련된다: state.json은 로컬에 있고 기억은 SQLite에 있어, 둘을 하나의 데이터베이스 트랜잭션에 넣을 수 없다. 그렇다면 반드시 "무엇을 영수증의 증빙으로 삼을 것인가"를 골라야 한다. 나의 선택은 기억 쓰기 성공을 기준으로 삼고, 로컬 상태는 성공한 이후에만 진행시키는 것이다. 실패하면 로컬은 이전 상태를 유지하고, 별도로 pending 파일을 하나 기록해 둔다. 이렇게 하면 최악의 경우는 "로컬상으로는 아직 저장되지 않은 것처럼 보이지만 기억에는 이미 있는" 상태이며, 다음에 멱등하게 보충할 수 있다; "로컬상으로는 저장된 것처럼 보이지만 기억에는 아무것도 없는" 상태가 아니라.

테스트 test_checkpoint_saves_before_returning 과 test_checkpoint_write_failure_blocks_output 은 정상 경로와 실패 경로를 다룬다. 후자는 쓰기 실패 시 checkpoint()가 CheckpointWriteError를 발생시키고 중단점 텍스트를 반환하지 않는다고 단언한다.

I3: 쓰기 권한은 한 라운드만 적용된다

이것이 v3의 핵심 변화이며, 내가 가장 많은 시간을 들인 부분이기도 하다.

v2의 쓰기 권한은 하나의 불리언 스위치였다: read_only 또는 writable . v3는 그것을 두 개의 필드로 분리했다:

@dataclass class TaskState : ... # 대화 라운드. 매번 begin_turn / resume / recover_from_memory 마다 증가한다. turn_index: int = 0 # 쓰기 임차가 결합된 라운드. write_lease_turn == turn_index 일 때에만 writable이 유효하다. write_lease_turn: int | None = None

enable_writes()가 하는 일은 access_mode를 writable로 바꾸는 것만이 아니라, 임차를 현재 라운드에 결합하는 것이기도 하다:

state.access_mode = AccessMode.WRITABLE state.write_lease_turn = state.turn_index

그리고 require_writable()는 동시에 두 가지를 검사한다:

if state.access_mode is not AccessMode.WRITABLE: raise WriteGateViolation( f"현재는 읽기 전용/대화 상태이므로「 {action} 」 실행을 거부합니다." " 먼저 조사를 완료하고 enable_writes(이유)를 호출해 명시적으로 잠금을 해제하세요." ) if state.write_lease_turn != state.turn_index: self._revoke_write_lease(state, "stale write lease" ) raise WriteGateViolation( f"「 {action} 」거부됨: 쓰기 권한이 만료되었습니다" f"(승인 라운드 {state.write_lease_turn} , 현재 라운드 {state.turn_index} )." "현재 라운드에서 enable_writes(이유)를 다시 호출하세요." )

사용법은 이렇습니다:

agent.enable_writes( "방안 확정" ) agent.require_writable( "adapters/xometry.py 수정" ) # OK, 1라운드 agent.begin_turn( "사용자가 다음 메시지를 보냄" ) agent.require_writable( "adapters/xometry.py 계속 수정" ) # WriteGateViolation: 승인 라운드 1, 현재 라운드 2

왜 access_mode 를 read_only 로 되돌리기만 하면 안 되는가?

회수 지점이 하나가 아니기 때문입니다: begin_turn() , end_turn() , checkpoint() , resume() , recover_from_memory() 모두 쓰기 권한을 회수해야 합니다. 불리언 값 하나만 두면 어느 한 경로에서 바꾸는 것을 잊는 순간 권한이 새어 나갑니다. 더 골치 아픈 점은, 디스크에서 오래된 상태를 로드할 때 access_mode 는 여전히 writable 일 수 있지만 그 라운드는 이미 오래전에 끝났다는 것입니다.

두 필드가 「어느 라운드에 승인했는가」를 명시적으로 만듭니다. access_mode 가 여전히 writable 이더라도 write_lease_turn 이 turn_index 와 맞지 않으면 require_writable() 역시 거부합니다. 테스트 test_stale_lease_is_rejected_even_if_mode_is_writable 가 이 시나리오를 전문적으로 다룹니다.

이것이 내가 이를 「잠금」이 아니라 「임대(리즈)」라고 부르는 이유이기도 합니다. 잠금은 상호 배제를 해결합니다: 같은 순간에 하나의 보유자만 허용합니다. 임대는 승인 범위를 해결합니다: 누가 승인했고, 어느 라운드까지 승인했으며, 언제 회수하는가. 여기에는 동시성 경합 문제가 없고, 진짜 문제는 「승인의 유효 기간」입니다. 이를 잠금으로 구현하면 코드는 잠금과 해제를 중심으로 돌아가고, 임대로 구현하면 코드는 라운드와 회수 지점을 중심으로 돌아가는데, 후자가 Agent의 실제 대화 모델에 더 가깝습니다.

v2 의 옛 상태는 로드 시 자동으로 read_only 로 강등되며, 테스트 test_legacy_writable_state_is_revoked_on_load 가 이 호환 동작을 고정합니다. 업그레이드가 옛 writable 을 새 버전으로 끌고 들어와서는 안 됩니다.

쉽게 간과되는 디테일이 하나 더 있습니다: checkpoint() 자체도 임대를 회수하며, 그것도 기억을 쓰기 전에 회수합니다. 체크포인트는 이 라운드 실행이 끝났음을 의미하므로, 복구된 결과는 반드시 read_only 입니다. resume() 과 recover_from_memory() 는 각각 새로운 대화 라운드를 시작하므로, 복구 후에도 다시 승인해야 합니다.

여기에는 호스트 책임이 하나 있습니다: 프레임워크는 「사용자가 새 메시지를 보냈다」는 사실을 볼 수 없습니다. 호스트가 매 새 메시지가 시작될 때 begin_turn() 을 호출해야 합니다. 이것이 현재 설계의 경계이며, 뒤의 「알려진 제한」의 첫 번째 항목입니다.

I4: 상태와 기억이 같은 프로젝트 루트에 바인딩된다

LongRunAgent(root=...) 는 동시에 두 가지를 결정합니다:

- 핫 상태를 <root>/.longrun/state.json 에 기록한다;

- 기본 MCPMemoryBackend 가 --project_path <root> 로 시작하며, 기억은 <root>/memory.db 에 기록한다.

이는 resume() 이 읽는 상태와 기억이 영원히 같은 프로젝트에 속함을 의미합니다. 프로젝트를 바꿀 때는 root 만 바꾸면 되고, 상태 디렉터리와 기억 디렉터리를 따로 설정할 필요가 없습니다.

테스트 test_default_backend_binds_to_project_root 와 test_mcp_backend_passes_project_path 는 각각 Python 백엔드와 MCP 서브프로세스의 바인딩을 검증합니다. 후자는 생성자 인자만 확인하는 것이 아니라, 서브프로세스 인자에 --project_path <root> 가 실제로 포함되어 있는지 확인합니다.

기억 자체는 프로젝트 단위의 화이트박스 구조입니다: SQLite 인덱스 + raw_data/ 원문, 콜드/핫 분리. 벡터 데이터베이스가 아니며, 검색은 현재 키워드 휴리스틱입니다; 이 점은 제한 항목에서 명확히 적겠습니다.

4. 7-상태 상태 머신: 먼저 「멈춤」의 세 가지 의미를 구분하자

쓰기 권한은 「이번 라운드에 손대도 되는가」를 해결하고, 상태 머신은 「지금 이야기해야 하는가, 아니면 해야 하는가」를 해결합니다.

v3 에는 7개의 상태가 있습니다:

pending in_progress paused blocked waiting_verification completed failed

이것들은 5개 상태를 잘게 쪼갠 것이 아니라, 「멈춤」의 원인을 분리한 것입니다:

- paused : 진도를 능동적으로 저장하고, 복구를 기다림;

- blocked : 외부 조건에 막혀 의존성이 해제되기를 기다림;

- waiting_verification : 구현이 완료되어, 사람의 검수를 기다림.

이 세 상태는 모두 in_progress 로 돌아갈 수 있지만, 복구할 때 해야 할 일은 다릅니다. 이들을 하나의 paused 로 섞어 버리면, Agent는 복구 후에 계속 써야 할지, 외부 서비스를 기다려야 할지, 아니면 사용자의 승낙을 기다려야 할지 알 수 없습니다.

상태 전이는 화이트리스트입니다:

TRANSITIONS = { TaskStatus.PENDING: {TaskStatus.IN_PROGRESS}, TaskStatus.IN_PROGRESS: { TaskStatus.PAUSED, TaskStatus.BLOCKED, TaskStatus.WAITING_VERIFICATION, TaskStatus.COMPLETED, TaskStatus.FAILED, }, TaskStatus.PAUSED: {TaskStatus.IN_PROGRESS, TaskStatus.FAILED}, TaskStatus.BLOCKED: {TaskStatus.IN_PROGRESS, TaskStatus.FAILED}, TaskStatus.WAITING_VERIFICATION: { TaskStatus.IN_PROGRESS, TaskStatus.COMPLETED, TaskStatus.FAILED, }, TaskStatus.COMPLETED: set (), TaskStatus.FAILED: set (), }

표에 없는 전이는 전부 TransitionError를 던진다. completed와 failed는 종료 상태이며, 출구가 없다.

또 하나 아주 작은 결정이 있다. 상태 파일이 손상되었을 때는 곧바로 StateCorruptError를 던지고, 「과제 없음」으로 취급하지 않는다. 조용히 빈 상태로 취급하면, 다음 start_task()가 실제 진행 상황을 덮어쓰게 되기 때문이다. 차라리 멈추더라도, 사용자의 진행 상황을 삼켜버리지 않는다.

五、한번 돌려보기: 임차(lease)는 정말 만료되는가

위의 내용은 모두 설계다. 진짜로 안심이 되는 것은 재현 가능한 출력 한 토막이다.

깨끗한 환경에서 두 개의 패키지를 설치한다:

python -m pip install "agent-memory-router @ git+https://github.com/lss486558354-ship-it/agent-memory-router-v2.git" python -m pip install "agent-longrun-framework @ git+https://github.com/lss486558354-ship-it/Agent-LongRun-Framework-v3.git" agent-longrun demo

demo는 임시 디렉터리에서 전체 경로를 실행하며, LLM을 호출하지 않고 API key도 필요 없다. 핵심 출력은 다음과 같다:

[1] start_task: read-only by default gate: 当前是只读/对话态,拒绝执行「demo write」。 [2] enable_writes: lease for this turn only gate: writable; turn: 1 [3] checkpoint: save memory before emitting the resume token [LongRun Checkpoint] 状态: paused · 读写门禁: read_only · 对话轮次: 1 [4] new agent: resume gate after resume: read_only; turn: 2 gate: 当前是只读/对话态,拒绝执行「write after resume」。 expected: the write lease does not survive resume [5] re-authorize for the new turn, then finish status: completed; memory: S_0002 OK: checkpoint -> resume -> single-turn write lease -> finish

가장 눈여겨볼 것은 4번째 단계다. 새 Agent 프로세스가 과제를 복구한 뒤, 첫 번째 쓰기 작업이 거부되었다. prompt를 잊어버려서가 아니라, resume()이 이미 turn_index를 2로 올렸는데 임차는 아직 1에 머물러 있기 때문이다.

5번째 단계에서 다시 권한을 부여한 뒤에야 쓰기를 계속하고, 그다음 finish할 수 있다.

이 출력은 README에 있는 GIF의 내용 출처이기도 하다. 직접 한번 돌려보면 된다. 나를 믿지 않아도 된다.

六、이것이 아닌 것: LangGraph / Temporal / Mem0 / hooks와의 경계

이 프로젝트는 「또 하나의 Agent 프레임워크」로 오해받기 쉽다. 먼저 경계를 분명히 해두자:

프로젝트 그것이 해결하는 것 이 프로젝트와의 관계 LangGraph 그래프 오케스트레이션, 그래프 내 checkpointer 이 프로젝트는 쓰기 권한 수명 주기, 기억 쓰기 순서, 프로젝트 바인딩을 관리한다; LangGraph 바깥에 감쌀 수 있다 Temporal 분산 durable execution 이 프로젝트는 단일 머신, 파일 + SQLite의 경량 계약이며, 클러스터가 필요 없다 Mem0 / 벡터 DB 의미 기반 장기 기억 이 프로젝트는 프로젝트 단위 SQLite + 원문 냉온 분리이며, 검색은 현재 키워드 방식이다; 기억 백엔드는 교체 가능하고, 계약은 변하지 않는다 Claude Code hooks 호스트 수준의 도구 차단 이 프로젝트는 백엔드 비종속적인 Python API + MCP이며, 특정 호스트에 묶이지 않는다

혹자는 묻는다: 왜 데이터베이스 트랜잭션을 그냥 쓰지 않는가?

트랜잭션은 단일 데이터베이스 내의 원자성은 보장하지만, 「한 차례의 대화」가 무엇인지는 정의할 수 없고, 저장소를 넘나드는 쓰기 순서도 대체할 수 없기 때문이다. 이 프로젝트는 로컬 JSON + SQLite이고, 목표는 단일 머신 Agent다; 분산 durable execution이 필요한 시나리오라면 Temporal 같은 시스템을 써야 하지, 이 얇은 계약에 가짜 분산 트랜잭션을 덧붙일 일이 아니다.

이것은 오케스트레이션 프레임워크가 아니고, LLM을 호출하지 않으며, DSL도 없다. 이것은 단지 세 가지 질문에 답한다:

- 이 과제는 지금 어떤 상태이고, 다음 전이가 합법인가?

- 이번 라운드에는 파일 쓰기가 허용되는가?

- 프로세스가 죽은 뒤에, 진짜 진행 상황은 어디에 있는가?

이미 오케스트레이션 계층이 있다면, 이것은 그것을 대체하는 것이 아니라 그 위에 겹쳐 올릴 수 있는 제약 계층이어야 한다.

七、알려진 제한 사항

이 부분은 내가 직접 쓴다. README 구석에 숨기지 않는다.

- begin_turn()은 호스트가 각각의 새 사용자 메시지가 시작될 때 호출해야 한다. 프레임워크는 메시지 경계를 볼 수 없으며, 이것은 현재 설계의 한계다.

- resume() / recover_from_memory()는 이미 각각 begin_turn을 한 번 포함하고 있으므로, 같은 메시지에 대해 중복 호출하지 말 것.

- MCP 호출마다 서브 프로세스가 하나씩 생성된다. 고빈도 검색 시나리오에서는 장기 연결이나 로컬 백엔드가 필요하다.

- LocalMemoryBackend의 프로젝트 루트는 모듈 수준 전역 설정이므로, 같은 프로세스에서 병렬로 프로젝트를 전환할 수 없다.

- 키워드 추출은 휴리스틱 방식이며, 의미 벡터가 없다.

- 상태 파일에는 잠금이 걸려 있지 않으며, 같은 .longrun/은 동시 쓰기를 지원하지 않는다.

- 현재는 동기 API이므로, 비동기 호스트는 스레드 풀로 감싸야 한다.

- Windows 콘솔에서는 PYTHONIOENCODING=utf-8 이 필요하며, 예제에서 이미 재설정을 해 두었다.

- 현재 버전은 v0.3.0으로, 개인과 소규모 팀의 장기 작업에 적합하다. 프로덕션 사용 시에는 자신의 동시성 및 영속화 요구사항에 따라 평가해야 한다.

프레임워크 자체에는 52개의 pytest가 있고, 함께 제공되는 메모리 라우터에는 47개가 있다. CI는 Python 3.10–3.12를 커버한다. 테스트는 「돌려 봤다」는 식의 장식이 아니며, 위의 모든 불변 조건에는 대응하는 동작 테스트가 있다.

八、마지막으로

맨 처음의 질문으로 돌아가 보자. AI는 왜 항상 당신이 승인하지 않았는데도 손을 대는가?

그것은 「승인」에 범위가 있다는 것을 모르기 때문이다.

프롬프트는 그것에게 「먼저 물어봐」라고 말할 수 있지만, 오직 런타임 계약만이 보장할 수 있다. 당신이 승인하지 않았을 때 쓰기 작업이 정말로 거부된다는 것, 당신이 한 번 승인했을 때 그 승인이 정말로 한 라운드만 커버한다는 것, 중단점이 저장했다고 하면 메모리에 정말로 레코드가 하나 있다는 것을.

나는 이 구조를 한 문장으로 압축했다.

대화 상태는 정렬을 담당하고, 실행 상태는 작업 하달을 담당하며, 리스는 「하달」을 만료시키는 역할을 한다.

당신도 장기 작업 Agent를 만들고 있다면, 데모를 직접 실행해 보거나 코드 속 테스트를 살펴보기 바란다. 가장 지적받고 싶은 것은 단일 라운드 리스라는 설계다. 이것은 과잉 설계인가? 당신의 시나리오에서 어떤 작업은 라운드를 넘어 권한을 부여해야 하고, 어떤 것은 매 라운드 다시 확인해야 하는가?

저장소: github.com/lss48655835…

한 줄 명령으로 체험:

이 경계 설계가 당신에게 유용하다면, 어디가 잘못되었는지 issue로 알려 주기 바란다.

리우수이성

1

글

29

읽음

0

팬