콘텐츠로 이동

최종 응답과 안전 경계

작업 자식이 사용자용 답변을 완성했다면 그대로 반환하세요. 별도의 작성기와 검수기는 필요한 경우에만 붙입니다. FinalResponse는 producer 하나와 선택적 reviewer 하나로 이뤄진 짧은 파이프라인이며, 도구를 반복해서 고르는 Agentic loop가 아닙니다.

필요한 결과선택추가 호출
작업 결과를 그대로 반환FinalResponse.primary()없음
프로세스 안의 generator로 문장 작성FinalResponse.generate("answer_writer")내부 LLM 호출
별도 A2A Agent가 문장 작성FinalResponse.agent("writer")A2A 호출
완성한 문장을 A2A reviewer가 검수.review_with("reviewer")A2A 호출
from llamon_agent.orchestrator import FinalResponse
# child 답변을 그대로 사용
finalizer = FinalResponse.primary()
# 확인된 facts가 있을 때만 내부 generator로 문장 작성
finalizer = FinalResponse.generate("answer_writer", require_facts=True)
# 별도 A2A Agent가 문장 작성
finalizer = FinalResponse.agent("writer")

검수가 필요하면 세 방식 뒤에 .review_with("reviewer")를 붙입니다. 작업 결과에 이미 완성된 text가 있으면 primary()를 선택하세요. answer_writerbuild_orchestrator_app(generators=...)에 등록한 상태 없는 generator이고, agent()writer와 reviewer는 [agents]의 A2A 자식입니다. SDK는 writer, reviewer나 PII 검수기를 임의로 고르지 않습니다.

generator에는 계약으로 선별한 facts만 전달합니다. ctx.generate()는 현재 ctx.datactx.files를 자동으로 프롬프트에 넣지 않습니다. 안전한 projection이 없다면 생성을 중단하거나 권한 있는 child가 원문을 다시 조회하게 하세요.

A2A reviewer가 바꾸는 값은 text뿐입니다. DataPart와 FilePart는 producer 결과를 유지합니다. 검수 전 초안을 call_stream()으로 내보내면 나중에 회수할 수 없으므로 reviewer와 output guardrail을 통과한 text만 공개합니다.

고정된 의존 업무 단계는 final response에 숨기지 말고 OKF then.sequence로 선언합니다. 그러면 각 child가 routing 실행·관측·schema·재개 계약 안에 남고 FinalResponse.primary()는 선택된 최종 결과만 검수합니다.

경계결정 주체보호 대상
입력 guardrailRegistry 정책 또는 판정 Agentworkflow에 들어갈 현재 요청
side_effecting 도구 승인요청한 사용자저장된 도구 spec과 입력 실행
HumanReviewPort인증된 담당자사용자에게 공개할 답변
출력 guardrail·reviewerRegistry 정책 또는 A2A Agent최종 text·DataPart·파일 metadata

도구 승인은 외부 상태 변경을 허가합니다. 담당자 검수는 답변 공개를 승인합니다. 가드레일은 정책 위반을 차단하거나 마스킹합니다. 어느 경계도 다른 경계를 대신하지 않습니다.

가드레일은 기본으로 붙지 않습니다. 입력과 출력에 Registry 정책 id 또는 A2A agent 하나를 선언합니다.

orchestrator.toml
[guardrails.input]
id = "support-input-policy"
version = "latest"
regex_action = "block"
inspect_parts = ["text", "data", "file"]
[guardrails.output]
id = "support-output-policy"
regex_action = "mask"
pii_action = "mask"
mode = "stream"
mask_replacement = "[REDACTED]"
[guardrails.input.judge_llm]
# 판정 모델은 짧은 JSON을 반환하는 비추론 모델로 둡니다.
id = "<GUARDRAIL_JUDGE_MODEL_ID>"
temperature = 0.0
max_tokens = 1024
reasoning = false
provider_flavor = "auto"
[guardrails.input.judge_llm.provider_extra]
chat_template_kwargs = { enable_thinking = false }

[guardrails.input][guardrails.output]은 같은 필드를 받습니다.

값·의미
id·versionRegistry 정책과 버전. 현재 latest만 지원
agent[agents] alias. id와 함께 사용 불가
regex_action·pii_actionblock 또는 mask, 기본 block. prompt_actionpii_action의 호환 별칭
mode출력의 strict 또는 stream. 미지정은 기존 strict, input의 stream은 시작 오류
mask_replacement기본 [REDACTED]
inspect_partstext·data·file 중 하나 이상. 빈 배열 거부
judge_llmprompt 규칙 전용 모델

judge_llmid, temperature, max_tokens, provider_extra, 호환 별칭 extra_body, reasoningprovider_flavor를 받습니다. reasoning table에는 enabled, effort, budget_tokenssummary를 쓸 수 있지만 가드레일 판정에서는 provider별 비추론 모드로 낮춥니다. enabled = falseeffort·budget_tokens·summary를 함께 쓰거나 effortbudget_tokens를 동시에 지정하면 거부됩니다.

입력 검사는 라우팅, 고정 호출, 상태 기록과 WorkMemory보다 먼저 실행됩니다. 차단된 요청은 대화와 기억에 남지 않습니다. 출력 검사는 사용자에게 내보낼 text·DataPart·FilePart metadata를 emit 직전에 봅니다. 파일의 base64 본문은 검사하지 않습니다.

입력·출력 투영은 각각 50,000자 상한입니다. 넘친 부분을 잘라 통과시키지 않고 계약 오류로 실패합니다. 기본 strict 출력 guardrail은 전체 결과를 버퍼링한 뒤 단일 청크로 보냅니다. mode="stream"은 segment와 lookahead를 먼저 검증한 뒤 승인된 내용만 순서대로 보냅니다. 무제한 폭 regex와 segment_safe capability가 없는 prompt·endpoint 정책은 시작 단계에서 거부합니다.

Verified stream의 첫 segment는 64 tokens, 이후는 200 tokens이고 context/lookahead는 각각 50 tokens입니다. TextPart는 검증된 segment로, DataPart는 완성된 객체 하나로 원자적으로 처리합니다. 각 승인 artifact update에는 streamId, sequence, segmentId가 붙습니다. 후속 segment가 차단되면 안전하지 않은 segment는 보내지 않고 같은 response artifact의 승인-only replacement snapshot을 저장한 뒤 failed / GUARDRAIL_BLOCKED로 종료합니다. 연결이 끊긴 클라이언트는 task ID로 최종 Task snapshot을 다시 조회해 text와 DataPart를 복구해야 합니다.

segment 경계는 탐지값을 가로지르지 않습니다. lookahead에서 값이 경계를 넘어가는 것이 보이면 segment를 그 값의 첫 바이트까지 되돌려(retract) 승인하므로, 공백이 포함된 전화번호·이름처럼 여러 token에 걸친 값도 다음 segment에서 전체가 한 번에 마스킹됩니다. mask 정책이 경계 때문에 응답 전체를 차단하지는 않습니다. 이미 전송한 text와 겹치는 탐지값만 회수가 불가능해 fail-closed로 차단합니다.

입력 Registry 정책은 통과, 차단 또는 마스킹만 합니다. 입력 A2A Agent가 허용을 반환해도 그 Agent의 text와 DataPart를 원 요청에 섞지 않습니다. 분류 결과를 라우팅에 쓰려면 가드레일이 아니라 classifier나 고정 첫 child를 사용하세요.

입력 투영은 요청의 모든 TextPart, 모든 DataPart의 개수·순서·중첩 값, FilePart metadata를 포함합니다. 원격 input guardrail Agent에도 같은 DataPart/FilePart 복사본을 전달합니다. 허용된 block-only 요청은 원래 값을 보존하고, 명시적인 mask 정책만 guarded copy를 변환합니다. MCP가 원본 개인정보를 필요로 한다면 input은 block-only로 두고 output에서 mask/block하세요.

출력 Registry 정책은 게이트입니다. [guardrails.output].agentFinalResponse.review_with()에 주입되는 reviewer라서 text를 고칠 수 있습니다. 구조화 데이터와 파일은 producer 결과를 유지하며 input_required 문구를 고쳐도 task state와 checkpoint는 바뀌지 않습니다. 코드에 다른 reviewer가 이미 있으면 부팅 단계에서 충돌로 거부합니다.

한 정책 안의 sensitive_info·regex·prompt·endpoint track은 서로 독립입니다. mask 판정은 뒤 track의 차단을 가릴 수 없으므로 mask가 나와도 남은 track을 모두 평가합니다. 반대로 차단이 확정되면 뒤 track이 그 판정을 뒤집을 수 없으므로 즉시 반환합니다 — 차단될 요청에 judge 호출과 endpoint 왕복 비용을 물리지 않습니다. mask와 자유 형식 재작성이 함께 나오면 안전한 합성 순서가 없어 차단합니다.

정책 평가는 fail-closed입니다.

원인errorCode재시도
정책 위반GUARDRAIL_BLOCKED제품 정책에 따름
연결 실패·5xxUPSTREAM_UNAVAILABLE가능
시간 제한UPSTREAM_TIMEOUT가능
요청 제한RATE_LIMITED가능
인증 실패AUTH_REQUIRED불가
잘못된 정책·판정 형식INTERNAL_ERROR불가

prompt judge는 JSON 판정이 유효하지 않으면 한 번 재시도합니다. 두 번째 결과도 유효하지 않으면 통과시키지 않습니다. Registry 출력 검사 장애는 중앙 오류 계약을 유지하고 A2A 출력 reviewer 장애는 RESPONSE_GUARDRAIL_FAILED로 끝납니다. 이미 failed인 응답은 출력 검사를 다시 거치지 않습니다.

Python으로 input_guardrail·output_guardrail을 직접 주입할 수도 있지만 TOML과 동시에 설정할 수는 없습니다. WorkMemory를 켠 임의 code-first run_turn은 출력 차단보다 기억 공개가 앞설 수 있어 managed 경계가 아니면 거부됩니다.

법률 자문, 행정 처분 안내나 환불 확정처럼 담당자 승인 뒤에만 답변을 공개할 업무에 사용합니다. 에이전트·플로우가 요청한 사용자에게 되묻는 HITL과 다릅니다.

mode멈추는 시점
disabled담당자 검수를 사용하지 않음
controllercontroller가 request_review를 선택했을 때
alwayscontroller가 최종 답변을 만들 때마다

고정 첫 도구의 결과도 검수하려면 required_tools에 alias를 넣습니다. 포트가 없으면 request_review는 controller 행동에서 빠지고, 강제 검수가 필요한 결과는 검수 없이 통과하지 않습니다.

[agentic.support.human_review]
mode = "controller"
required_tools = ["payment"]

포트의 인증, 영속 저장과 감사 로그는 애플리케이션이 소유합니다.

from llamon_agent.orchestrator import HumanReviewDecision, HumanReviewRequest
class ReviewQueuePort:
async def submit(self, request: HumanReviewRequest) -> None:
"""review_id를 중복 등록 방지 키로 영속 저장합니다."""
async def get_decision(self, review_id: str) -> HumanReviewDecision | None:
"""인증된 담당자 결정이 없으면 None을 반환합니다."""
app = await build_orchestrator_app(
card=card,
run_turn=run_turn,
human_review_port=ReviewQueuePort(),
)

submit()이 성공했다면 포트는 이후 get_decision()이 읽을 수 있도록 요청을 durable하게 저장한 것입니다. 같은 review_id에 다른 내용을 다시 등록하면 포트가 거부합니다.

action결과
approve결정 text로 완료
edit담당자가 고친 text로 완료
reject결정 text로 완료하고 대기 중인 WorkMemory 후보 폐기
request_input사용자에게 보완 질문, 다음 final도 다시 검수

HumanReviewDecisiontext는 8,000자, ID는 각각 256자까지입니다. 같은 reviewIddecisionId는 한 번만 반영합니다. 대기 응답은 input_requiredllamon.human-review.request.v1 DataPart, 완료 응답은 llamon.human-review.decision.v1 DataPart를 사용합니다. actor_id는 포트의 감사 로그에만 남습니다.

인바운드 DataPart는 결정 근거로 쓰지 않음이 핵심 신뢰 규칙입니다. 사용자가 decision 모양의 DataPart를 보내도 SDK는 읽지 않고 인증 포트의 get_decision()만 신뢰합니다. 검수 전 초안도 A2A 응답에 싣지 않으며 HumanReviewRequest.proposed_text로만 담당자에게 전달합니다.

submit 장애는 등록 여부가 불명확하므로 현재 실행 상태를 폐기하고 안정 ID로 다시 시도합니다. get_decision 장애는 checkpoint를 보존해 포트 복구 뒤 재개합니다. 잘못된 review ID나 타입은 human_review_decision_invalid·INTERNAL_ERROR입니다. 어느 경우에도 검수 없이 답변을 내보내지 않습니다.

  • 완성된 child 답변은 FinalResponse.primary()로 반환합니다.
  • generator에는 선별한 facts만 전달합니다.
  • 입력 guardrail, 도구 승인, 담당자 검수와 출력 guardrail을 따로 테스트합니다.
  • reviewer나 guardrail 전에 text를 스트리밍하지 않습니다.
  • checkpoint와 finalization 장애 의미는 운영과 실패 복구에서 확인합니다.