agentforge-multi 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +178 -0
- package/README.md +178 -0
- package/agentforge +693 -0
- package/package.json +35 -0
- package/scripts/postinstall.js +83 -0
package/README.ko.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# AgentForge
|
|
2
|
+
|
|
3
|
+
> **Worker**와 **Evaluator**, 두 에이전트가 협력하며 목표를 달성하는 멀티에이전트 터미널 CLI
|
|
4
|
+
|
|
5
|
+
[OpenCode](https://opencode.ai)에서 영감을 받아 제작된 AgentForge는 AI 코딩 CLI(현재 `codex`, 향후 Claude·Gemini 지원 예정)를 감싸 실행 에이전트와 평가 에이전트가 반복 협력하는 루프를 구현합니다.
|
|
6
|
+
|
|
7
|
+
[English README](./README.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## 화면 구성
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
┌─ ⚙ WORKER AGENT ──────────────────────┬─ ◈ EVALUATOR AGENT ────────────────────┐
|
|
15
|
+
│ │ │
|
|
16
|
+
│ > Reading App.tsx... │ [반복 1] │
|
|
17
|
+
│ > Writing dark_mode.css... │ IMPROVE: │
|
|
18
|
+
│ > Modifying index.html... │ 토글 버튼이 없음. │
|
|
19
|
+
│ ▌ │ localStorage에 상태 저장 필요. │
|
|
20
|
+
│ │ │
|
|
21
|
+
│ [반복 2] │ [반복 2] │
|
|
22
|
+
│ > Adding ThemeToggle.tsx... │ ✓ DONE │
|
|
23
|
+
│ ✓ Created 2 files │ 결과물: ./src/ThemeToggle.tsx │
|
|
24
|
+
│ │ │
|
|
25
|
+
└────────────────────────────────────────┴─────────────────────────────────────────┘
|
|
26
|
+
[AgentForge] > /plan 리액트 앱에 다크모드 추가해줘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## 동작 원리
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
사용자 입력 (목표)
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
┌─────────────┐ 코드 변경 실행 ┌────────────────┐
|
|
38
|
+
│ Worker │ ──────────────────► │ 파일 시스템 │
|
|
39
|
+
│ Agent │ (full-auto sandbox) └────────────────┘
|
|
40
|
+
└──────┬──────┘
|
|
41
|
+
│ 실행 결과
|
|
42
|
+
▼
|
|
43
|
+
┌──────────────────┐
|
|
44
|
+
│ Evaluator │
|
|
45
|
+
│ Agent │ (read-only sandbox — 파일 수정 불가)
|
|
46
|
+
└────────┬─────────┘
|
|
47
|
+
│
|
|
48
|
+
├── DONE → 한국어 요약 출력 후 다음 명령 대기
|
|
49
|
+
├── IMPROVE → 피드백을 Worker에 전달 후 재실행
|
|
50
|
+
└── REDIRECT → 전략 변경 후 재실행
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
DONE 판정이 나거나 최대 반복 횟수에 도달할 때까지 루프가 계속됩니다.
|
|
54
|
+
|
|
55
|
+
### DONE 시 출력 예시
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
════════════════ ✓ 완료 — 3번 반복 ════════════════
|
|
59
|
+
|
|
60
|
+
판단 이유
|
|
61
|
+
목표로 제시된 다크모드가 정상적으로 구현되었습니다.
|
|
62
|
+
토글 버튼이 추가되었고 상태가 localStorage에 저장됩니다.
|
|
63
|
+
|
|
64
|
+
결과물 위치
|
|
65
|
+
• ./src/ThemeToggle.tsx
|
|
66
|
+
• ./src/App.tsx (수정됨)
|
|
67
|
+
|
|
68
|
+
결과 요약
|
|
69
|
+
리액트 컴포넌트 기반 다크모드 구현. 새로고침 후에도 상태 유지.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## 요구사항
|
|
75
|
+
|
|
76
|
+
- Python 3.10 이상
|
|
77
|
+
- [`codex` CLI](https://github.com/openai/codex) — 설치 및 로그인 완료
|
|
78
|
+
- Python 패키지: `rich`, `prompt_toolkit`
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 설치
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git clone https://github.com/<your-username>/AgentForge.git
|
|
86
|
+
cd AgentForge
|
|
87
|
+
bash install.sh
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
또는 수동 설치:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
cp agentforge ~/.local/bin/agentforge
|
|
94
|
+
chmod +x ~/.local/bin/agentforge
|
|
95
|
+
pip install rich prompt_toolkit
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## 사용법
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
agentforge # 인터랙티브 CLI 실행
|
|
104
|
+
agentforge -d /my/project # 작업 디렉토리 지정
|
|
105
|
+
agentforge -n 20 # 최대 반복 횟수 지정 (기본: 5000)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### 슬래시 커맨드
|
|
109
|
+
|
|
110
|
+
| 커맨드 | 설명 |
|
|
111
|
+
|--------|------|
|
|
112
|
+
| `<목표 입력>` | Worker 에이전트에게 즉시 전달, 루프 시작 |
|
|
113
|
+
| `/plan <목표>` | Plan Agent가 계획 수립 → 질의응답 → 확인 → 실행 |
|
|
114
|
+
| `/exit` | AgentForge 종료 |
|
|
115
|
+
|
|
116
|
+
> `/` 를 입력하면 사용 가능한 커맨드가 자동완성으로 표시됩니다 (Claude Code 스타일).
|
|
117
|
+
|
|
118
|
+
### /plan 흐름
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
[AgentForge] > /plan 안녕을 출력하는 단순 웹 만들어줘
|
|
122
|
+
|
|
123
|
+
[Plan Agent]
|
|
124
|
+
계획:
|
|
125
|
+
- index.html 생성
|
|
126
|
+
- <h1>안녕</h1> 포함
|
|
127
|
+
|
|
128
|
+
accept (y/n) > y
|
|
129
|
+
|
|
130
|
+
▶ Worker + Evaluator 루프를 시작합니다...
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 옵션
|
|
136
|
+
|
|
137
|
+
| 플래그 | 기본값 | 설명 |
|
|
138
|
+
|--------|--------|------|
|
|
139
|
+
| `-d DIR` | `.` | 작업 디렉토리 |
|
|
140
|
+
| `-n N` | `5000` | 최대 반복 횟수 |
|
|
141
|
+
| `--worker-model M` | config 기본값 | Worker 에이전트 모델 |
|
|
142
|
+
| `--eval-model M` | config 기본값 | Evaluator 에이전트 모델 |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## 로드맵
|
|
147
|
+
|
|
148
|
+
- [x] `codex` CLI 백엔드
|
|
149
|
+
- [ ] Claude CLI 백엔드
|
|
150
|
+
- [ ] Gemini CLI 백엔드
|
|
151
|
+
- [ ] 에이전트 커스텀 페르소나 설정
|
|
152
|
+
- [ ] 세션 히스토리 내보내기
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 프로젝트 구조
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
AgentForge/
|
|
160
|
+
├── agentforge # 메인 실행 스크립트
|
|
161
|
+
├── install.sh # 설치 스크립트
|
|
162
|
+
├── README.md # 영어 README
|
|
163
|
+
├── README.ko.md # 한국어 README (이 파일)
|
|
164
|
+
└── .gitignore
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## 참고
|
|
170
|
+
|
|
171
|
+
- [OpenCode](https://opencode.ai) — 영감의 출처, 터미널 퍼스트 AI 코딩 에이전트
|
|
172
|
+
- [Codex CLI](https://github.com/openai/codex) — 현재 기반 엔진
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## 라이선스
|
|
177
|
+
|
|
178
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
# AgentForge
|
|
2
|
+
|
|
3
|
+
> A multi-agent CLI that forges code through a **Worker** and an **Evaluator** collaborating in a loop — until the goal is achieved.
|
|
4
|
+
|
|
5
|
+
Inspired by [OpenCode](https://opencode.ai), AgentForge wraps AI coding CLIs (currently `codex`, with Claude and Gemini support planned) to orchestrate two specialized agents: one that acts, one that judges.
|
|
6
|
+
|
|
7
|
+
[한국어 README](./README.ko.md)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Demo
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
┌─ ⚙ WORKER AGENT ──────────────────────┬─ ◈ EVALUATOR AGENT ────────────────────┐
|
|
15
|
+
│ │ │
|
|
16
|
+
│ > Reading App.tsx... │ [Iter 1] │
|
|
17
|
+
│ > Writing dark_mode.css... │ IMPROVE: │
|
|
18
|
+
│ > Modifying index.html... │ Toggle button is missing. │
|
|
19
|
+
│ ▌ │ Save state to localStorage. │
|
|
20
|
+
│ │ │
|
|
21
|
+
│ [Iter 2] │ [Iter 2] │
|
|
22
|
+
│ > Adding ThemeToggle.tsx... │ ✓ DONE │
|
|
23
|
+
│ ✓ Created 2 files │ 결과물: ./src/ThemeToggle.tsx │
|
|
24
|
+
│ │ │
|
|
25
|
+
└────────────────────────────────────────┴─────────────────────────────────────────┘
|
|
26
|
+
[AgentForge] > /plan Add dark mode to the React app
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## How It Works
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
User input (goal)
|
|
35
|
+
│
|
|
36
|
+
▼
|
|
37
|
+
┌─────────────┐ code changes ┌────────────────┐
|
|
38
|
+
│ Worker │ ───────────────────► │ file system │
|
|
39
|
+
│ Agent │ (full-auto sandbox) └────────────────┘
|
|
40
|
+
└──────┬──────┘
|
|
41
|
+
│ output
|
|
42
|
+
▼
|
|
43
|
+
┌──────────────────┐
|
|
44
|
+
│ Evaluator │
|
|
45
|
+
│ Agent │ (read-only sandbox — cannot modify files)
|
|
46
|
+
└────────┬─────────┘
|
|
47
|
+
│
|
|
48
|
+
├── DONE → Print Korean summary, wait for next command
|
|
49
|
+
├── IMPROVE → Send feedback to Worker → repeat
|
|
50
|
+
└── REDIRECT → Change strategy entirely → repeat
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The loop continues until the Evaluator decides `DONE` or the iteration limit is reached.
|
|
54
|
+
|
|
55
|
+
### DONE Output Example
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
════════════════ ✓ Done — 3 iterations ════════════════
|
|
59
|
+
|
|
60
|
+
판단 이유
|
|
61
|
+
Dark mode has been fully implemented with a toggle button.
|
|
62
|
+
The theme state persists via localStorage across page reloads.
|
|
63
|
+
|
|
64
|
+
결과물 위치
|
|
65
|
+
• ./src/ThemeToggle.tsx
|
|
66
|
+
• ./src/App.tsx (modified)
|
|
67
|
+
|
|
68
|
+
결과 요약
|
|
69
|
+
React-based dark mode with persistent state. No extra dependencies.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Requirements
|
|
75
|
+
|
|
76
|
+
- Python 3.10+
|
|
77
|
+
- [`codex` CLI](https://github.com/openai/codex) — installed and authenticated
|
|
78
|
+
- Python packages: `rich`, `prompt_toolkit`
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Installation
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
git clone https://github.com/<your-username>/AgentForge.git
|
|
86
|
+
cd AgentForge
|
|
87
|
+
bash install.sh
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Or manually:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
cp agentforge ~/.local/bin/agentforge
|
|
94
|
+
chmod +x ~/.local/bin/agentforge
|
|
95
|
+
pip install rich prompt_toolkit
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Usage
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
agentforge # Launch interactive CLI
|
|
104
|
+
agentforge -d /my/project # Set working directory
|
|
105
|
+
agentforge -n 20 # Set max iterations (default: 5000)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### Slash Commands
|
|
109
|
+
|
|
110
|
+
| Command | Description |
|
|
111
|
+
|---------|-------------|
|
|
112
|
+
| `<goal text>` | Send goal directly to the Worker agent and start the loop |
|
|
113
|
+
| `/plan <goal>` | Plan Agent drafts a plan → Q&A → confirm → execute |
|
|
114
|
+
| `/exit` | Exit AgentForge |
|
|
115
|
+
|
|
116
|
+
> Type `/` to see available commands with autocomplete (like Claude Code).
|
|
117
|
+
|
|
118
|
+
### /plan Flow
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
[AgentForge] > /plan Build a simple web page that says hello
|
|
122
|
+
|
|
123
|
+
[Plan Agent]
|
|
124
|
+
계획:
|
|
125
|
+
- Create index.html
|
|
126
|
+
- Add <h1>hello</h1>
|
|
127
|
+
|
|
128
|
+
accept (y/n) > y
|
|
129
|
+
|
|
130
|
+
▶ Starting Worker + Evaluator loop...
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Options
|
|
136
|
+
|
|
137
|
+
| Flag | Default | Description |
|
|
138
|
+
|------|---------|-------------|
|
|
139
|
+
| `-d DIR` | `.` | Working directory |
|
|
140
|
+
| `-n N` | `5000` | Max iterations |
|
|
141
|
+
| `--worker-model M` | config default | Model for Worker agent |
|
|
142
|
+
| `--eval-model M` | config default | Model for Evaluator agent |
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Roadmap
|
|
147
|
+
|
|
148
|
+
- [x] `codex` CLI backend
|
|
149
|
+
- [ ] Claude CLI backend
|
|
150
|
+
- [ ] Gemini CLI backend
|
|
151
|
+
- [ ] Configurable agent personas
|
|
152
|
+
- [ ] Session history export
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Project Structure
|
|
157
|
+
|
|
158
|
+
```
|
|
159
|
+
AgentForge/
|
|
160
|
+
├── agentforge # Main executable script
|
|
161
|
+
├── install.sh # Installation script
|
|
162
|
+
├── README.md # English README (this file)
|
|
163
|
+
├── README.ko.md # Korean README
|
|
164
|
+
└── .gitignore
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Inspiration
|
|
170
|
+
|
|
171
|
+
- [OpenCode](https://opencode.ai) — terminal-first AI coding agent
|
|
172
|
+
- [Codex CLI](https://github.com/openai/codex) — current underlying engine
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## License
|
|
177
|
+
|
|
178
|
+
MIT
|
package/agentforge
ADDED
|
@@ -0,0 +1,693 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
agentforge — Worker + Evaluator 인터랙티브 멀티에이전트 CLI
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
agentforge [-d DIR] [--worker-model M] [--eval-model M]
|
|
7
|
+
|
|
8
|
+
Slash commands:
|
|
9
|
+
/exit 종료
|
|
10
|
+
/plan <목표> 계획 수립 후 실행
|
|
11
|
+
<일반 텍스트> Worker에게 즉시 전달 (목표 설정)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import argparse
|
|
15
|
+
import re
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
import textwrap
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from collections import deque
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.layout import Layout
|
|
27
|
+
from rich.live import Live
|
|
28
|
+
from rich.panel import Panel
|
|
29
|
+
from rich.rule import Rule
|
|
30
|
+
from rich.text import Text
|
|
31
|
+
from rich import box
|
|
32
|
+
|
|
33
|
+
from prompt_toolkit import prompt as pt_prompt
|
|
34
|
+
from prompt_toolkit.completion import Completer, Completion
|
|
35
|
+
from prompt_toolkit.styles import Style as PtStyle
|
|
36
|
+
from prompt_toolkit.formatted_text import HTML
|
|
37
|
+
|
|
38
|
+
# ── Constants ─────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
CODEX_BIN = Path.home() / ".npm-global" / "bin" / "codex"
|
|
41
|
+
DEFAULT_MAX_ITER = 5000
|
|
42
|
+
WORKER_BUF_LINES = 60
|
|
43
|
+
ANSI_RE = re.compile(r'\x1b\[[0-9;]*[a-zA-Z]|\x1b\][^\x07]*\x07')
|
|
44
|
+
DECISION_RE = re.compile(r'^(DONE|IMPROVE|REDIRECT)(?::\s*(.*))?$', re.I | re.M)
|
|
45
|
+
NOISE_RE = re.compile(r'^\s*([\-─═\s]+)?$')
|
|
46
|
+
|
|
47
|
+
console = Console()
|
|
48
|
+
|
|
49
|
+
# ── Slash command autocomplete ────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
SLASH_COMMANDS = [
|
|
52
|
+
("/plan", "계획을 수립한 뒤 Worker + Evaluator 루프 실행"),
|
|
53
|
+
("/exit", "agentforge 종료"),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
class SlashCompleter(Completer):
|
|
57
|
+
def get_completions(self, document, complete_event):
|
|
58
|
+
text = document.text_before_cursor
|
|
59
|
+
if text.startswith('/'):
|
|
60
|
+
typed = text.lstrip('/')
|
|
61
|
+
for cmd, desc in SLASH_COMMANDS:
|
|
62
|
+
name = cmd.lstrip('/')
|
|
63
|
+
if name.startswith(typed):
|
|
64
|
+
yield Completion(
|
|
65
|
+
cmd,
|
|
66
|
+
start_position=-len(text),
|
|
67
|
+
display=HTML(f'<ansicyan>{cmd}</ansicyan>'),
|
|
68
|
+
display_meta=HTML(f'<ansiwhite>{desc}</ansiwhite>'),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
PROMPT_STYLE = PtStyle.from_dict({
|
|
72
|
+
'prompt': 'ansicyan bold',
|
|
73
|
+
'completion-menu.completion': 'bg:#1e1e2e #cdd6f4',
|
|
74
|
+
'completion-menu.completion.current': 'bg:#313244 #cba6f7 bold',
|
|
75
|
+
'completion-menu.meta.completion': 'bg:#1e1e2e #6c7086',
|
|
76
|
+
'completion-menu.meta.completion.current': 'bg:#313244 #bac2de',
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
# ── Output colorizer ──────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
def colorize_line(line: str) -> str:
|
|
82
|
+
"""Worker 출력 라인에 문맥 기반 색상 적용."""
|
|
83
|
+
s = line.strip()
|
|
84
|
+
if not s:
|
|
85
|
+
return ""
|
|
86
|
+
# 주석 → 초록
|
|
87
|
+
if s.startswith(('#', '//', '/*', '*')):
|
|
88
|
+
return f"[green]{line}[/green]"
|
|
89
|
+
# 에러/경고 → 빨강
|
|
90
|
+
if any(w in s.lower() for w in ['error', 'traceback', 'exception', 'failed', 'fatal', 'errno']):
|
|
91
|
+
return f"[red]{line}[/red]"
|
|
92
|
+
# 성공 표시 → 초록
|
|
93
|
+
if any(w in s for w in ['✓', 'Created', 'Modified', 'written', 'Success', 'complete', 'Saved']):
|
|
94
|
+
return f"[green]{line}[/green]"
|
|
95
|
+
# 파일/작업 → 시안
|
|
96
|
+
if s.startswith(('>', 'Writing', 'Reading', 'Creating', 'Updating', 'Deleting', 'Running')):
|
|
97
|
+
return f"[cyan]{line}[/cyan]"
|
|
98
|
+
if s.startswith(('+', '-')) and len(s) > 1 and not s.startswith('--'):
|
|
99
|
+
return f"[cyan]{line}[/cyan]"
|
|
100
|
+
# 코드 키워드 → 밝은 흰색
|
|
101
|
+
if any(kw in s for kw in ['def ', 'class ', 'import ', 'from ', 'function ', 'const ', 'let ', 'var ', 'async ']):
|
|
102
|
+
return f"[bright_white]{line}[/bright_white]"
|
|
103
|
+
# 문자열 → 노란색
|
|
104
|
+
if (s.count('"') >= 2 or s.count("'") >= 2) and len(s) < 80:
|
|
105
|
+
return f"[yellow]{line}[/yellow]"
|
|
106
|
+
return line
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def strip_ansi(text: str) -> str:
|
|
110
|
+
return ANSI_RE.sub('', text)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ── Prompt Templates ──────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
WORKER_SYSTEM = textwrap.dedent("""\
|
|
116
|
+
You are a skilled software engineer executing a task step by step.
|
|
117
|
+
Your work will be reviewed by an evaluator agent.
|
|
118
|
+
Make concrete, real code changes — do not just describe what to do.
|
|
119
|
+
Be thorough and complete.
|
|
120
|
+
""").strip()
|
|
121
|
+
|
|
122
|
+
EVALUATOR_SYSTEM = textwrap.dedent("""\
|
|
123
|
+
You are a strict code reviewer. You do NOT modify any files.
|
|
124
|
+
Evaluate whether the goal has been achieved.
|
|
125
|
+
|
|
126
|
+
Respond with EXACTLY ONE of the following as your very first line:
|
|
127
|
+
|
|
128
|
+
DONE
|
|
129
|
+
(goal is fully and correctly achieved)
|
|
130
|
+
|
|
131
|
+
IMPROVE: <specific actionable feedback>
|
|
132
|
+
(progress made but refinements needed — same approach)
|
|
133
|
+
|
|
134
|
+
REDIRECT: <description of a completely different approach>
|
|
135
|
+
(current approach is fundamentally wrong)
|
|
136
|
+
|
|
137
|
+
When responding with DONE, after the keyword write a Korean summary in this EXACT format:
|
|
138
|
+
|
|
139
|
+
판단 이유: <목표 달성 근거 2~3문장>
|
|
140
|
+
결과물 위치: <생성/수정된 파일 경로 목록>
|
|
141
|
+
결과 요약: <무엇이 만들어졌는지 한 문장>
|
|
142
|
+
|
|
143
|
+
After IMPROVE or REDIRECT, write the feedback in Korean if possible.
|
|
144
|
+
Do NOT write anything before the decision keyword.
|
|
145
|
+
""").strip()
|
|
146
|
+
|
|
147
|
+
PLAN_SYSTEM = textwrap.dedent("""\
|
|
148
|
+
You are a planning agent. DO NOT write any code or modify any files.
|
|
149
|
+
Your job is to create a detailed implementation plan in Korean.
|
|
150
|
+
|
|
151
|
+
If anything is unclear, list your questions first in this format:
|
|
152
|
+
질문:
|
|
153
|
+
1. ...
|
|
154
|
+
2. ...
|
|
155
|
+
|
|
156
|
+
If everything is clear, output the plan directly:
|
|
157
|
+
계획:
|
|
158
|
+
- ...
|
|
159
|
+
- ...
|
|
160
|
+
|
|
161
|
+
Be specific and actionable. No code, only plan.
|
|
162
|
+
""").strip()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build_worker_prompt(goal: str, history: list) -> str:
|
|
166
|
+
lines = [f"GOAL: {goal}", ""]
|
|
167
|
+
if not history:
|
|
168
|
+
lines += [WORKER_SYSTEM, "", "Begin working on the goal above. Make concrete changes now."]
|
|
169
|
+
else:
|
|
170
|
+
lines.append("ITERATION HISTORY:")
|
|
171
|
+
for h in history:
|
|
172
|
+
lines.append(f" Iteration {h['iter']}:")
|
|
173
|
+
lines.append(f" Your work: {h['worker_summary']}")
|
|
174
|
+
lines.append(f" Evaluator: {h['decision']}" +
|
|
175
|
+
(f" — {h['feedback']}" if h['feedback'] else ""))
|
|
176
|
+
lines.append("")
|
|
177
|
+
last = history[-1]
|
|
178
|
+
if last['decision'].upper() == 'IMPROVE':
|
|
179
|
+
lines.append(f"INSTRUCTION: Refine your previous work. Feedback: {last['feedback']}")
|
|
180
|
+
else:
|
|
181
|
+
lines.append(f"INSTRUCTION: Abandon previous approach. Try differently: {last['feedback']}")
|
|
182
|
+
lines.append("Make the changes now.")
|
|
183
|
+
return "\n".join(lines)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def build_evaluator_prompt(goal: str, worker_output: str, iteration: int) -> str:
|
|
187
|
+
return "\n".join([
|
|
188
|
+
EVALUATOR_SYSTEM,
|
|
189
|
+
"",
|
|
190
|
+
f"ORIGINAL GOAL: {goal}",
|
|
191
|
+
f"CURRENT ITERATION: {iteration}",
|
|
192
|
+
"",
|
|
193
|
+
"WORKER OUTPUT FROM THIS ITERATION:",
|
|
194
|
+
"─" * 60,
|
|
195
|
+
worker_output[-3000:].strip(),
|
|
196
|
+
"─" * 60,
|
|
197
|
+
"",
|
|
198
|
+
"Now evaluate. First line must be DONE, IMPROVE: ..., or REDIRECT: ...",
|
|
199
|
+
])
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def build_plan_prompt(goal: str, qa_history: list) -> str:
|
|
203
|
+
lines = [PLAN_SYSTEM, "", f"목표: {goal}"]
|
|
204
|
+
if qa_history:
|
|
205
|
+
lines.append("")
|
|
206
|
+
lines.append("이전 질의응답:")
|
|
207
|
+
for qa in qa_history:
|
|
208
|
+
lines.append(f" Q: {qa['q']}")
|
|
209
|
+
lines.append(f" A: {qa['a']}")
|
|
210
|
+
lines.append("")
|
|
211
|
+
lines.append("위 정보를 바탕으로 계획을 수립하거나 질문을 출력하라.")
|
|
212
|
+
return "\n".join(lines)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# ── Agent Runners ─────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
def _run_codex(cmd: list, workdir: str, buf: deque | None,
|
|
218
|
+
status_ref: list | None) -> tuple[str, int]:
|
|
219
|
+
"""codex exec 실행. buf가 있으면 stdout 스트리밍. 최종 메시지 반환."""
|
|
220
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f:
|
|
221
|
+
out_file = f.name
|
|
222
|
+
|
|
223
|
+
full_cmd = cmd + ["-o", out_file]
|
|
224
|
+
try:
|
|
225
|
+
if buf is not None:
|
|
226
|
+
if status_ref:
|
|
227
|
+
status_ref[0] = "running"
|
|
228
|
+
proc = subprocess.Popen(
|
|
229
|
+
full_cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
230
|
+
text=True, errors='replace',
|
|
231
|
+
)
|
|
232
|
+
prev_line = ""
|
|
233
|
+
for raw in proc.stdout:
|
|
234
|
+
clean = strip_ansi(raw).rstrip()
|
|
235
|
+
if clean and not NOISE_RE.match(clean) and clean != prev_line:
|
|
236
|
+
buf.append(colorize_line(clean))
|
|
237
|
+
prev_line = clean
|
|
238
|
+
proc.wait()
|
|
239
|
+
rc = proc.returncode
|
|
240
|
+
if status_ref:
|
|
241
|
+
status_ref[0] = "done"
|
|
242
|
+
else:
|
|
243
|
+
proc = subprocess.run(
|
|
244
|
+
full_cmd, capture_output=True, text=True, errors='replace',
|
|
245
|
+
)
|
|
246
|
+
rc = proc.returncode
|
|
247
|
+
except Exception as e:
|
|
248
|
+
if buf is not None:
|
|
249
|
+
buf.append(f"[red][ERROR] {e}[/red]")
|
|
250
|
+
if status_ref:
|
|
251
|
+
status_ref[0] = "error"
|
|
252
|
+
rc = 1
|
|
253
|
+
|
|
254
|
+
last_msg = ""
|
|
255
|
+
try:
|
|
256
|
+
last_msg = strip_ansi(Path(out_file).read_text()).strip()
|
|
257
|
+
except Exception:
|
|
258
|
+
if buf:
|
|
259
|
+
last_msg = strip_ansi("\n".join(list(buf)[-10:]))
|
|
260
|
+
finally:
|
|
261
|
+
try:
|
|
262
|
+
Path(out_file).unlink()
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
|
|
266
|
+
return last_msg, rc
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def run_worker(prompt: str, workdir: str, model: str | None,
|
|
270
|
+
buf: deque, status_ref: list) -> tuple[str, int]:
|
|
271
|
+
cmd = [
|
|
272
|
+
str(CODEX_BIN), "exec",
|
|
273
|
+
"--full-auto", "--skip-git-repo-check", "--color", "never",
|
|
274
|
+
"-C", workdir,
|
|
275
|
+
]
|
|
276
|
+
if model:
|
|
277
|
+
cmd += ["-m", model]
|
|
278
|
+
cmd.append(prompt)
|
|
279
|
+
return _run_codex(cmd, workdir, buf, status_ref)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def run_evaluator(prompt: str, workdir: str, model: str | None) -> tuple[str, int]:
|
|
283
|
+
cmd = [
|
|
284
|
+
str(CODEX_BIN), "exec",
|
|
285
|
+
"-s", "read-only", "--skip-git-repo-check", "--color", "never",
|
|
286
|
+
"-C", workdir,
|
|
287
|
+
]
|
|
288
|
+
if model:
|
|
289
|
+
cmd += ["-m", model]
|
|
290
|
+
cmd.append(prompt)
|
|
291
|
+
return _run_codex(cmd, workdir, None, None)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def run_plan_agent(prompt: str, workdir: str, model: str | None) -> tuple[str, int]:
|
|
295
|
+
cmd = [
|
|
296
|
+
str(CODEX_BIN), "exec",
|
|
297
|
+
"-s", "read-only", "--skip-git-repo-check", "--color", "never",
|
|
298
|
+
"-C", workdir,
|
|
299
|
+
]
|
|
300
|
+
if model:
|
|
301
|
+
cmd += ["-m", model]
|
|
302
|
+
cmd.append(prompt)
|
|
303
|
+
return _run_codex(cmd, workdir, None, None)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def parse_decision(text: str) -> tuple[str, str]:
|
|
307
|
+
m = DECISION_RE.search(text)
|
|
308
|
+
if m:
|
|
309
|
+
return m.group(1).upper(), (m.group(2) or "").strip()
|
|
310
|
+
lower = text.lower()
|
|
311
|
+
if any(w in lower for w in ['task is complete', 'fully complete', 'goal achieved', 'all done']):
|
|
312
|
+
return 'DONE', ''
|
|
313
|
+
return 'IMPROVE', f"(parse failed) {text[:200]}"
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# ── TUI ───────────────────────────────────────────────────────────────────────
|
|
317
|
+
|
|
318
|
+
def make_layout() -> Layout:
|
|
319
|
+
layout = Layout()
|
|
320
|
+
layout.split(
|
|
321
|
+
Layout(name="header", size=3),
|
|
322
|
+
Layout(name="body"),
|
|
323
|
+
)
|
|
324
|
+
layout["body"].split_row(
|
|
325
|
+
Layout(name="worker"),
|
|
326
|
+
Layout(name="evaluator"),
|
|
327
|
+
)
|
|
328
|
+
return layout
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def render_header(goal: str, iteration: int, max_iter: int, phase: str) -> Panel:
|
|
332
|
+
g = goal[:72] + "..." if len(goal) > 72 else goal
|
|
333
|
+
status = {
|
|
334
|
+
"idle": "[dim]명령 대기 중 /plan <목표> 또는 <목표> 입력[/dim]",
|
|
335
|
+
"worker": "[yellow]⚙ Worker 실행 중...[/yellow]",
|
|
336
|
+
"evaluator": "[magenta]◈ Evaluator 평가 중...[/magenta]",
|
|
337
|
+
"planning": "[cyan]◑ Plan Agent 실행 중...[/cyan]",
|
|
338
|
+
"done": "[bold green]✓ 완료[/bold green]",
|
|
339
|
+
"max": "[bold red]최대 반복 도달[/bold red]",
|
|
340
|
+
}.get(phase, phase)
|
|
341
|
+
iter_str = f" Iter [bold]{iteration}[/bold]/{max_iter}" if iteration > 0 else ""
|
|
342
|
+
content = f"[bold cyan]Goal:[/bold cyan] {g}\n{status}{iter_str}"
|
|
343
|
+
return Panel(content, border_style="cyan", box=box.HEAVY_HEAD)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def render_worker_panel(buf: deque, iteration: int, status: str,
|
|
347
|
+
history: list, done: bool) -> Panel:
|
|
348
|
+
lines = []
|
|
349
|
+
for h in history[-3:]: # 최근 3 iteration만 dim으로 표시
|
|
350
|
+
lines.append(f"[dim]── Iter {h['iter']} {'─'*30}[/dim]")
|
|
351
|
+
for l in h.get('worker_lines', [])[-5:]:
|
|
352
|
+
lines.append(f"[dim]{l}[/dim]")
|
|
353
|
+
if buf or status == "running":
|
|
354
|
+
if history:
|
|
355
|
+
lines.append(f"[yellow]── Iter {iteration} {'─'*30}[/yellow]")
|
|
356
|
+
for line in list(buf):
|
|
357
|
+
lines.append(line)
|
|
358
|
+
if status == "running":
|
|
359
|
+
lines.append("[yellow blink]▌[/yellow blink]")
|
|
360
|
+
border = "green" if done else "yellow"
|
|
361
|
+
icon = "✓" if done else "⚙"
|
|
362
|
+
content = "\n".join(lines) if lines else "[dim]명령을 입력하면 작업을 시작합니다.[/dim]"
|
|
363
|
+
return Panel(
|
|
364
|
+
Text.from_markup(content),
|
|
365
|
+
title=f"[{border}]{icon} WORKER AGENT[/{border}]",
|
|
366
|
+
border_style=border, box=box.ROUNDED, padding=(0, 1),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def render_evaluator_panel(eval_history: list, iteration: int,
|
|
371
|
+
phase: str, done: bool) -> Panel:
|
|
372
|
+
lines = []
|
|
373
|
+
for e in eval_history:
|
|
374
|
+
d, fb, fm = e['decision'], e['feedback'], e.get('full_msg', '')
|
|
375
|
+
lines.append(f"[dim]── Iter {e['iter']} {'─'*28}[/dim]")
|
|
376
|
+
if d == 'DONE':
|
|
377
|
+
lines.append("[bold green]✓ DONE[/bold green]")
|
|
378
|
+
# 한국어 요약 섹션 추출
|
|
379
|
+
for section in ['판단 이유', '결과물 위치', '결과 요약']:
|
|
380
|
+
m = re.search(rf'{section}:\s*(.+?)(?=\n판단|결과물|결과 요약|$)', fm, re.S)
|
|
381
|
+
if m:
|
|
382
|
+
val = m.group(1).strip()[:120]
|
|
383
|
+
lines.append(f"[dim]{section}:[/dim] [white]{val}[/white]")
|
|
384
|
+
elif d == 'IMPROVE':
|
|
385
|
+
lines.append("[bold yellow]IMPROVE[/bold yellow]")
|
|
386
|
+
for l in textwrap.wrap(fb, 42):
|
|
387
|
+
lines.append(f"[yellow]{l}[/yellow]")
|
|
388
|
+
elif d == 'REDIRECT':
|
|
389
|
+
lines.append("[bold red]REDIRECT[/bold red]")
|
|
390
|
+
for l in textwrap.wrap(fb, 42):
|
|
391
|
+
lines.append(f"[red]{l}[/red]")
|
|
392
|
+
lines.append("")
|
|
393
|
+
if phase == "evaluator":
|
|
394
|
+
lines.append(f"[magenta]── Iter {iteration} {'─'*28}[/magenta]")
|
|
395
|
+
lines.append("[magenta blink]평가 중...[/magenta blink]")
|
|
396
|
+
elif phase == "planning":
|
|
397
|
+
lines.append("[cyan blink]계획 수립 중...[/cyan blink]")
|
|
398
|
+
border = "green" if done else "magenta"
|
|
399
|
+
icon = "✓" if done else "◈"
|
|
400
|
+
content = "\n".join(lines) if lines else "[dim]Worker 완료 후 평가를 시작합니다.[/dim]"
|
|
401
|
+
return Panel(
|
|
402
|
+
Text.from_markup(content),
|
|
403
|
+
title=f"[{border}]{icon} EVALUATOR AGENT[/{border}]",
|
|
404
|
+
border_style=border, box=box.ROUNDED, padding=(0, 1),
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ── Plan Mode ─────────────────────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
def run_plan_mode(raw_goal: str, workdir: str, model: str | None,
|
|
411
|
+
layout: Layout, live: Live) -> str | None:
|
|
412
|
+
"""
|
|
413
|
+
/plan 모드: Plan Agent와 대화 후 최종 계획(목표 문자열)을 반환.
|
|
414
|
+
취소 시 None 반환.
|
|
415
|
+
"""
|
|
416
|
+
goal = raw_goal.strip()
|
|
417
|
+
if not goal:
|
|
418
|
+
live.stop()
|
|
419
|
+
try:
|
|
420
|
+
goal = input("계획할 목표를 입력하세요: ").strip()
|
|
421
|
+
except (EOFError, KeyboardInterrupt):
|
|
422
|
+
return None
|
|
423
|
+
if not goal:
|
|
424
|
+
return None
|
|
425
|
+
|
|
426
|
+
qa_history = []
|
|
427
|
+
|
|
428
|
+
while True:
|
|
429
|
+
# Plan Agent 실행
|
|
430
|
+
layout["header"].update(render_header(goal, 0, 0, "planning"))
|
|
431
|
+
layout["worker"].update(render_worker_panel(deque(), 0, "idle", [], False))
|
|
432
|
+
layout["evaluator"].update(render_evaluator_panel([], 0, "planning", False))
|
|
433
|
+
live.start()
|
|
434
|
+
time.sleep(0.2)
|
|
435
|
+
|
|
436
|
+
plan_prompt = build_plan_prompt(goal, qa_history)
|
|
437
|
+
plan_output, _ = run_plan_agent(plan_prompt, workdir, model)
|
|
438
|
+
|
|
439
|
+
live.stop()
|
|
440
|
+
console.print()
|
|
441
|
+
|
|
442
|
+
# 질문이 있는지 확인
|
|
443
|
+
has_questions = '질문:' in plan_output or re.search(r'^\d+\.\s', plan_output, re.M)
|
|
444
|
+
has_plan = '계획:' in plan_output or re.search(r'^[-•]\s', plan_output, re.M)
|
|
445
|
+
|
|
446
|
+
console.print(Rule("[cyan]Plan Agent[/cyan]"))
|
|
447
|
+
console.print(plan_output)
|
|
448
|
+
console.print()
|
|
449
|
+
|
|
450
|
+
if has_questions and not has_plan:
|
|
451
|
+
# 질의응답
|
|
452
|
+
console.print("[dim]질문에 답변하세요 (취소: /cancel):[/dim]")
|
|
453
|
+
try:
|
|
454
|
+
answer = input("> ").strip()
|
|
455
|
+
except (EOFError, KeyboardInterrupt):
|
|
456
|
+
return None
|
|
457
|
+
if answer.lower() in ('/cancel', '/exit'):
|
|
458
|
+
return None
|
|
459
|
+
qa_history.append({'q': plan_output, 'a': answer})
|
|
460
|
+
continue # 다시 Plan Agent 실행
|
|
461
|
+
|
|
462
|
+
# 계획 수립 완료 → accept
|
|
463
|
+
console.print("[dim]이 계획으로 진행하시겠습니까? (y: 실행 / n: 다시 작성 / /cancel: 취소)[/dim]")
|
|
464
|
+
try:
|
|
465
|
+
ans = input("accept (y/n) > ").strip().lower()
|
|
466
|
+
except (EOFError, KeyboardInterrupt):
|
|
467
|
+
return None
|
|
468
|
+
if ans == 'y':
|
|
469
|
+
# 계획 내용을 목표로 삼아 반환
|
|
470
|
+
final_goal = f"{goal}\n\n[계획]\n{plan_output}"
|
|
471
|
+
return final_goal
|
|
472
|
+
elif ans in ('/cancel', '/exit'):
|
|
473
|
+
return None
|
|
474
|
+
# n → 다시 처음부터
|
|
475
|
+
qa_history.clear()
|
|
476
|
+
try:
|
|
477
|
+
new_goal = input("새 목표를 입력하거나 Enter로 기존 목표 유지: ").strip()
|
|
478
|
+
except (EOFError, KeyboardInterrupt):
|
|
479
|
+
return None
|
|
480
|
+
if new_goal:
|
|
481
|
+
goal = new_goal
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
# ── Agent Loop ────────────────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
def run_agent_loop(goal: str, workdir: str, worker_model: str | None,
|
|
487
|
+
eval_model: str | None, max_iter: int,
|
|
488
|
+
layout: Layout, live: Live) -> str:
|
|
489
|
+
"""
|
|
490
|
+
Worker + Evaluator 반복 루프.
|
|
491
|
+
반환: 'done' | 'max'
|
|
492
|
+
"""
|
|
493
|
+
history = []
|
|
494
|
+
eval_history = []
|
|
495
|
+
worker_buf = deque(maxlen=WORKER_BUF_LINES)
|
|
496
|
+
worker_status = ["idle"]
|
|
497
|
+
done = False
|
|
498
|
+
|
|
499
|
+
def refresh(phase: str, iteration: int):
|
|
500
|
+
layout["header"].update(render_header(goal, iteration, max_iter, phase))
|
|
501
|
+
layout["worker"].update(render_worker_panel(
|
|
502
|
+
worker_buf, iteration, worker_status[0], history, done))
|
|
503
|
+
layout["evaluator"].update(render_evaluator_panel(
|
|
504
|
+
eval_history, iteration, phase, done))
|
|
505
|
+
|
|
506
|
+
for iteration in range(1, max_iter + 1):
|
|
507
|
+
# ── Worker ──────────────────────────────────────────────────────
|
|
508
|
+
worker_buf.clear()
|
|
509
|
+
worker_status[0] = "running"
|
|
510
|
+
if not live._started:
|
|
511
|
+
live.start()
|
|
512
|
+
refresh("worker", iteration)
|
|
513
|
+
|
|
514
|
+
worker_prompt = build_worker_prompt(goal, history)
|
|
515
|
+
worker_result = [None, None]
|
|
516
|
+
|
|
517
|
+
def _worker():
|
|
518
|
+
worker_result[0], worker_result[1] = run_worker(
|
|
519
|
+
worker_prompt, workdir, worker_model, worker_buf, worker_status)
|
|
520
|
+
|
|
521
|
+
t = threading.Thread(target=_worker, daemon=True)
|
|
522
|
+
t.start()
|
|
523
|
+
while t.is_alive():
|
|
524
|
+
refresh("worker", iteration)
|
|
525
|
+
time.sleep(0.1)
|
|
526
|
+
t.join()
|
|
527
|
+
refresh("worker", iteration)
|
|
528
|
+
|
|
529
|
+
last_msg, _ = worker_result
|
|
530
|
+
worker_summary = (last_msg or "").replace('\n', ' ')[:300]
|
|
531
|
+
history.append({
|
|
532
|
+
'iter': iteration,
|
|
533
|
+
'worker_summary': worker_summary,
|
|
534
|
+
'worker_lines': list(worker_buf)[-12:],
|
|
535
|
+
'decision': '',
|
|
536
|
+
'feedback': '',
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
# ── Evaluator ────────────────────────────────────────────────────
|
|
540
|
+
refresh("evaluator", iteration)
|
|
541
|
+
eval_prompt = build_evaluator_prompt(goal, last_msg or "", iteration)
|
|
542
|
+
eval_msg, _ = run_evaluator(eval_prompt, workdir, eval_model)
|
|
543
|
+
|
|
544
|
+
decision, feedback = parse_decision(eval_msg)
|
|
545
|
+
history[-1]['decision'] = decision
|
|
546
|
+
history[-1]['feedback'] = feedback
|
|
547
|
+
eval_history.append({
|
|
548
|
+
'iter': iteration,
|
|
549
|
+
'decision': decision,
|
|
550
|
+
'feedback': feedback,
|
|
551
|
+
'full_msg': eval_msg,
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
if decision == 'DONE':
|
|
555
|
+
done = True
|
|
556
|
+
refresh("done", iteration)
|
|
557
|
+
time.sleep(0.8)
|
|
558
|
+
live.stop()
|
|
559
|
+
|
|
560
|
+
# 완료 요약 출력
|
|
561
|
+
console.print()
|
|
562
|
+
console.print(Rule(f"[bold green]✓ 완료 — {iteration}번 반복[/bold green]"))
|
|
563
|
+
console.print()
|
|
564
|
+
for section, color in [('판단 이유', 'white'), ('결과물 위치', 'cyan'), ('결과 요약', 'bright_white')]:
|
|
565
|
+
m = re.search(rf'{section}:\s*(.+?)(?=\n판단|결과물|결과 요약|\Z)', eval_msg, re.S)
|
|
566
|
+
if m:
|
|
567
|
+
val = m.group(1).strip()
|
|
568
|
+
console.print(f"[bold]{section}[/bold]")
|
|
569
|
+
for line in val.splitlines():
|
|
570
|
+
console.print(f" [{color}]{line}[/{color}]")
|
|
571
|
+
console.print()
|
|
572
|
+
return 'done'
|
|
573
|
+
|
|
574
|
+
live.stop()
|
|
575
|
+
return 'max'
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
579
|
+
|
|
580
|
+
def main():
|
|
581
|
+
parser = argparse.ArgumentParser(
|
|
582
|
+
prog="agentforge",
|
|
583
|
+
description="Worker + Evaluator 인터랙티브 멀티에이전트 CLI",
|
|
584
|
+
)
|
|
585
|
+
parser.add_argument("-d", "--dir", default=".", metavar="DIR",
|
|
586
|
+
help="작업 디렉토리 (기본: 현재 디렉토리)")
|
|
587
|
+
parser.add_argument("--worker-model", default=None, metavar="MODEL",
|
|
588
|
+
help="Worker 모델")
|
|
589
|
+
parser.add_argument("--eval-model", default=None, metavar="MODEL",
|
|
590
|
+
help="Evaluator 모델")
|
|
591
|
+
parser.add_argument("-n", "--max-iterations", type=int, default=DEFAULT_MAX_ITER,
|
|
592
|
+
metavar="N", help=f"최대 반복 횟수 (기본: {DEFAULT_MAX_ITER})")
|
|
593
|
+
args = parser.parse_args()
|
|
594
|
+
|
|
595
|
+
if not CODEX_BIN.exists():
|
|
596
|
+
console.print(f"[bold red]Error:[/bold red] codex not found at {CODEX_BIN}")
|
|
597
|
+
sys.exit(1)
|
|
598
|
+
|
|
599
|
+
workdir = str(Path(args.dir).resolve())
|
|
600
|
+
max_iter = args.max_iterations
|
|
601
|
+
|
|
602
|
+
# ── 초기 TUI 표시 ─────────────────────────────────────────────────
|
|
603
|
+
layout = make_layout()
|
|
604
|
+
layout["header"].update(render_header("", 0, max_iter, "idle"))
|
|
605
|
+
layout["worker"].update(render_worker_panel(deque(), 0, "idle", [], False))
|
|
606
|
+
layout["evaluator"].update(render_evaluator_panel([], 0, "idle", False))
|
|
607
|
+
|
|
608
|
+
live = Live(layout, refresh_per_second=8, screen=False)
|
|
609
|
+
live.start()
|
|
610
|
+
time.sleep(0.3)
|
|
611
|
+
live.stop()
|
|
612
|
+
|
|
613
|
+
# ── REPL 루프 ─────────────────────────────────────────────────────
|
|
614
|
+
console.print()
|
|
615
|
+
console.print("[dim]명령을 입력하세요. /plan <목표> | /exit[/dim]")
|
|
616
|
+
|
|
617
|
+
_completer = SlashCompleter()
|
|
618
|
+
|
|
619
|
+
while True:
|
|
620
|
+
try:
|
|
621
|
+
raw = pt_prompt(
|
|
622
|
+
HTML('<ansicyan><b>[agentforge]</b></ansicyan> <ansiwhite>></ansiwhite> '),
|
|
623
|
+
completer=_completer,
|
|
624
|
+
complete_while_typing=True,
|
|
625
|
+
style=PROMPT_STYLE,
|
|
626
|
+
).strip()
|
|
627
|
+
except (EOFError, KeyboardInterrupt):
|
|
628
|
+
console.print("\n[dim]종료합니다.[/dim]")
|
|
629
|
+
break
|
|
630
|
+
|
|
631
|
+
if not raw:
|
|
632
|
+
continue
|
|
633
|
+
|
|
634
|
+
# 슬래시 커맨드 처리
|
|
635
|
+
if raw.startswith('/'):
|
|
636
|
+
token = raw[1:].split(None, 1)
|
|
637
|
+
cmd_name = token[0].lower() if token else ''
|
|
638
|
+
cmd_arg = token[1] if len(token) > 1 else ''
|
|
639
|
+
|
|
640
|
+
if cmd_name == 'exit':
|
|
641
|
+
console.print("[dim]종료합니다.[/dim]")
|
|
642
|
+
break
|
|
643
|
+
|
|
644
|
+
elif cmd_name == 'plan':
|
|
645
|
+
# Plan 모드
|
|
646
|
+
layout["header"].update(render_header(cmd_arg or "", 0, max_iter, "idle"))
|
|
647
|
+
layout["worker"].update(render_worker_panel(deque(), 0, "idle", [], False))
|
|
648
|
+
layout["evaluator"].update(render_evaluator_panel([], 0, "idle", False))
|
|
649
|
+
|
|
650
|
+
final_goal = run_plan_mode(cmd_arg, workdir, args.worker_model, layout, live)
|
|
651
|
+
if final_goal is None:
|
|
652
|
+
console.print("[dim]계획 취소됨.[/dim]")
|
|
653
|
+
continue
|
|
654
|
+
console.print()
|
|
655
|
+
console.print("[cyan]계획 확정. Worker + Evaluator 루프를 시작합니다...[/cyan]")
|
|
656
|
+
time.sleep(0.5)
|
|
657
|
+
# 새 layout/live 인스턴스로 루프 실행
|
|
658
|
+
layout2 = make_layout()
|
|
659
|
+
layout2["header"].update(render_header(final_goal, 0, max_iter, "idle"))
|
|
660
|
+
layout2["worker"].update(render_worker_panel(deque(), 0, "idle", [], False))
|
|
661
|
+
layout2["evaluator"].update(render_evaluator_panel([], 0, "idle", False))
|
|
662
|
+
live2 = Live(layout2, refresh_per_second=8, screen=False)
|
|
663
|
+
outcome = run_agent_loop(
|
|
664
|
+
final_goal, workdir, args.worker_model, args.eval_model,
|
|
665
|
+
max_iter, layout2, live2,
|
|
666
|
+
)
|
|
667
|
+
if outcome == 'max':
|
|
668
|
+
console.print(f"[red]{max_iter}번 반복 후에도 완료되지 않았습니다.[/red]")
|
|
669
|
+
console.print("[dim]다음 명령을 입력하세요. /exit 로 종료.[/dim]")
|
|
670
|
+
|
|
671
|
+
else:
|
|
672
|
+
console.print(f"[red]알 수 없는 커맨드: /{cmd_name}[/red]")
|
|
673
|
+
console.print("[dim]사용 가능: /plan <목표> /exit[/dim]")
|
|
674
|
+
|
|
675
|
+
else:
|
|
676
|
+
# 일반 텍스트 → 바로 Worker에게 목표로 전달
|
|
677
|
+
goal = raw
|
|
678
|
+
layout2 = make_layout()
|
|
679
|
+
layout2["header"].update(render_header(goal, 0, max_iter, "idle"))
|
|
680
|
+
layout2["worker"].update(render_worker_panel(deque(), 0, "idle", [], False))
|
|
681
|
+
layout2["evaluator"].update(render_evaluator_panel([], 0, "idle", False))
|
|
682
|
+
live2 = Live(layout2, refresh_per_second=8, screen=False)
|
|
683
|
+
outcome = run_agent_loop(
|
|
684
|
+
goal, workdir, args.worker_model, args.eval_model,
|
|
685
|
+
max_iter, layout2, live2,
|
|
686
|
+
)
|
|
687
|
+
if outcome == 'max':
|
|
688
|
+
console.print(f"[red]{max_iter}번 반복 후에도 완료되지 않았습니다.[/red]")
|
|
689
|
+
console.print("[dim]다음 명령을 입력하세요. /exit 로 종료.[/dim]")
|
|
690
|
+
|
|
691
|
+
|
|
692
|
+
if __name__ == "__main__":
|
|
693
|
+
main()
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentforge-multi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Multi-agent CLI: Worker + Evaluator agents collaborate in a loop to achieve your goal",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"agent",
|
|
8
|
+
"multi-agent",
|
|
9
|
+
"cli",
|
|
10
|
+
"codex",
|
|
11
|
+
"llm",
|
|
12
|
+
"automation"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://github.com/<your-username>/AgentForge",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "https://github.com/<your-username>/AgentForge.git"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"bin": {
|
|
21
|
+
"agentforge": "./agentforge"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"postinstall": "node scripts/postinstall.js"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=14"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"agentforge",
|
|
31
|
+
"scripts/",
|
|
32
|
+
"README.md",
|
|
33
|
+
"README.ko.md"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* AgentForge postinstall: Python 의존성 및 환경 확인
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { execSync, spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const RESET = '\x1b[0m';
|
|
9
|
+
const CYAN = '\x1b[36m';
|
|
10
|
+
const GREEN = '\x1b[32m';
|
|
11
|
+
const YELLOW = '\x1b[33m';
|
|
12
|
+
const RED = '\x1b[31m';
|
|
13
|
+
|
|
14
|
+
function check(label, fn) {
|
|
15
|
+
try {
|
|
16
|
+
fn();
|
|
17
|
+
console.log(`${GREEN} ✓${RESET} ${label}`);
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
console.log(`${RED} ✗${RESET} ${label}`);
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
console.log(`\n${CYAN}AgentForge — 환경 확인${RESET}`);
|
|
26
|
+
console.log('─'.repeat(40));
|
|
27
|
+
|
|
28
|
+
// 1. Python 3.10+
|
|
29
|
+
const pyOk = check('Python 3.10+', () => {
|
|
30
|
+
const r = spawnSync('python3', ['--version'], { encoding: 'utf8' });
|
|
31
|
+
if (r.status !== 0) throw new Error();
|
|
32
|
+
const ver = r.stdout.trim().replace('Python ', '').split('.').map(Number);
|
|
33
|
+
if (ver[0] < 3 || (ver[0] === 3 && ver[1] < 10)) throw new Error('version too low');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// 2. rich
|
|
37
|
+
const richOk = check('Python package: rich', () => {
|
|
38
|
+
const r = spawnSync('python3', ['-c', 'import rich'], { encoding: 'utf8' });
|
|
39
|
+
if (r.status !== 0) throw new Error();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// 3. prompt_toolkit
|
|
43
|
+
const ptOk = check('Python package: prompt_toolkit', () => {
|
|
44
|
+
const r = spawnSync('python3', ['-c', 'import prompt_toolkit'], { encoding: 'utf8' });
|
|
45
|
+
if (r.status !== 0) throw new Error();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 4. codex CLI
|
|
49
|
+
const codexOk = check('codex CLI', () => {
|
|
50
|
+
const r = spawnSync('which', ['codex'], { encoding: 'utf8' });
|
|
51
|
+
if (r.status !== 0) throw new Error();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
console.log('─'.repeat(40));
|
|
55
|
+
|
|
56
|
+
// 누락 패키지 자동 설치 시도
|
|
57
|
+
if (!richOk || !ptOk) {
|
|
58
|
+
console.log(`\n${YELLOW}▶ Python 패키지 설치 중...${RESET}`);
|
|
59
|
+
const missing = [];
|
|
60
|
+
if (!richOk) missing.push('rich');
|
|
61
|
+
if (!ptOk) missing.push('prompt_toolkit');
|
|
62
|
+
try {
|
|
63
|
+
execSync(`pip install ${missing.join(' ')} --quiet`, { stdio: 'inherit' });
|
|
64
|
+
console.log(`${GREEN} ✓ 설치 완료: ${missing.join(', ')}${RESET}`);
|
|
65
|
+
} catch {
|
|
66
|
+
console.log(`${RED} ✗ 자동 설치 실패. 수동으로 설치하세요:${RESET}`);
|
|
67
|
+
console.log(` pip install ${missing.join(' ')}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!codexOk) {
|
|
72
|
+
console.log(`\n${YELLOW}⚠ codex CLI가 필요합니다:${RESET}`);
|
|
73
|
+
console.log(' https://github.com/openai/codex');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (pyOk) {
|
|
77
|
+
console.log(`\n${GREEN}✓ AgentForge 설치 완료!${RESET}`);
|
|
78
|
+
console.log(`\n사용법:\n ${CYAN}agentforge${RESET} 인터랙티브 실행`);
|
|
79
|
+
console.log(` ${CYAN}agentforge -d /project${RESET} 작업 디렉토리 지정`);
|
|
80
|
+
} else {
|
|
81
|
+
console.log(`\n${RED}✗ Python 3.10+ 이 필요합니다.${RESET}`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|