상태·기억·재개
멀티턴 업무에서는 무엇을 기억할지보다 어디에 저장할지를 먼저 정합니다. 대화, 앱 상태, 작업 결과와 실행 재개 지점은 수명과 신뢰 범위가 다릅니다.
네 가지 저장 영역
섹션 제목: “네 가지 저장 영역”| 저장 영역 | 담는 값 | 용도 |
|---|---|---|
| 대화 기록 | 사용자·assistant text, 축약 DataPart, 파일 참조 | 최근 문맥 |
WorkflowState | 작은 구조화 값 | 마지막 route, 진행 플래그, 누계 |
WorkHistory | 검토된 summary·facts·keywords | 관련 작업 결과 회수 |
| durable checkpoint | 단계, target snapshot, relay artifact | 재개와 중복 호출 방지 |
파일 원본, OCR 전문, base64와 signed URL은 장기 state에 넣지 마세요. 현재 요청의 원본을 자식에게 전달하는 일과 다음 턴을 위한 안전한 참조·요약을 저장하는 일은 별개입니다.
자동 문맥과 명시적 문맥
섹션 제목: “자동 문맥과 명시적 문맥”AgenticProgram에서는 최근 대화와 WorkMemory 회수가 자동입니다. context_turns만큼의 대화와 context_roles로 허용한 역할, 관련 기억의 제한된 projection이 controller 첫 관측에 들어갑니다. work_memory_context = "none"은 자동 주입만 끄며 read-only work_recall 도구는 남습니다.
원본 FilePart, OCR과 가공 전 payload는 이 projection에 들어가지 않습니다. 직접 작성한 run_turn(ctx)와 code-first Rules hook은 대화나 WorkHistory를 자동으로 자식 프롬프트에 넣지 않습니다. 필요한 값만 접어 전달하세요.
Controller는 이 bounded 대화를 항상 보지만, Agentic child의 text는 기본적으로 현재 요청을 유지합니다. 대명사형 후속 질문을 독립 요청으로 해소해야 하는 read-only 도구에만 input = "conversation"을 명시하세요. 직전 assistant 답변까지 참조하려면 context_roles = ["user", "assistant"]가 필요합니다. 기본 input = "context"인 기존 도구의 입력은 바뀌지 않습니다.
# code-first 경로에서만 최근 사용자 입력을 직접 합칩니다.text = ctx.fold_prior( 3, prior_header="[최근 요청]", current_header="[현재 요청]",)result = await ctx.call( "verification", text=text, data=ctx.data, files=ctx.files,)fold_prior()는 턴 시작 snapshot을 읽어 현재 요청을 중복하지 않습니다. fold_files()는 파일 참조만 합치며 과거 bytes를 복구하거나 다시 다운로드하지 않습니다.
요청 기록과 WorkflowState
섹션 제목: “요청 기록과 WorkflowState”일반 서버는 auto_record_request=True, auto_record_request_policy="compact"를 기본으로 사용합니다. 성공한 run_turn 뒤에 현재 요청을 기록하면서 큰 DataPart와 파일을 축약합니다. 공통 정책으로 표현할 수 없는 마스킹이 있을 때만 ctx.record_request()를 직접 호출하세요. 인자 없는 수동 호출은 현재 DataPart와 FilePart를 그대로 저장할 수 있습니다.
다음 턴의 분기에 필요한 작은 값은 WorkflowState에 둡니다.
# 최신 route 하나만 남깁니다.ctx.remember("last_route", "verification")
# 처리 ID는 중복을 제거하고 최근 100개로 제한합니다.ctx.reduce( "processed_ids", ["ORD-1002"], lambda old, new: [*dict.fromkeys([*(old or []), *new])][-100:],)현재 턴에서만 쓰는 값은 지역 변수로 두세요. WorkflowState는 업무 데이터베이스가 아닙니다.
작업 기억 WorkMemory
섹션 제목: “작업 기억 WorkMemory”WorkHistory는 원본 응답 저장소가 아니라 업무 결과 요약 저장소입니다. summary, 공개 가능한 facts와 keywords만 남깁니다.
근거와 저장 시점
섹션 제목: “근거와 저장 시점”[work_memory.policy]는 summary Agent에 보낼 근거를 정합니다.
| mode | 근거 |
|---|---|
none | 요약하지 않음 |
request | child에게 보낸 DataPart·FilePart |
response | child가 반환한 DataPart·FilePart |
both | request와 response를 분리된 segment로 전달 |
both는 두 배열을 섞지 않습니다. 파일에서는 안정된 ID, 이름, MIME, 확장자와 크기만 남기고 bytes·base64·content·OCR·URL은 제거합니다. input_required, 도메인 오류와 pending unavailable은 성공 결과가 아니므로 저장하지 않습니다.
timing | 동작 |
|---|---|
before_finalize | 성공한 child 직후 provisional summary 준비 |
turn_end | 턴 끝에 호출 순서대로 요약하는 기본값 |
explicit | ctx.work.summarize_result()를 호출한 결과만 요약 |
write_behind | 성공한 child 직후 계약 facts와 결정적 core summary를 저장하고, 문장 품질 개선은 응답 commit 뒤 background worker에서 수행 |
write_behind의 응답 경로는 LLM을 호출하지 않습니다. Orchestrator는 summaryPhase="core"로 summary Agent에 deterministic projection을 요청하며, ResponseContractSummaryAdapter는 wrapped model을 우회하고 자신이 소유한 ResponseContractCache의 단일 snapshot으로 facts를 만듭니다. 따라서 summary Agent의 okf-contracts/·원격 contract overlay와 다른 로컬 계약을 Orchestrator가 임의로 읽지 않습니다. 이 authoritative facts와 core summary는 같은 턴의 finalizer·WorkMemoryFactsProvider·recall에 즉시 보이고, background enrichment는 facts·ID·순서·lifecycle을 바꾸지 않은 채 summary 문장만 개선합니다.
동기 core snapshot을 만들거나 저장할 수 없으면 facts 계약을 지키기 위해 해당 턴을 실패시키지만, worker 시작이나 enqueue, enrichment child 호출이 실패해도 저장된 core 응답은 정상적으로 계속됩니다. PostgreSQL의 pending generation은 만료되는 claim lease로 replica 하나만 LLM을 호출하고, 검증된 LLM 결과는 새 attempt를 만들지 않은 채 write-back만 backoff 재시도합니다. enrichment LLM 호출은 최대 세 번이며 store claim·write-back lock을 각각 짧게 점유하므로, 이 timing을 쓰는 custom WorkflowStateStore는 keyword-only lock_timeout을 지원해야 합니다. all뿐 아니라 latest·relevant controller projection도 write-back 전후 facts와 byte budget을 보존합니다.
문장 개선이 실제로 반영됐는지 확인할 때는 턴 trace가 아니라 orchestrator.work-memory-enrichment trace를 이름으로 찾으세요. 응답을 보낸 뒤에 도는 작업이라 턴 trace 안에는 나타나지 않고, 세션 id 없이 기록되므로 세션 필터로도 걸리지 않습니다. 요약 대상 facts는 다른 child 호출과 마찬가지로 이 trace에 남으니, 민감한 값이 섞이는 도메인이라면 관측 마스킹 정책을 함께 검토하세요. 개선이 반영되지 않아도 대화는 그대로 이어집니다 — 저장된 core 요약과 facts는 유지되고, 반영되지 않은 이유는 orchestrator 로그에 경고 한 줄로 남습니다.
provisional에서 공개까지
섹션 제목: “provisional에서 공개까지”자동 summary는 provisional로 시작합니다. FinalResponse와 output guardrail을 통과해야 committed로 이동해 일반 recall에 보입니다. 담당자 반려나 출력 차단은 해당 항목을 quarantine으로 옮깁니다. state 저장이나 commit이 실패하면 메시지, 기억 bucket과 checkpoint를 턴 시작 상태로 함께 복원합니다.
ctx.work.remember()와 remember_result()로 앱이 직접 넣은 항목은 곧바로 committed입니다. 공개 가능한 facts인지 애플리케이션이 먼저 확인하세요.
설정 표면
섹션 제목: “설정 표면”[agents]verification = "registry:verification"summary = "registry:data-summary"
[work_memory]enabled = truesummary_agent = "summary"summary_schema = "llamon.work-summary.v1"timing = "before_finalize"store_states = ["completed"]failure_mode = "fail_soft"
[work_memory.policy]# 요청과 결과를 출처가 다른 segment로 요약합니다.verification = "both"
[work_memory.history]state_key = "work_history"max_items = 10select_limit = 3max_item_bytes = 16384max_summary_chars = 2000max_text_chars = 2000| 키 | 기본값·제약 |
|---|---|
enabled | false. 자동 WorkMemory 활성화 |
summary_agent | summary. [agents]의 alias |
summary_schema | llamon.work-summary.v1. 빈 문자열 금지 |
timing | turn_end; before_finalize·turn_end·explicit·write_behind |
store_states | ["completed"]. terminal 상태는 넣어도 저장되지 않음 |
failure_mode | fail_soft 또는 fail_closed |
policy | alias별 none·request·response·both |
state_key | work_history |
max_items | 10, 1~100 |
select_limit | 3, 0 이상 |
max_item_bytes | 16384, 양의 정수 |
max_summary_chars·max_text_chars | 2000, 0 이상 |
enabled=true이면 summary alias를 뺀 모든 static child에 policy를 적습니다. summary alias 자체를 다시 요약하도록 지정하면 시작 단계에서 거부합니다. 기존 timing에서 fail_soft는 경고와 trace를 남기고 summary 없이 계속하며 fail_closed는 계약을 지킬 수 없을 때 현재 경계를 실패시킵니다. write_behind는 동기 core 실패만 fail-closed이고 background worker 시작·enrichment는 항상 fail-soft이므로, worker가 비활성이어도 저장된 core facts 계약은 유지됩니다.
회수 API
섹션 제목: “회수 API”| 필요 | API |
|---|---|
| 검토한 summary 저장 | ctx.work.remember(...) |
| 정책에 따라 즉시 요약 | await ctx.work.summarize_result(result) |
| 현재 execution의 facts | ctx.work.summary_for(result) |
| LLM용 제한된 projection | ctx.work.recall(query, ...) |
| 신뢰된 앱의 내부 관리 | history()·latest()·select() |
recall()은 관련 항목을 결정적으로 고르고 items, parts와 크기를 제한한 JSON text를 반환합니다. 저장 ID, 내부 metadata와 원본 DataPart·FilePart는 공개 projection에서 빠집니다. history()나 select()의 가공 전 반환값을 프롬프트에 넣지 마세요.
recalled = ctx.work.recall(ctx.user_text, limit=3)if not recalled.items: # 안전한 기억이 없으면 권한 있는 자식이 원문을 다시 조회합니다. return await ctx.call("verification", data=ctx.data, files=ctx.files)
return await ctx.generate( "answer_writer", text=ctx.user_text, facts={"work_history": list(recalled.parts)},)요약에 실패해도 원본 DataPart나 FilePart를 generator facts로 대신 넘기지 마세요. 안전한 projection이 없다면 생성을 멈추거나 원문을 다시 조회합니다.
AgenticProgram에서는 WorkMemory 결합이 자동입니다. code-first 흐름은 recall() 결과를 직접 전달합니다. data-summary Agent의 ResponseContractSummaryAdapter와 custom summary_schema는 에이전트 구성, projection 계약은 응답 계약을 참고하세요.
checkpoint와 재개
섹션 제목: “checkpoint와 재개”Agentic과 내장 TurnWorkflow는 controller 판단, 도구 승인, 담당자 검수와 child의 input_required를 DurableExecutionStore에 checkpoint합니다. 같은 conversation으로 새 입력이 오면 완료한 단계를 다시 호출하지 않고 중단 지점부터 이어 갑니다.
| workflow checkpoint | 수동 resume | |
|---|---|---|
| 단계와 target snapshot | SDK가 저장 | 앱이 관리 |
| 완료 호출 중복 방지 | 있음 | 자체로는 없음 |
| 새 턴 입력 병합 | workflow가 관리 | 앱이 전달 |
| 주된 용도 | Agentic·내장 workflow | @managed_run_turn 분기 |
ctx.clear_resume()은 workflow checkpoint를 지우지 않으며 ctx.get_resume()도 pending workflow 상태를 보여 주지 않습니다. 수동 재진입은 Deterministic 워크플로우의 call_key 패턴을 따르세요.
PostgreSQL 운영
섹션 제목: “PostgreSQL 운영”state_backend = "in_memory"는 프로세스를 다시 시작하면 state와 checkpoint가 사라집니다. 재시작과 여러 worker를 견뎌야 한다면 state_backend = "postgres"를 사용합니다.
DSN은 TOML이 아니라 POSTGRES_ORCH_DSN에 두며, 값이 없으면 POSTGRES_MEMORY_DSN을 사용합니다. 배포 전에는 같은 conversation의 동시 처리가 직렬화되는지, 저장 실패 때 응답과 WorkMemory가 함께 롤백되는지 확인하세요. 장애와 재시도 의미는 운영과 실패 복구에 있습니다.