19 KiB
TradingAgents × Codex 브리지 구현 설계서
1. 목표
TradingAgents가 요구하는 LLM 호출을 OpenAI API key 대신 로컬 Codex app-server + ChatGPT/Codex 인증으로 처리한다.
핵심 목표는 다음과 같다.
- TradingAgents의 기존 LangGraph / ToolNode 구조를 유지한다.
- OpenAI 호환 프록시를 억지로 에뮬레이션하지 않고, 새 provider(
codex)를 추가한다. - ChatGPT Pro 사용자는 **Codex 로그인(
codex login/codex login --device-auth) 또는 Codex의 managed auth cache(~/.codex/auth.json)**를 통해 인증한다. bind_tools()가 필요한 analyst 노드와 plaininvoke()만 필요한 debate / manager / trader 노드가 모두 동작해야 한다.- Codex의 자체 shell/web/tool 생태계에 의존하지 않고, TradingAgents가 이미 가진 도구 실행 루프를 그대로 사용한다.
2. 왜 이 방식이 최선인가
채택할 방식
권장안: codex app-server를 로컬에 띄우고, Python에서 stdio(JSONL)로 통신하는 Custom LangChain ChatModel을 만든다.
채택하지 않을 방식
A. OpenAI-compatible /v1/responses 프록시
비추천. TradingAgents는 현재 openai provider에서 langchain_openai.ChatOpenAI를 사용하고 native OpenAI일 때 use_responses_api=True를 켠다.
즉 /v1/responses와 tool-calling semantics를 꽤 정확히 흉내 내야 한다. 구현 난도가 높고 유지보수 비용이 크다.
B. Codex dynamic tools 직접 사용
비추천. app-server의 dynamicTools와 item/tool/call은 experimental 이다.
TradingAgents는 이미 ToolNode로 툴 실행을 잘 처리하므로, 여기까지 Codex에 넘길 이유가 없다.
C. Codex SDK 직접 내장
부분적으로 가능하지만 비권장. SDK는 TypeScript 중심이다. Python 프로젝트인 TradingAgents에선 app-server stdio 브리지가 더 단순하다.
설계 핵심
Codex는 모델 추론만 담당하고, 실제 툴 실행은 여전히 TradingAgents/LangGraph가 담당한다.
따라서 Codex 쪽에는 tool schema를 설명하고, 응답은 엄격한 JSON schema로만 받는다.
- 툴이 필요하면:
{"mode":"tool_calls", ...} - 툴이 더 이상 필요 없으면:
{"mode":"final", ...}
이렇게 하면 analyst 노드의 bind_tools() 요구사항을 만족시키면서도 Codex의 experimental dynamic tool API를 피할 수 있다.
3. 구현 아키텍처
3.1 새 provider 추가
수정 파일
tradingagents/llm_clients/factory.pytradingagents/default_config.pytradingagents/llm_clients/__init__.py- CLI/UI 관련 파일(선택 사항이 아니라 사실상 권장)
추가 파일
tradingagents/llm_clients/codex_client.pytradingagents/llm_clients/codex_chat_model.pytradingagents/llm_clients/codex_app_server.pytradingagents/llm_clients/codex_schema.pytradingagents/llm_clients/codex_message_codec.pytradingagents/llm_clients/codex_preflight.pytests/llm_clients/test_codex_chat_model.pytests/llm_clients/test_codex_app_server.pytests/integration/test_codex_provider_smoke.py
3.2 런타임 구성
TradingAgents 측
TradingAgentsGraph.__init__()는 deep/quick 두 개 LLM을 한 번 생성해 재사용한다.
따라서 CodexChatModel도 모델 인스턴스당 app-server 세션 1개를 유지하는 것이 적절하다.
- quick_thinking_llm → Codex app-server session A
- deep_thinking_llm → Codex app-server session B
중요 원칙
- 세션은 재사용
- thread는 per-invoke 새로 생성
- 이유: 여러 analyst / debate agent가 같은 LLM 인스턴스를 공유하므로 thread까지 재사용하면 문맥 오염이 발생한다.
즉:
- app-server process: 재사용
- Codex thread: 매 호출마다 새로 생성 후
thread/unsubscribe
3.3 인증 전략
기본/권장
사용자가 먼저 로컬에서:
codex login
브라우저 callback이 막히거나 headless면:
codex login --device-auth
headless / container / 원격 머신
cli_auth_credentials_store = "file"로 설정해서~/.codex/auth.json을 사용- 신뢰 가능한 머신에서 생성한
auth.json을 복사 - refresh는 직접 구현하지 말고 Codex가 하게 둔다
auth.json은 절대 커밋 금지
고급 옵션: OAuth URL helper
원한다면 Python helper에서 app-server로 아래를 호출해 브라우저 login URL을 직접 받아 출력할 수 있다.
account/readaccount/login/startwithtype="chatgpt"
하지만 v1 구현은 이 helper 없이도 충분하다. 실제 운영에서는 codex login이 더 단순하고 안정적이다.
3.4 보안 / 하드닝
Codex를 “코딩 에이전트”가 아니라 “모델 백엔드”로만 쓰기 위해 다음을 권장한다.
.codex/config.toml 예시
model = "gpt-5.4"
model_reasoning_effort = "medium"
approval_policy = "never"
sandbox_mode = "read-only"
web_search = "disabled"
personality = "none"
log_dir = ".codex-log"
cli_auth_credentials_store = "file"
선택적 하드닝
[features]
apps = false
shell_tool = false
multi_agent = false
추가 권장
cwd를 프로젝트 루트가 아니라 비어 있는 전용 workspace로 준다.
예:
~/.cache/tradingagents/codex_workspace- 또는 repo 내
./.tradingagents_codex_workspace
이렇게 해야 Codex가 리포지토리를 뒤지거나 파일을 읽는 쪽으로 샐 가능성을 낮출 수 있다.
4. 메시지/툴 호출 설계
4.1 입력 정규화
CodexChatModel은 아래 입력을 모두 받아야 한다.
strlist[BaseMessage]list[dict(role=..., content=...)]
이유:
- analyst 체인은 prompt pipeline 때문에
BaseMessage시퀀스를 넘길 가능성이 높다 - trader / manager 쪽은 OpenAI-style dict list를 직접
llm.invoke(messages)로 넘긴다
내부 정규화 포맷 예시
[SYSTEM]
...
[USER]
...
[ASSISTANT]
...
[ASSISTANT_TOOL_CALL]
name=get_news
args={"query":"AAPL",...}
[TOOL_RESULT]
name=get_news
call_id=call_xxx
content=...
4.2 bind_tools 처리
TradingAgents analyst 노드는 다음 패턴을 사용한다.
chain = prompt | llm.bind_tools(tools)
result = chain.invoke(state["messages"])
따라서 CodexChatModel.bind_tools()는 반드시 구현해야 한다.
구현 방식
- LangChain tool 객체를 OpenAI-style tool schema로 변환
- 내부적으로
self.bind(tools=formatted_tools, tool_choice=...)형태로 바인딩 _generate(..., tools=..., tool_choice=...)에서 그 schema를 읽어 사용
tool schema 변환
가능한 한 LangChain의 표준 helper(convert_to_openai_tool 계열)를 사용한다.
각 tool에 대해 다음 정보를 확보한다.
namedescriptionparametersJSON schema
4.3 output schema 설계
plain invoke용
{
"type": "object",
"properties": {
"answer": { "type": "string" }
},
"required": ["answer"],
"additionalProperties": false
}
tool-capable invoke용
루트는 final 또는 tool_calls 중 하나가 되도록 강제한다.
{
"oneOf": [
{
"type": "object",
"properties": {
"mode": { "const": "final" },
"content": { "type": "string" },
"tool_calls": {
"type": "array",
"maxItems": 0
}
},
"required": ["mode", "content", "tool_calls"],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"mode": { "const": "tool_calls" },
"content": { "type": "string" },
"tool_calls": {
"type": "array",
"minItems": 1,
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"name": { "const": "get_news" },
"arguments": { "...": "get_news parameters schema" }
},
"required": ["name", "arguments"],
"additionalProperties": false
}
]
}
}
},
"required": ["mode", "content", "tool_calls"],
"additionalProperties": false
}
]
}
중요한 포인트
tool_calls.items.oneOf 안에 툴별 arguments schema를 넣는다.
그래야 Codex가 tool 이름과 인자를 아무렇게나 생성하지 못한다.
4.4 tool-call 정책
Codex에게 항상 다음 규칙을 준다.
- 지금 당장 필요한 다음 단계 툴 호출만 요청할 것
- speculative call 금지
- tool result를 아직 보지 않은 상태에서 downstream tool을 미리 호출하지 말 것
- 툴이 필요 없으면 final로 답할 것
- 응답은 output schema에 맞는 JSON만 낼 것
왜 필요한가
예를 들어 market analyst는 get_stock_data 이후에 get_indicators가 자연스럽다.
하지만 CSV 생성/캐시 같은 간접 의존성이 있으므로 한 번에 여러 단계를 추측 호출하게 두는 것보다 최소 다음 호출만 받는 편이 안전하다.
5. Codex app-server 통신 계층 설계
5.1 CodexAppServerConnection
책임:
codex app-serversubprocess 시작/종료initialize/initialized- request/response correlation (
id) - stdout JSONL reader thread
- notifications 수집
- timeout / error propagation
- graceful shutdown
핵심 메서드
start()close()request(method, params, timeout)wait_for_turn_completion(thread_id, turn_id, timeout)read_account()read_models()read_rate_limits()
transport
- stdio(JSONL) 사용
- websocket transport는 실익이 적으므로 v1에서 제외
5.2 초기 handshake
시작 직후:
- subprocess spawn:
codex app-server initializeinitializedaccount/read- 필요 시
model/list
initialize 예시
{
"method": "initialize",
"id": 1,
"params": {
"clientInfo": {
"name": "tradingagents_codex_bridge",
"title": "TradingAgents Codex Bridge",
"version": "0.1.0"
}
}
}
5.3 preflight 체크
codex_preflight.py 또는 helper 함수에서:
codexbinary 존재 여부 확인- app-server 시작 가능 여부 확인
account/read(refreshToken=false)실행account.type == "chatgpt"또는"apiKey"인지 확인- 가능하면
planType == "pro"확인 model/list에서deep_think_llm,quick_think_llm가용성 확인account/rateLimits/read가능하면 출력
실패 시 메시지 예시
Codex not installed. Install with npm i -g @openai/codexNo ChatGPT/API auth found. Run codex loginRequested model gpt-5.4-mini is not available under current Codex account
6. LangChain 커스텀 모델 설계
6.1 CodexChatModel
상속:
langchain_core.language_models.chat_models.BaseChatModel
필수 구현:
_generate(...)_llm_typebind_tools(...)
권장 추가:
_identifying_paramsinvoke(...)입력 정규화 보강- 에러 래핑
내부 필드 예시
modelreasoning_effortsummarypersonalityrequest_timeoutmax_retriesserver: CodexAppServerConnectionworkspace_dircleanup_threadsservice_name
6.2 _generate() 동작
tools 없는 경우
- 입력 messages 정규화
- plain schema 생성 (
answer) - thread/start
- turn/start with
outputSchema - 최종 agent message JSON 파싱
AIMessage(content=answer)반환
tools 있는 경우
- 입력 messages 정규화
- tool schema 생성
- root oneOf output schema 생성
- thread/start
- turn/start with
outputSchema - 최종 agent message JSON 파싱
mode == "tool_calls"면:- 각 call에
id = "call_" + uuid AIMessage(content=content or "", tool_calls=[...])
- 각 call에
mode == "final"면:AIMessage(content=content, tool_calls=[])
종료 처리
thread/unsubscribe- reader queue cleanup
- 필요 시 thread archive는 선택 옵션
6.3 app-server 호출 파라미터
thread/start
{
"method": "thread/start",
"params": {
"model": "gpt-5.4",
"cwd": "/abs/path/to/.tradingagents_codex_workspace",
"approvalPolicy": "never",
"serviceName": "tradingagents_codex_bridge"
}
}
turn/start
{
"method": "turn/start",
"params": {
"threadId": "...",
"input": [
{ "type": "text", "text": "<serialized prompt>" }
],
"model": "gpt-5.4",
"effort": "medium",
"summary": "concise",
"personality": "none",
"sandboxPolicy": {
"type": "readOnly",
"access": { "type": "fullAccess" }
},
"outputSchema": { ... }
}
}
6.4 프롬프트 래퍼 템플릿
plain invoke wrapper
You are the language model backend for a LangGraph-based financial multi-agent system.
Rules:
1. Answer only from the provided conversation transcript.
2. Do not inspect files.
3. Do not run commands.
4. Do not use web search.
5. Return ONLY JSON that matches the provided schema.
Conversation transcript:
<...serialized messages...>
tool-capable wrapper
You are the language model backend for a LangGraph-based financial multi-agent system.
You may either:
- request the next necessary tool call(s), or
- provide the final assistant response.
Hard rules:
1. Use only the allowed tools listed below.
2. Arguments must conform exactly to the JSON schema for that tool.
3. Request only the next required tool call batch.
4. Do not speculate past missing tool results.
5. Do not inspect files.
6. Do not run commands.
7. Do not use web search.
8. Return ONLY JSON that matches the provided schema.
Allowed tools:
<tool schemas pretty-printed>
Conversation transcript:
<...serialized messages...>
안정화 팁
- tool schema를 pretty JSON으로 포함
- 1~2개의 few-shot example을 포함할 수 있음
- 단, prompt를 너무 길게 만들어 토큰 낭비하지 않도록 주의
7. TradingAgents 코드 변경 체크리스트
7.1 default_config.py
추가 권장 key:
"llm_provider": "openai",
"codex_binary": "codex",
"codex_reasoning_effort": "medium",
"codex_summary": "concise",
"codex_personality": "none",
"codex_workspace_dir": os.getenv("TRADINGAGENTS_CODEX_WORKSPACE", "./.tradingagents_codex_workspace"),
"codex_request_timeout": 120,
"codex_max_retries": 2,
"codex_cleanup_threads": True,
호환성 위해:
openai_reasoning_effort가 설정돼 있고codex_reasoning_effort가 비어 있으면 fallback 하도록 해도 좋다.
7.2 factory.py
대략:
if provider_lower == "codex":
return CodexClient(model, base_url, **kwargs)
7.3 codex_client.py
책임:
BaseLLMClient구현- kwargs를
CodexChatModel생성자에 전달 validate_model()에서 preflight/model list 확인
7.4 CLI / UI
반드시 추가할 항목:
- provider 목록에
codex - backend_url 입력은 codex일 때 숨기거나 무시
- advanced options:
codex_reasoning_effortcodex_summarycodex_personalitycodex_workspace_dir
7.5 README / docs
반드시 문서화:
- ChatGPT Pro/Codex auth와 API key의 차이
codex login- headless auth cache 사용법
.codex/config.toml예시- provider 선택 방법
- known limitations
8. 테스트 전략
8.1 단위 테스트
test_codex_message_codec.py
str입력 정규화BaseMessage시퀀스 정규화- dict message 시퀀스 정규화
ToolMessage직렬화
test_codex_schema.py
- plain schema 생성
- tool oneOf schema 생성
- tool args const / required / additionalProperties 검증
test_codex_chat_model.py
mock app-server 응답으로:
- plain final answer
- tool_calls answer
- malformed JSON retry
- timeout
- unsupported model error
test_codex_app_server.py
- initialize handshake
- request/response correlation
- notification draining
- turn completed / failed 처리
8.2 통합 테스트
smoke
- provider=
codex - analyst=
news한 개만 선택 - ticker=
AAPL - research depth=1
- 최종 리포트 파일 생성 확인
tool loop
- market analyst만 실행
- 첫 응답이
get_stock_datatool call - tool result 후 다음 응답이
get_indicators또는 final
multi-agent
market + news- graph 전체 완주
final_trade_decision비어 있지 않음
auth preflight
- 로그인 안 된 환경 → 친절한 실패
- 로그인 된 환경 → account/read 성공
8.3 운영 검증
실제 실행 전 아래 순서 권장:
codex login
python -m tradingagents.llm_clients.codex_preflight
python main.py
또는 CLI/UI에서 provider를 codex로 선택.
9. 장애 대응
9.1 malformed JSON
대응:
- 1회 재시도
- 재시도 prompt:
- “Your previous output was invalid JSON. Return valid JSON matching the schema only.”
- 그래도 실패하면 예외 raise
9.2 app-server 시작 실패
대응:
- binary path 재확인
codex --version확인- PATH 문제면
codex_binary절대경로 사용
9.3 로그인/권한 문제
대응:
codex login- headless면
codex login --device-auth cli_auth_credentials_store="file"설정~/.codex/auth.json존재 여부 확인
9.4 rate limit
대응:
account/rateLimits/read노출- 재시도(backoff)
- 긴 배치 작업은 serialized run
- 필요 시 Codex credits 사용 고려
9.5 thread log 과다 생성
대응:
thread/unsubscribe기본 수행.codex-log별도 디렉터리 사용- 오래된 로그 cleanup script 추가
10. 권장 구현 순서
Phase 1
- provider 추가
- app-server connection 추가
- plain invoke만 먼저 연결
- preflight 추가
Phase 2
bind_tools()+ tool schema oneOf 구현- analyst nodes smoke test
Phase 3
- CLI/UI 옵션 추가
- README/docs 작성
- 통합 테스트 보강
Phase 4
- malformed JSON retry
- rate limit/backoff
- log cleanup / diagnostics
11. 최종 권장안 요약
가장 좋은 구현 방식
TradingAgents에 codex provider를 새로 추가하고, 내부에서 codex app-server와 stdio(JSONL)로 통신하는 LangChain 커스텀 ChatModel을 구현한다.
tool calling은 Codex dynamicTools를 쓰지 말고, outputSchema + JSON oneOf 방식으로 모델 응답을 final 또는 tool_calls 형태로 강제한다.
이 방식의 장점
- OpenAI API key 불필요
- ChatGPT Pro / Codex 로그인 재사용 가능
- TradingAgents의 기존 ToolNode / graph 구조 유지
- Python 프로젝트에 자연스럽게 통합 가능
- dynamicTools 실험 API 의존 최소화
- 추후 유지보수 포인트가 명확함
반드시 지켜야 할 운영 원칙
- 직접 OAuth refresh 구현 금지
auth.json은 비밀 취급codex login또는 device-auth 우선- one auth cache per trusted runner / serialized workflow
- Codex를 모델 백엔드로만 쓰고 shell/web 기능은 최대한 비활성화
12. 최소 수용 기준(Acceptance Criteria)
아래가 모두 충족되면 구현 성공으로 간주한다.
llm_provider="codex"설정으로 TradingAgents가 실행된다.- API key 없이
codex login상태에서 동작한다. - analyst 노드가
bind_tools()를 통해 tool call을 생성하고 ToolNode가 이를 실행한다. - manager/trader/risk nodes가 plain
invoke()로 정상 응답한다. AAPL또는SPY에 대해 최소 1개 analyst + 전체 graph smoke run이 성공한다.- malformed JSON, auth missing, binary missing, model missing에 대한 에러 메시지가 명확하다.
- README와 preflight가 포함된다.