v0.3.0
호환성 깨짐 없음 — 모두 추가 변경뿐입니다. 기존 agent/flow 프로젝트는 코드·요청/응답·CLI 출력이 그대로이며, SDK 버전만 >=0.3.0으로 올리면 됩니다. orchestrator는 직접 만들어 쓰기 전까지 어떤 기존 경로에도 로드되지 않습니다.
한눈에 보기
섹션 제목: “한눈에 보기”신규 — Workflow Orchestrator · 세 번째 프로젝트 타입(agent/flow 옆). 여러 자식 에이전트를 하나의 멀티턴 워크플로우로 묶고 턴을 넘어 결과를 누적합니다.
- 코어에 LLM 없음 — 분류·검색·응답 작성은 모두 자식 에이전트가 맡습니다. orchestrator는 흐름 제어 + 턴 간 상태 누적만 책임집니다.
run_turn(ctx)코드퍼스트 — 순서/분기/병렬/루프를 순수 Python으로 작성.- state_backend 2종 —
in_memory(개발·데모) ·postgres(운영 — 영속·멀티 워커 안전). - 멀티턴 맥락 헬퍼 — 직전 대화의 텍스트(
fold_prior)·파일(fold_files)·결과(prior_result)를 자식 입력에 한 줄로 합칩니다.
공통 개선 · agent·flow·orchestrator 모두 적용
- typed
reasoning+provider_extra직접 전달 —reasoning=False(on/off) ·reasoning="low"(portable effort) ·provider_flavor(OpenAI-compatible discriminator) ·verbosity(OpenAI native 출력 분량). SDK가 provider별로 자동 매핑하며, 기본값이None/auto라 미지정 시 모델 기본 동작을 유지합니다. - LLM 호출 타임아웃 —
LLM_TIMEOUT(env, 기본 600초)로 단발 호출 상한 → 무한 대기 차단. message/send·stream·skill·judge 공통,0=무제한. - Langfuse 세션·trace 정합화 — 클라이언트
sessionId등 관측 키가 멀티 에이전트 체인 전체에 자동 전파. 같은 세션이 호출별로 쪼개져 보이던 문제 수정. - 운영 튜닝 env — HTTP 풀·동시성·타임아웃 상수를 env로 조정(미설정 시 기존 값 그대로).
- 가드레일 검사 파트 선별 —
GuardrailConfig(inspect_parts={...})로text/data/file중 검사 대상 한정(기본은 전부 검사라 미지정 시 동일).
개발 편의
- 타입 지정 HITL 페이로드(
HITLQuestion/HITLOption) ·MockOrchestratorContext— 네트워크 없는 단위 테스트. - Studio — 미저장 변경 보호 · 노드 삭제 실행 취소 · 예시 코드 삽입 · Host Run Auto-reload.
개발자가 직접 보게 되는 변경
섹션 제목: “개발자가 직접 보게 되는 변경”신규 프로젝트 타입 — orchestrator
섹션 제목: “신규 프로젝트 타입 — orchestrator”agent·flow에 이어 세 번째 프로젝트 타입입니다. 흐름은 app/orchestrator.py의 run_turn(ctx)(순수 Python)로 작성하고, 자식 에이전트 연결은 orchestrator.toml [agents]에 선언합니다.
llamon orchestrator intake-review --template generic --yes자식 에이전트의 대화 맥락은 자식별({conversationId}:{alias})로 격리되어 서로 섞이지 않습니다.
run_turn(ctx) — 워크플로우 흐름
섹션 제목: “run_turn(ctx) — 워크플로우 흐름”from app.reducers import merge_documents # 직접 작성하는 reducer (작성법은 오케스트레이터 가이드 참고)
async def run_turn(ctx): ctx.record_request() # 이번 입력(text·files·data)을 user 턴으로 기록 (emit의 짝)
# 동적 라우팅 = 자식 분류기(LLM). 결정형 분기는 그 출력으로. intent = await ctx.call("intent", text=ctx.user_text) if intent.data.get("intentType") == "unclassified": return ctx.emit(await ctx.call("unclassified", text=ctx.user_text))
# 병렬 (cap=16) — gather 안에서는 상태 변경 금지 if ctx.files: res = await ctx.gather({"review": ctx.call("document_review", files=ctx.files)}) ctx.reduce("documents", res["review"].data_parts, merge_documents) # 턴 간 누적
documents = ctx.get("documents", []) # 이전 턴 누적분까지 final = await ctx.call("response", data={"documents": documents}) return ctx.emit(final) # = append_message("assistant", …) + returnflow+memory로는 처리하기 어려운 턴 간 상태 누적(ctx.remember/ctx.reduce/ctx.get)을 기본으로 제공합니다. 분기마다 반복되는 응답 기록 + return은 ctx.emit(result) 한 줄로 줄일 수 있습니다.
멀티턴 맥락 헬퍼 — 텍스트·파일·결과 세 축
섹션 제목: “멀티턴 맥락 헬퍼 — 텍스트·파일·결과 세 축”“그거 검증해줘” 같은 모호한 발화를 보완하거나 여러 턴에 걸쳐 문서를 함께 처리할 때, messages를 직접 잘라 쓰지 말고 헬퍼를 쓰세요. 직전 대화 스냅샷은 턴 시작 시점에 고정되어 항상 직전 턴만 가리킵니다.
ctx.record_request() # 이번 입력을 user 턴으로 기록 (emit의 짝)work = await ctx.call("comparison", # 직전+현재 파일을 한 번에 text=ctx.fold_prior(3), files=ctx.fold_files(3))prior = ctx.prior_result() # 가장 최근 'data 있는' 결과- 텍스트 —
ctx.fold_prior(n)직전 대화를 합친 문자열(text=에 바로) ·ctx.prior_turns(n)직전 메시지 목록 - 파일(신규) —
ctx.fold_files(n)직전+현재 파일 병합(files=에 바로) ·ctx.prior_files(n)직전 파일만 - 결과 data(신규) —
ctx.prior_result()가장 최근 data 있는 결과 1건 ·ctx.prior_data(n)범위 내 전부 - 기록(신규) —
ctx.record_request()이번 입력을 user 턴으로 기록. 이게 있어야 다음 턴 헬퍼들이 직전 입력을 봅니다. - 윈도우 범위 밖의 영속 누적은
ctx.accumulate(key, items).
직전 결과를 자식 data=에 자동으로 넣어주지는 않습니다 — 자식 스키마 계약에 맞춰 앱이 직접 감싸 전달하세요([{"priorWork": prior}, ...]).
대화 기록(messages)은 개수(max_messages)와 바이트(max_state_bytes) 두 축으로 오래된 것부터 자동 정리됩니다. 대용량 파일은 바이트로 직접 싣기보다 URI로 넘기는 방식을 권장합니다.
버그 수정 — 컨텍스트 격리 · Langfuse (기존 배포 영향 없음)
섹션 제목: “버그 수정 — 컨텍스트 격리 · Langfuse (기존 배포 영향 없음)”- 익명 턴 격리 —
contextId없이 들어온 단발 호출들이 상태를 공유할 수 있던 가능성을 차단하고 턴마다 격리합니다. 멀티턴이 필요하면 기존처럼contextId를 보내면 됩니다. emit의 멀티 DataPart 보존 —ctx.emit(result)가 결과의 모든data_parts를 대화기록에 남깁니다(이전엔 첫 파트만).- Langfuse 세션 분절 수정 — 같은
sessionId로 보낸 요청인데 Langfuse 세션이 호출(에이전트)별로 쪼개져 보이던 문제를 일괄 수정했습니다. 이제 모든 trace 레이어가 동일 규칙(클라이언트metadata.sessionId우선, 없으면contextId)을 따르고, orchestrator도 수신 요청의 관측 키를 자식 호출에 자동 전달합니다.sessionId를 보내지 않는 배포는 종전과 동일합니다. - 스트리밍 trace output 보강 —
message/stream요청의 Langfuse trace output에 text만 남고 응답 DataPart가 누락되던 동작을 invoke 경로와 맞췄습니다. 파일 본문(base64)은 자리표시자로 치환되어 trace에 노출되지 않습니다. - 빌드타임 스냅샷
response_format누락 수정 —LLMConfig(response_format=...)이 런타임 resolve에선 적용됐지만 빌드타임 스냅샷(resolved.json, 에어갭) 경로에선 누락되어 조용히 드롭되던 문제를 수정했습니다. 이제provider_extra·reasoning과 동일하게 스냅샷에 함께 구워져 양 경로 동작이 일치합니다(registry-structured템플릿의 JSON 강제 권장이 에어갭 배포에서도 적용됨).response_format미설정 배포는 종전과 동일합니다.
언제 쓰나 (agent / flow / orchestrator)
섹션 제목: “언제 쓰나 (agent / flow / orchestrator)”| 무엇 | 상태 | |
|---|---|---|
| agent | 단일 에이전트 (LLM + tools/MCP/guardrail) | 에이전트별 멀티턴(thread memory) |
| flow | 정적 LangGraph 그래프 (순서/분기/병렬) — 한 번의 호출 내 | 대화 메모리(checkpointer) |
| orchestrator | 여러 자식을 묶는 상태 유지 워크플로우 | 턴 간 상태 누적(reduce/remember) |
→ “여러 에이전트를 묶어 자연스러운 통합 응답”을 만들면서 턴을 넘어 결과를 누적해야 하면 orchestrator를 쓰세요. 순서/분기만 필요하면 flow로 충분합니다.
런타임 — build_orchestrator_app
섹션 제목: “런타임 — build_orchestrator_app”from llamon_agent.orchestrator.server import build_orchestrator_app
app = await build_orchestrator_app(card=card, run_turn=run_turn, config_path="orchestrator.toml")기존 create_server/executor를 어댑터로 그대로 구동하므로 기존 코드 수정이 없습니다. 현재는 스트리밍을 지원하지 않고 단일 artifact로 응답합니다.
state_backend — in_memory / postgres
섹션 제목: “state_backend — in_memory / postgres”| 백엔드 | 영속 | 동시성 | 용도 |
|---|---|---|---|
in_memory (기본) | 없음 — restart 시 유실 | 단일 프로세스 내 잠금 | 개발·데모·단일 워커 |
postgres | 있음 | DB 행 잠금 — 멀티 워커에서도 같은 대화는 순서대로 처리 | 운영·영속·멀티 워커 |
postgres는 별도 schema(llamon_orch)를 쓰므로 메모리 backend와 같은 DB를 공유해도 안전하고, 스키마/테이블은 첫 turn에 자동 생성됩니다. DSN은 POSTGRES_ORCH_DSN(미설정 시 POSTGRES_MEMORY_DSN)에서 읽습니다.
선택적 경계 가드레일
섹션 제목: “선택적 경계 가드레일”app = await build_orchestrator_app( card=card, run_turn=run_turn, input_guardrail=my_input_guardrail, # 선택 output_guardrail=my_output_guardrail, # 선택)자식 에이전트가 각자 가드레일을 갖고 있어도, orchestrator 자신의 경계에서 합성 응답 전체(text + DataPart)를 한 번 더 검증할 수 있습니다. 둘 다 생략하면 기존 동작 그대로입니다.
결정 유틸 — build_decider + 프리셋 5종 (선택)
섹션 제목: “결정 유틸 — build_decider + 프리셋 5종 (선택)”from llamon_agent import build_classifier
scope = build_classifier("ollama/qwen3-7b") # 상태 없음 — 턴 간 재사용 안전
async def run_turn(ctx): res = await scope.decide(ctx.user_text, labels=["current_only", "accumulated"])run_turn 안에서 필요한 작은 LLM 판단(스코프 분류·중복 판정·모호 해소·불리언/필드 추출)을 한 줄로 만듭니다 — build_classifier / build_duplicate_detector / build_disambiguator / build_boolean_extractor / build_field_extractor. decider는 개발자가 써넣은 결정 지점에서만 돌고 응답을 생성하지 않으므로, 코어 조율은 여전히 LLM 없이 돌아갑니다.
registry id로 선언한 decider LLM은 빌드타임에 resolve되어(resolve --deciders) 런타임에 registry 네트워크 호출이 없습니다(폐쇄망 안전). resolve CLI의 인증도 선택사항이 되어 AUTH_HOST 미설정 시 무인증 registry를 그대로 조회합니다. → 가이드
세션·메타데이터 전파
섹션 제목: “세션·메타데이터 전파”자식 에이전트 호출에 세션·상관관계 식별자를 잇는 방법이 정리됐습니다. 해당 값을 쓰지 않으면 기존 요청 형식이 그대로 유지됩니다.
ctx.call(metadata=)/OrchestratorContext(base_metadata=)—request_id·traceparent같은 식별자를 자식 호출 metadata로 전달합니다.base_metadata는 매 호출에 자동 병합되고, 호출별metadata=의 같은 키가 우선합니다.- flow
call_agent_auto의forward_session(기본 ON) — 클라이언트 metadata 중 관측 키(sessionId·userId·workflowId·workflowName·agentId·agentName·chatbotId·chatbotName)만 자식 호출에 선별 전파합니다. 멀티 에이전트 체인 전체의 Langfuse trace가 클라이언트 세션 한 줄로 묶이고 같은 워크플로우 이름으로 그룹핑됩니다. 자식에 이 식별자를 노출하면 안 되면forward_session=False. → 헬퍼 레퍼런스 · 클라이언트가 넣는 법은 관측 메타데이터 - orchestrator도 동일 키 집합 자동 전파 — 수신 요청 metadata의 관측 키가 모든
ctx.call자식 호출에 병합됩니다(명시metadata=우선). call_agent에 keyword-onlycontext_id추가 — 미지정 시 기존과 동일하게 동작합니다.
타입 지정 HITL 페이로드 — HITLQuestion / HITLOption
섹션 제목: “타입 지정 HITL 페이로드 — HITLQuestion / HITLOption”노드 그래프(agent/flow)의 HITL 질문을 pydantic으로 선언합니다. 생성되는 페이로드는 기존 {"question", "options"} 형식을 포함하는 상위 호환 형식이라 기존 클라이언트가 그대로 동작하고, 재개 시 사용자의 답변을 옵션 value로 정규화합니다(LLM 불필요). → HITL 가이드
운영 튜닝 env
섹션 제목: “운영 튜닝 env”코드 상수로만 박혀 있던 풀/타임아웃/동시성 값을 env로 조정할 수 있습니다. 미설정·오타·범위 위반 시 경고 후 코드 기본값으로 동작하므로 env 실수로 부팅이 죽지 않습니다. 명시 코드 인자가 있으면 env보다 우선합니다.
| 그룹 | env (default) |
|---|---|
| HTTP 공유 풀 | HTTP_MAX_CONNECTIONS(500) · HTTP_MAX_KEEPALIVE_CONNECTIONS(100) · HTTP_STREAMING_MAX_CONNECTIONS(200) · HTTP_TIMEOUT_CONNECT(10s) · HTTP_TIMEOUT_READ(30s) |
| 외부 호출 | REGISTRY_HTTP_TIMEOUT(10s) · A2A_CARD_TIMEOUT(30s) · A2A_SEND_TIMEOUT(120s) · A2A_CONNECT_TIMEOUT(10s) · GUARDRAIL_HTTP_TIMEOUT(5s) · LLM_MAX_RETRIES(3) · LLM_TIMEOUT(600s, 미설정 시에도 적용·0=무제한) |
| 동시성 | FANOUT_MAX_CONCURRENCY(16) |
| 메모리·PostgreSQL | MEMORY_PG_COMMAND_TIMEOUT(10s) · MEMORY_PG_STATEMENT_CACHE_SIZE(100) · MEMORY_INMEMORY_MAX_THREADS(10000) · ORCH_PG_POOL_MAX_SIZE(5) · ORCH_PG_STATEMENT_TIMEOUT_MS(5000) |
CLI scaffold의 .env.example에 전 키가 주석 처리 상태(미설정 = 기본값)로 생성되며, 템플릿 종류에 맞는 그룹만 노출됩니다.
reasoning·verbosity 제어 + provider_extra 직접 전달
섹션 제목: “reasoning·verbosity 제어 + provider_extra 직접 전달”Thinking·Reasoning 설정 방식은 모델과 제공자마다 다릅니다. SDK는 이를 추상화하기 위해 reasoning을 공통 인터페이스로 제공하며, 각 제공자에 맞는 설정 형식으로 자동 변환합니다. 제공자 전용 옵션이 필요한 경우에만 provider_extra를 사용하세요.
LLMConfig(id="<MODEL_ID>", reasoning=False) # thinking/reasoning offLLMConfig(id="<MODEL_ID>", reasoning="low") # portable effortLLMConfig(id="<MODEL_ID>", provider_flavor="vllm") # OpenAI-compatible discriminatorLLMConfig(id="<MODEL_ID>", verbosity="low") # OpenAI native 출력 분량| 설정 | 대상 | provider 매핑 | 미지원 시 |
|---|---|---|---|
reasoning | on/off + effort + budget intent | OpenAI native=reasoning_effort/reasoning · vLLM=chat_template_kwargs.enable_thinking · Ollama=reasoning · Anthropic=thinking/output_config | vLLM/비-gpt-oss Ollama는 effort depth를 on/off로 degrade |
provider_flavor | OpenAI-compatible endpoint 구분 | Registry 경로는 provider_type.code 우선, code-first는 base URL로 추정 | 대부분 auto면 충분. 프록시/Azure는 명시 권장 |
verbosity | OpenAI native 응답 분량 조절 | OpenAI native=verbosity | 미지원 OpenAI 모델=서버 400 가능, SDK가 fail-soft 재시도 |
provider_extra | vendor raw 파라미터 | OpenAI/vLLM/Anthropic=extra_body · Ollama=options | 내용은 provider-specific |
reasoning과verbosity는 서로 독립입니다. 하나는 추론/생각 방식, 하나는 출력 분량입니다.- **기본값
None**이면 모델/서버 기본 동작을 그대로 따르므로, 미지정 시 결과가 바이트 단위까지 동일해 기존 배포를 그대로 둬도 됩니다. - registry 런타임 · 빌드타임 스냅샷(에어갭) · code-first 세 경로 모두에서 동작합니다.
- 직접 우회 통로
LLMConfig.provider_extra— 하나의 필드를 SDK가 provider raw 채널로 자동 매핑합니다(OpenAI/vLLM/Anthropic→extra_body, Ollama→options). portable knob으로 다룰 수 없는 provider 고유 파라미터를 직접 넘길 때 쓰며, 내용은 provider마다 다릅니다. - 주의: vLLM 경로는 게이트웨이·프록시가
chat_template_kwargs를 업스트림 서빙 엔진으로 전달해야 실제로 적용됩니다.
config.py설정 예시는 에이전트 합성 — LLM 튜닝 파라미터를, 증상별 원인·처방은 트러블슈팅을 참고하세요.
llamon run — 캐시 빌드 기본 복원 (빠른 반복)
섹션 제목: “llamon run — 캐시 빌드 기본 복원 (빠른 반복)”기본값이 --no-cache 전체 빌드에서 --cache(docker compose up --build, 레이어 캐시 사용)로 바뀌어 반복 실행이 수 초로 단축됩니다. 낡은 wheel이 끼어드는 근본 원인이던 Dockerfile 템플릿의 bind-mount RUN cp /build/llamon_agent-*.whl 방식을 콘텐츠 체크섬 기반 COPY로 교체해, 같은 파일명 wheel의 내용 변경도 캐시 빌드에 정확히 반영됩니다.
# 변경 전 (낡은 산출물 위험 + 캐시 무력화)RUN --mount=type=bind,source=.,target=/build \ cp /build/llamon_agent-*.whl /app/ 2>/dev/null || true && \ cp /build/llamon_agent-*.whl.sha256 /app/ 2>/dev/null || true
# 변경 후 (콘텐츠 체크섬 기반 — 정확하고 빠름)COPY llamon_agent-*.whl* /app/기존 생성 프로젝트는 Dockerfile/Dockerfile.local의 해당 RUN 블록을 위 COPY 한 줄로 교체하세요(미교체 시 llamon run . --no-cache로 종전 전체 빌드 동작). 전체 빌드가 필요하면 --no-cache를 명시해 그대로 쓸 수 있습니다.
doctor
섹션 제목: “doctor”orchestrator.toml이 있으면 llamon doctor가 설정 파일과 app/orchestrator.py 존재를 검증합니다(DOC130–DOC133). 비-orchestrator 프로젝트에는 영향 없습니다.
Studio — 편집 안전성·노드 예시코드
섹션 제목: “Studio — 편집 안전성·노드 예시코드”flow/agent 비주얼 에디터의 데이터 유실 방지와 노드 코드 작성 경험을 다듬었습니다(orchestrator는 코드퍼스트라 해당 없음).
- 미저장 변경 보호 —
.env·에이전트 카드 패널을 미저장으로 닫거나 백업 복원 시 확인 단계를 거칩니다. - 노드 삭제 실행 취소 — 휴지통·Delete로 지운 노드·코드·라우팅을 토스트의 ‘실행 취소’로 한 번에 복원합니다.
- 예시 코드 삽입 — 노드/라우팅 함수 에디터에서 백엔드 기본 코드를 언제든 불러와 편집·복사할 수 있습니다.
- Host Run Auto-reload (항상 on) — Host Run으로 실행 중이면 프로젝트 파일(
*.py·.env·pyproject.toml·orchestrator.toml) 저장 시 호스트 프로세스를 자동 재시작합니다(uvicorn --reload와 같은 방식 — 재시작 동안 진행 중 요청은 끊김). 헤더 상태 배지에auto-reload표시. 수동 Stop·크래시 시에는 자동 재시작하지 않으며,.venv·__pycache__·숨김 디렉터리(백업 포함)는 감시에서 제외됩니다. - 저장 피드백·접근성(
aria-live)·라이트 테마 대비 등 다수 개선.
테스트 (네트워크 없이)
섹션 제목: “테스트 (네트워크 없이)”import pytestfrom llamon_agent.orchestrator.testing import MockOrchestratorContextfrom app.orchestrator import run_turn
@pytest.mark.asyncioasync def test_unclassified_branch(): ctx = MockOrchestratorContext(conversation_id="t-1", user_text="???") ctx.mock_call("intent", data={"intentType": "unclassified"}) ctx.mock_call("unclassified", text="다시 말씀해주세요")
result = await run_turn(ctx)
assert ctx.calls_made == ["intent", "unclassified"] # 호출 순서/분기 assert result.text == "다시 말씀해주세요"마이그레이션
섹션 제목: “마이그레이션”| 시나리오 | 작업 |
|---|---|
| 기존 agent/flow 프로젝트 | 수정 불필요 — 전부 추가 변경뿐. orchestrator를 쓰지 않으면 아무것도 로드·변경되지 않습니다. |
| SDK 버전 지정 | pyproject.toml의 llamon-agent>=0.2.x → >=0.3.0. |
| orchestrator 신규 생성 | llamon orchestrator <name> --agent alias=target (generic 템플릿). |
| postgres state | orchestrator.toml에 state_backend = "postgres" + POSTGRES_ORCH_DSN(미설정 시 POSTGRES_MEMORY_DSN). 새 PyPI 의존성 없음. |
| Studio | orchestrator는 코드퍼스트 — 비주얼 편집 미지원. app/orchestrator.py/orchestrator.toml을 직접 편집(CLI가 안내). |
관련 자료
섹션 제목: “관련 자료”- 오케스트레이터 만들기 — run_turn · state_backend · 경계 가드레일 · 폐쇄망 배포 가이드.
- 에이전트 합성 — 여러 에이전트를 묶는 기존 패턴(orchestrator의 자식 호출과 연결).
- 아키텍처 개요 — agent/flow/orchestrator의 위치와 A2A 합성.
현재 제약: 스트리밍 미지원(단일 artifact) · Studio 비주얼 편집 미지원(코드퍼스트). 워크플로우 상태에는 PII/민감정보가 담길 수 있으므로 운영 배포 전 가드레일 적용과 상태 보존/암호화/접근통제를 검토하세요.