System Reminders
System Reminders
이 문서에서는 SystemReminders capability를 소개해요. 실행 중간에 목표를 정한 행동 안내를 — 고정 주기로 또는 조건에 반응형으로 — 재진술해서, 여러 턴에 걸쳐 나타나는 지침 퇴색(instruction fade)에 대응하고 프롬프트 캐시를 절대 무효화하지 않아요.
출처: 문서
본문
SystemReminders는 실행 중간에 목표를 정한 행동 안내를 — 고정 주기로 또는 조건에 반응형으로 — 재진술해서, 많은 턴에 걸쳐 나타나는 지침 퇴색에 대응하고 프롬프트 캐시를 절대 무효화하지 않아요.
Pydantic AI Harness가 0.x 릴리스인 동안에는 minor 릴리스 사이에 API가 바뀔 수 있어요. 그럴 때는 deprecation 경고와 릴리스 노트 마이그레이션 안내가 (당신이나 당신의 에이전트에게) 정확히 업그레이드 방법을 알려줘요. 버전 정책을 참고하세요.
The problem
긴 다중 턴 실행은 지침 퇴색을 겪어요. 많은 도구 사용 턴 후에 모델이 시작 시 받은 안내를 점차 무시하게 되죠. 세션 시작 시 하나의 시스템 프롬프트로는 확장된 작업에 충분하지 않아요. 해결책은 실행 중간에 목표를 정한 안내를 — 고정 주기로, 또는 조건이 감지되면 반응형으로 — 재진술하는 것이에요.
The solution
SystemReminders는 각 모델 요청에 리마인더를 주입해요. 정적으로(Reminder, 주기로) 또는 동적으로(실행 컨텍스트를 읽는 콜러블) 주입해요. 리마인더는 CachePoint 뒤의 임시 UserPromptPart로 요청의 꼬리에 추가돼요:
- 주입은 영속 히스토리가 저장된 뒤에 실행되므로, 리마인더는 모델에 도달하지만
message_history에는 절대 기록되지 않아요. 턴마다 리마인더가 누적되지 않아요. CachePoint는 리마인더 바로 앞에 배치되므로, 캐시된 접두사(도구 + 시스템 + 실제 대화)가 턴에서 턴으로 바이트 동일하게 유지돼요. 작은 리마인더만 캐시 밖에 있어요.
대신 시스템 프롬프트(또는 영속된 부분)에 주입하면 요청의 앞에 위치하므로, 모든 리마인더가 캐시된 접두사를 깨고 낡은 리마인더가 히스토리에 쌓여요. 이 capability는 둘 다 피해요.
Usage
SystemReminders(...)를 capabilities에 넣어 Agent를 구성하세요:
from pydantic_ai import Agent
from pydantic_ai_harness import SystemReminders
from pydantic_ai_harness.system_reminders import Reminder
agent = Agent(
'anthropic:claude-sonnet-4-6',
capabilities=[
SystemReminders(
reminders=[Reminder('Stay focused on the original request.', interval=5)],
)
],
)
result = agent.run_sync('Refactor the auth module and add tests.')
print(result.output)
Static reminders
Reminder는 실행 내에서 주기로 발화해요:
Field
Purpose
content
리마인더 텍스트.
interval
N개의 모델 요청마다 발화(interval=3은 3번째, 6번째, ...에 발화).
first_after
첫 발화의 요청 번호, 이후 매 interval마다. None = interval의 첫 배수(단순 modulo).
trigger
RunContext 위의 술어. 설정되면 True를 반환할 때 그리고 주기가 일치할 때만 발화.
max_fires
실행당 발화 횟수를 제한. None = 무제한.
tag
콘텐츠를 <tag>\ncontent\n</tag>로 감싸요. 기본값은 'system-reminder'. 원시 콘텐츠를 원하면 None 설정.
기본 tag='system-reminder'는 모든 리마인더를 <system-reminder>...</system-reminder>로 감싸서, Claude Code의 관례를 따라 모델이 이를 사용자 텍스트가 아닌 대역외(out-of-band) 안내 메모로 읽게 해요.
tag 감싸기는 정적 Reminder 콘텐츠에만 적용돼요. 동적 콜러블(GoalReanchor와 LLMReminder 포함)은 반환한 텍스트를 원시로 주입하고 자체 포맷을 소유해요.
Dynamic reminders
동적 리마인더는 (RunContext) -> str | None(동기 또는 비동기)인 모든 콜러블이며, 모델 요청마다 평가돼요. 문자열을 반환하면 주입하고, None을 반환하면 건너뛰어요. 이는 하드코딩된 감지기 없이 실행 상태(토큰 예산, 압축 후, 모드 전환)를 필요로 하는 조건을 위한 일반적인 이음새예요:
from pydantic_ai_harness import SystemReminders
SystemReminders(
dynamic_reminders=[
lambda ctx: 'Wrap up soon.' if ctx.run_step > 20 else None,
],
)
GoalReanchor -- zero-cost goal anchoring
GoalReanchor는 실행의 첫 사용자 요청을 앵커로 재진술하고 모델에게 다음 행동이 그 목표를 진전시키는지 확인하도록 요청해요. 모델 호출도 의존성도 없어요:
from pydantic_ai_harness import SystemReminders
from pydantic_ai_harness.system_reminders import GoalReanchor
SystemReminders(dynamic_reminders=[GoalReanchor()])
LLMReminder -- model-generated nudges
LLMReminder는 모델이 컴팩트한 트랜스크립트(원래 목표 + 최근 활동)를 짧은 온-태스크 유지 넛지로 요약하게 해요. 명시적인 model이 필요해요 — 기본 모델 id는 없어요 — 그리고 어떤 오류에도 GoalReanchor 텍스트로 폴백하므로, 실패한 생성이 실행을 막지 않아요:
from pydantic_ai_harness import SystemReminders
from pydantic_ai_harness.system_reminders import LLMReminder
SystemReminders(dynamic_reminders=[LLMReminder(model='anthropic:claude-haiku-4-5')])
동적 리마인더는 자체 주기가 없어요 — 모델 요청마다 실행돼요. 따라서 LLMReminder는 턴마다 추가 모델 호출 한 번을 발생시켜요(그 사용량은 ctx.usage를 통해 부모 실행에 이어붙여져 result.usage()에 나타나요). 중첩 호출은 또한 부모의 usage_limits 아래에서 실행되며 그 앞에 오는 모델 요청을 위해 요청 한 개를 예약해 두므로, 리마인더가 실행을 request_limit을 넘어 밀어낼 수 없어요. 예산이 그렇게 빠듯해지면 생성이 생략되고 GoalReanchor 텍스트가 대신 사용돼요. 폴백이 조용하므로, 지속적으로 잘못 구성된 model(잘못된 id, 누락된 키)은 정상 동작처럼 보여요. 비용을 제한하려면 비동기 래퍼로 주기 뒤에 게이트하세요:
_llm = LLMReminder(model='anthropic:claude-haiku-4-5')
async def every_tenth(ctx):
return await _llm(ctx) if ctx.run_step % 10 == 0 else None
SystemReminders(dynamic_reminders=[every_tenth])
durability 엔진 아래에서 dynamic_reminders에 직접 나열된 LLMReminder는 저널링된 capability 연산이에요. 재생은 기록된 리마인더를 복원하고 모델 호출을 반복하지 않으며, 생성 오류는 엔진의 재시도 정책을 상속하는 대신 GoalReanchor 폴백으로 기록되므로 best-effort 리마인더가 실행을 지연시킬 수 없어요. SystemReminders는 안정적인 기본 id='system_reminders'를 지니므로, 영속 복구가 구성 없이 동작해요.
직접 항목만 그 경로를 타요. 두 가지 모양은 그러지 않아요:
- 위의
every_tenth같은 래퍼는 오케스트레이션 컨텍스트에서LLMReminder를 호출하는데, I/O를 금지하는 엔진은 호출을 완전히 실패시킬 수 있어요. __call__을 재정의하는LLMReminder하위 클래스는 그 오버라이드를 직접 실행하므로 역시 저널링될 수 없어요.
durability 엔진이 없으면 생성이 세 경우 모두 직접 실행되며, 오류 시 GoalReanchor로 같은 폴백을 해요.
Configuration
추가된 후 리마인더를 관찰하려면 ReminderFiredEvent를 구독하세요:
from pydantic_ai import Agent
from pydantic_ai_harness import SystemReminders
from pydantic_ai_harness.system_reminders import Reminder, ReminderFiredEvent
agent = Agent(
'anthropic:claude-sonnet-4-6',
capabilities=[SystemReminders(reminders=[Reminder('...', interval=5)])],
)
@agent.on_event(ReminderFiredEvent)
async def record(ctx, event):
print(event.text)
마이그레이션: on_fire는 계속 지원되지만 deprecate됐어요. 그 콜백 본문을 이 구독으로 옮기세요.
from pydantic_ai_harness import SystemReminders
from pydantic_ai_harness.system_reminders import Reminder
SystemReminders(
reminders=[Reminder('...', interval=5)],
dynamic_reminders=[], # callables evaluated every request
cache_ttl='5m', # TTL for the cache breakpoint before the reminder ('5m' | '1h')
)
실행별 상태(요청 카운터와 리마인더별 발화 횟수)는 for_run으로 격리되므로, 같은 에이전트의 동시 실행이 발화 상태를 절대 공유하지 않아요.
Caching guarantee
리마인더는 시스템 프롬프트나 instructions에 절대 주입되지 않아요. CachePoint 뒤의 임시 꼬리에 실려서, 턴을 넘어:
- 영속 히스토리는 append-only로 자라며 바이트 동일하게 재생되므로, 전체 접두사가 캐시 히트 대상이 돼요(프로바이더의 캐시 TTL 적용 —
cache_ttl보다 긴 간격은 변경되지 않은 접두사에서도 항목을 만료시켜요); - 리마인더와 그
CachePoint는 요청별 복사본에만 존재하므로, 아무것도 무효화할 수 없고 영속되지 않아요.
CachePoint는 Anthropic, Amazon Bedrock(Converse API), OpenRouter(Anthropic과 Gemini 모델)에서 지원돼요. 프롬프트 캐싱이 없는 프로바이더에서는 그냥 무시돼요(깨뜨릴 것이 없음). 리마인더는 요청이 breakpoint가 붙을 사용자 콘텐츠를 이미 지닐 때에만 자체 CachePoint로 시작해요 — 꼬리 콘텐츠가 리마인더뿐인 턴(예: instructions 전용 실행의 첫 요청)에서는 보호할 접두사가 없으므로 breakpoint 없이 주입돼요.
Composition
- Planning은 같은 임시 꼬리 메커니즘을 사용해 계획을 노출해요. 둘 다 한 에이전트에서 조합돼요. 각각 자체
CachePoint뒤에 자체 꼬리 부분을 추가하고, 어느 쪽도 영속되지 않아요. 각 임시 꼬리 capability가 캐시 breakpoint를 추가한다는 점을 주의하세요. Anthropic은 4개를 허용하며(자동 캐싱으로 3개), core는 낡은 것부터 가장 오래된 것을 정리하므로,anthropic_cache_instructions/anthropic_cache_tool_definitions와 함께 꼬리 주입 capability 여러 개를 쌓으면 더 오래된 breakpoint를 축출할 수 있어요. capability 두 개와 기본값은 예산 안에 들어요. - 루프 감지(감지-및-중단 with durable nudge)는 별개의 관심사예요.
SystemReminders는 임시로 유지되는 주기/조건 안내예요. 그 위에서 안내하려면 동적 리마인더가 deps에서 루프 상태를 읽을 수 있어요.
꼬리 리마인더는 요청의 마지막 메시지가 ModelRequest이고 최소 하나의 리마인더가 발화할 때만 추가되므로, 아무것도 발화하지 않는 턴은 요청에 아무것도 추가하지 않아요. 프로바이더 재개(resume) 턴(요청 꼬리가 그대로 에코되는 일시 중단된 ModelResponse인 턴)은 건너뛰어지고 주기 슬롯을 소모하지 않아요.
Not spec-serializable
SystemReminders.get_serialization_name()은 None을 반환해요. 리마인더가 임의의 콜러블을 취하므로 agent spec으로 직렬화할 수 없어요.
Further reading
- Pydantic AI capabilities
- Hooks --
wrap_model_request가 여기서 사용되는 임시 주입 지점이에요 - Anthropic prompt caching
- Planning -- 프롬프트 캐시를 인지하는 또 다른 harness capability
API reference
SystemReminders
Bases: AbstractCapability[AgentDepsT]
긴 세션의 지침 퇴색에 대응하기 위해 주기적 또는 조건부 리마인더를 주입.
긴 다중 턴 실행은 지침 퇴색을 겪어요. 많은 도구 사용 턴 후에 모델이 세션 시작 안내를 점차 무시해요. SystemReminders는 실행 중간에 목표를 정한 안내를 고정 주기(Reminder)로 또는 콜러블(dynamic_reminders)에서 반응형으로 재주입해요.
캐시 안전이 설계 제약이에요. 리마인더는 각 요청의 꼬리 에 CachePoint 뒤의 임시 UserPromptPart로 추가돼요. wrap_model_request 안에서(이는 core가 영속 히스토리를 저장한 뒤 실행). 그래서 리마인더는 모델에 도달하지만 message_history에는 절대 들어가지 않아요. 낡은 리마인더가 쌓이지 않고, 캐시된 접두사가 턴을 넘어 바이트 동일하게 유지돼요 — 작은 리마인더만 캐시 밖에 있어요. 대신 시스템 프롬프트나 영속된 부분에 주입하면 발화마다 캐시 접두사를 깨고 리마인더가 쌓이게 해요.
from pydantic_ai import Agent
from pydantic_ai_harness.system_reminders import SystemReminders, Reminder
agent = Agent(
'anthropic:claude-sonnet-4-6',
capabilities=[
SystemReminders(
reminders=[Reminder('Stay focused on the original request.', interval=5)],
)
],
)
Attributes
reminders
주기로 주입되는 정적 리마인더.
Type: Sequence[Reminder[AgentDepsT]] Default: ()
dynamic_reminders
모델 요청마다 평가되는 콜러블. 주입할 텍스트를 반환하거나 None을 반환해 건너뛰기.
Type: Sequence[DynamicReminder[AgentDepsT] | AsyncDynamicReminder[AgentDepsT]] Default: ()
cache_ttl
꼬리 리마인더 앞에 배치되는 캐시 breakpoint의 TTL.
Type: Literal['5m', '1h'] Default: '5m'
on_fire
렌더링된 각 리마인더로 호출되는 deprecate된 콜백. 대신 ReminderFiredEvent를 구독하세요.
Type: Callable[[str], None] | None Default: None
Methods
for_run
@async
def for_run(ctx: RunContext[AgentDepsT]) -> SystemReminders[AgentDepsT]
카운터가 리셋된(구성 보존) 새 실행별 인스턴스를 반환.
복제본은 _request_count와 _fire_counts를 리셋하므로, 같은 에이전트의 동시 실행이 발화 상태를 공유하지 않아요.
Returns
SystemReminders[AgentDepsT]
wrap_model_request
@async
def wrap_model_request(
ctx: RunContext[AgentDepsT],
*,
request_context: ModelRequestContext,
handler: WrapModelRequestHandler,
) -> ModelResponse
발화된 리마인더를 캐시 breakpoint 뒤의 요청 꼬리에 추가한 다음 모델을 호출.
core가 영속 히스토리를 저장한 뒤에 실행되며, 여기서 변경된 요청별 메시지 목록은 절대 다시 쓰이지 않으므로 리마인더와 그 CachePoint는 모델에 도달하지만 ctx.state.message_history에는 절대 들어가지 않아요.
Returns
get_serialization_name
@classmethod
def get_serialization_name(cls) -> str | None
spec 직렬화 불가: 리마인더가 임의의 콜러블을 취함.
Returns
Reminder
Bases: Generic[AgentDepsT]
에이전트 실행 중 주기로 주입되는 정적 리마인더.
Attributes
content
리마인더 텍스트.
Type: str
interval
실행 내에서 N개의 모델 요청마다 발화. interval=3은 3번째, 6번째, 9번째, ... 요청에 발화.
Type: int Default: 1
first_after
첫 발화의 요청 번호. None(기본값)은 interval의 첫 배수(단순 modulo)에 발화. 설정되면 first_after에 발화하고 그 후 매 interval 요청마다 발화.
Type: int | None Default: None
trigger
현재 RunContext 위의 선택적 술어. 설정되면 트리거가 True를 반환할 때 그리고 주기 조건이 충족될 때만 발화.
Type: Callable[[RunContext[AgentDepsT]], bool] | None Default: None
max_fires
실행 내에서 이 리마인더가 발화할 수 있는 최대 횟수. None은 무제한.
Type: int | None Default: None
tag
설정되면 콘텐츠를 XML 태그로 감싸기: <tag>\ncontent\n</tag>. 기본값은 'system-reminder'(Claude Code의 관례). 원시 콘텐츠를 내보내려면 None 설정.
Type: str | None Default: 'system-reminder'
GoalReanchor
Bases: Generic[AgentDepsT]
실행의 첫 사용자 요청을 앵커로 재진술하는 제로 비용 동적 리마인더.
모델 호출과 의존성이 없어요. ctx.messages에서 첫 사용자 메시지를 읽고 모델에게 다음 행동이 그 목표를 진전시키는지 확인하도록 요청해요. 아직 사용자 메시지가 없으면 정적 한 줄로 폴백해요. SystemReminders.dynamic_reminders에 추가하세요.
LLMReminder
Bases: Generic[AgentDepsT]
모델이 컴팩트한 트랜스크립트에서 텍스트를 생성하는 동적 리마인더.
옵트인이며 의존성 없음(pydantic_ai.Agent를 사용). model은 필수이며 기본값이 없어요 — 명시적 모델을 전달하세요. 어떤 오류에도 GoalReanchor 텍스트로 폴백하므로 실패한 생성이 실행을 절대 막지 않아요. SystemReminders.dynamic_reminders에 추가하세요.
모든 동적 리마인더처럼 모델 요청마다 평가되므로 턴마다 추가 모델 호출 한 번을 발생시켜요. 그 사용량은 부모 실행(ctx.usage)에 이어붙여지고, 예약된 요청 한 개를 뺀 부모의 usage_limits 아래에서 실행되므로 리마인더가 실행을 request_limit을 넘어 밀어낼 수 없어요. 예산이 그렇게 빠듯해지면 생성이 생략되고 GoalReanchor 텍스트가 대신 사용돼요. 턴별 생성이 너무 비싸다면 주기로 게이트하세요(문서 참고).
SystemReminders가 소유하면 durability 엔진 아래에서 생성은 저널링된 capability 연산이에요. 재생은 모델 호출을 반복하는 대신 생성된 텍스트를 복원해요.