agentforge-multi 0.1.5 → 0.1.6
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/agentforge +216 -55
- package/package.json +1 -1
package/agentforge
CHANGED
|
@@ -27,6 +27,7 @@ from collections import deque
|
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
|
|
29
29
|
import requests as _requests
|
|
30
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
30
31
|
|
|
31
32
|
from rich.console import Console
|
|
32
33
|
from rich.layout import Layout
|
|
@@ -56,6 +57,7 @@ NOISE_RE = re.compile(r'^\s*([\-─═\s]+)?$')
|
|
|
56
57
|
console = Console()
|
|
57
58
|
_last_session: dict | None = None # {goal, history, eval_history, workdir}
|
|
58
59
|
_interrupt_event = threading.Event() # ESC 감지 플래그
|
|
60
|
+
_current_response = None # 현재 스트리밍 HTTP 응답 (즉시 끊기용)
|
|
59
61
|
|
|
60
62
|
# ── ChatGPT backend API ────────────────────────────────────────────────────────
|
|
61
63
|
|
|
@@ -134,30 +136,57 @@ WORKER_TOOLS = [
|
|
|
134
136
|
]
|
|
135
137
|
|
|
136
138
|
|
|
139
|
+
RESEARCH_TOOLS = WORKER_TOOLS + [
|
|
140
|
+
{
|
|
141
|
+
"type": "function", "name": "web_search",
|
|
142
|
+
"description": "Search the web using DuckDuckGo (or Brave if BRAVE_API_KEY set). Returns summaries and links.",
|
|
143
|
+
"parameters": {"type": "object",
|
|
144
|
+
"properties": {"query": {"type": "string"}},
|
|
145
|
+
"required": ["query"]},
|
|
146
|
+
"strict": False,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"type": "function", "name": "fetch_url",
|
|
150
|
+
"description": "Fetch and extract text content from a URL.",
|
|
151
|
+
"parameters": {"type": "object",
|
|
152
|
+
"properties": {"url": {"type": "string"}},
|
|
153
|
+
"required": ["url"]},
|
|
154
|
+
"strict": False,
|
|
155
|
+
},
|
|
156
|
+
]
|
|
157
|
+
|
|
158
|
+
|
|
137
159
|
def _iter_events(payload: dict):
|
|
138
160
|
"""
|
|
139
161
|
ChatGPT backend-api/codex/responses 스트리밍 호출.
|
|
140
|
-
SSE 이벤트를 실시간으로 yield. _interrupt_event가 set되면
|
|
162
|
+
SSE 이벤트를 실시간으로 yield. _interrupt_event가 set되면 즉시 연결 끊기.
|
|
141
163
|
"""
|
|
164
|
+
global _current_response
|
|
142
165
|
headers = _get_auth_headers()
|
|
143
166
|
try:
|
|
144
167
|
r = _requests.post(
|
|
145
168
|
CHATGPT_RESPONSES_URL, headers=headers,
|
|
146
169
|
json=payload, stream=True, timeout=300,
|
|
147
170
|
)
|
|
171
|
+
_current_response = r
|
|
148
172
|
r.raise_for_status()
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
173
|
+
try:
|
|
174
|
+
for line in r.iter_lines():
|
|
175
|
+
if _interrupt_event.is_set():
|
|
176
|
+
r.close()
|
|
177
|
+
break
|
|
178
|
+
if not line:
|
|
179
|
+
continue
|
|
180
|
+
decoded = line.decode("utf-8", errors="replace")
|
|
181
|
+
if decoded.startswith("data: "):
|
|
182
|
+
try:
|
|
183
|
+
yield json.loads(decoded[6:])
|
|
184
|
+
except Exception:
|
|
185
|
+
pass
|
|
186
|
+
finally:
|
|
187
|
+
_current_response = None
|
|
160
188
|
except Exception as e:
|
|
189
|
+
_current_response = None
|
|
161
190
|
yield {"type": "_error", "message": str(e)}
|
|
162
191
|
|
|
163
192
|
|
|
@@ -195,6 +224,48 @@ def _execute_tool(name: str, args: dict, workdir: str) -> str:
|
|
|
195
224
|
return "\n".join(
|
|
196
225
|
("dir " if p.is_dir() else "file ") + p.name for p in items
|
|
197
226
|
)
|
|
227
|
+
elif name == "web_search":
|
|
228
|
+
query = args["query"]
|
|
229
|
+
brave_key = os.environ.get("BRAVE_API_KEY")
|
|
230
|
+
if brave_key:
|
|
231
|
+
r = _requests.get(
|
|
232
|
+
"https://api.search.brave.com/res/v1/web/search",
|
|
233
|
+
params={"q": query, "count": 10},
|
|
234
|
+
headers={"Accept": "application/json",
|
|
235
|
+
"Accept-Encoding": "gzip",
|
|
236
|
+
"X-Subscription-Token": brave_key},
|
|
237
|
+
timeout=15,
|
|
238
|
+
)
|
|
239
|
+
items = r.json().get("web", {}).get("results", [])
|
|
240
|
+
return "\n".join(
|
|
241
|
+
f"[{i+1}] {it['title']}\n {it['url']}\n {it.get('description','')}"
|
|
242
|
+
for i, it in enumerate(items[:8])
|
|
243
|
+
) or "(결과 없음)"
|
|
244
|
+
else:
|
|
245
|
+
r = _requests.get(
|
|
246
|
+
"https://html.duckduckgo.com/html/",
|
|
247
|
+
params={"q": query},
|
|
248
|
+
headers={"User-Agent": "Mozilla/5.0"},
|
|
249
|
+
timeout=15,
|
|
250
|
+
)
|
|
251
|
+
snippets = re.findall(r'class="result__snippet">(.*?)</a>', r.text, re.S)
|
|
252
|
+
titles = re.findall(r'class="result__a"[^>]*>(.*?)</a>', r.text, re.S)
|
|
253
|
+
urls = re.findall(r'uddg=(https?[^&"]+)', r.text)
|
|
254
|
+
from urllib.parse import unquote
|
|
255
|
+
lines = []
|
|
256
|
+
for i, (t, u, s) in enumerate(zip(titles, urls, snippets)):
|
|
257
|
+
t = re.sub(r'<[^>]+>', '', t).strip()
|
|
258
|
+
s = re.sub(r'<[^>]+>', '', s).strip()
|
|
259
|
+
lines.append(f"[{i+1}] {t}\n {unquote(u)}\n {s}")
|
|
260
|
+
if i >= 7:
|
|
261
|
+
break
|
|
262
|
+
return "\n".join(lines) or "(결과 없음)"
|
|
263
|
+
elif name == "fetch_url":
|
|
264
|
+
url = args["url"]
|
|
265
|
+
r = _requests.get(url, timeout=15, headers={"User-Agent": "Mozilla/5.0"})
|
|
266
|
+
text = re.sub(r'<[^>]+>', ' ', r.text)
|
|
267
|
+
text = re.sub(r'\s+', ' ', text).strip()
|
|
268
|
+
return text[:8000]
|
|
198
269
|
else:
|
|
199
270
|
return f"Unknown tool: {name}"
|
|
200
271
|
except Exception as e:
|
|
@@ -203,23 +274,30 @@ def _execute_tool(name: str, args: dict, workdir: str) -> str:
|
|
|
203
274
|
# ── Slash command autocomplete ────────────────────────────────────────────────
|
|
204
275
|
|
|
205
276
|
SLASH_COMMANDS = [
|
|
206
|
-
("/resume",
|
|
207
|
-
("/exit",
|
|
277
|
+
("/resume", "마지막 세션 재개"),
|
|
278
|
+
("/exit", "종료"),
|
|
279
|
+
("/mode code", "코딩 모드로 전환"),
|
|
280
|
+
("/mode research", "연구 모드로 전환"),
|
|
281
|
+
("/eval-every <N>", "N번마다 Evaluator 실행"),
|
|
282
|
+
("/status", "현재 설정 확인"),
|
|
283
|
+
("/help", "커맨드 목록 표시"),
|
|
208
284
|
]
|
|
209
285
|
|
|
210
286
|
class SlashCompleter(Completer):
|
|
211
287
|
def get_completions(self, document, complete_event):
|
|
212
288
|
text = document.text_before_cursor
|
|
213
289
|
if text.startswith('/'):
|
|
214
|
-
typed = text
|
|
290
|
+
typed = text[1:] # strip leading /
|
|
215
291
|
for cmd, desc in SLASH_COMMANDS:
|
|
216
|
-
name = cmd.
|
|
217
|
-
|
|
292
|
+
name = cmd[1:].split()[0] # first word without /
|
|
293
|
+
full = cmd[1:] # full command without /
|
|
294
|
+
if full.startswith(typed) or name.startswith(typed.split()[0] if typed else ''):
|
|
295
|
+
import html as _html
|
|
218
296
|
yield Completion(
|
|
219
297
|
cmd,
|
|
220
298
|
start_position=-len(text),
|
|
221
|
-
display=HTML(f'<ansicyan>{cmd}</ansicyan>'),
|
|
222
|
-
display_meta=HTML(f'<ansiwhite>{desc}</ansiwhite>'),
|
|
299
|
+
display=HTML(f'<ansicyan>{_html.escape(cmd)}</ansicyan>'),
|
|
300
|
+
display_meta=HTML(f'<ansiwhite>{_html.escape(desc)}</ansiwhite>'),
|
|
223
301
|
)
|
|
224
302
|
|
|
225
303
|
PROMPT_STYLE = PtStyle.from_dict({
|
|
@@ -298,6 +376,30 @@ EVALUATOR_SYSTEM = textwrap.dedent("""\
|
|
|
298
376
|
Do NOT write anything before the decision keyword.
|
|
299
377
|
""").strip()
|
|
300
378
|
|
|
379
|
+
RESEARCH_WORKER_SYSTEM = textwrap.dedent("""\
|
|
380
|
+
You are an expert researcher. Your goal is to investigate a topic thoroughly.
|
|
381
|
+
- Use web_search to find relevant papers, articles, and data
|
|
382
|
+
- Use fetch_url to read full content of important pages
|
|
383
|
+
- Use read_file/write_file to organize findings into structured notes
|
|
384
|
+
- Synthesize information across multiple sources
|
|
385
|
+
- Stay on topic; do not drift from the research goal
|
|
386
|
+
""").strip()
|
|
387
|
+
|
|
388
|
+
RESEARCH_EVALUATOR_SYSTEM = textwrap.dedent("""\
|
|
389
|
+
You are a rigorous academic reviewer. Evaluate whether the research goal is achieved.
|
|
390
|
+
Respond with EXACTLY ONE first line: DONE, IMPROVE: <feedback>, or REDIRECT: <feedback>
|
|
391
|
+
|
|
392
|
+
DONE only if: sufficient sources found, content analyzed, findings written to file(s).
|
|
393
|
+
IMPROVE if: more sources needed, analysis incomplete, or notes missing.
|
|
394
|
+
REDIRECT if: wrong direction entirely.
|
|
395
|
+
|
|
396
|
+
When DONE, add Korean summary:
|
|
397
|
+
판단 이유: ...
|
|
398
|
+
결과물 위치: ...
|
|
399
|
+
결과 요약: ...
|
|
400
|
+
""").strip()
|
|
401
|
+
|
|
402
|
+
|
|
301
403
|
def build_worker_prompt(goal: str, history: list) -> str:
|
|
302
404
|
lines = [f"GOAL: {goal}", ""]
|
|
303
405
|
if not history:
|
|
@@ -422,6 +524,11 @@ def _esc_listener(stop: threading.Event):
|
|
|
422
524
|
ch = os.read(tty_fd, 1)
|
|
423
525
|
if ch == b'\x1b':
|
|
424
526
|
_interrupt_event.set()
|
|
527
|
+
if _current_response:
|
|
528
|
+
try:
|
|
529
|
+
_current_response.close()
|
|
530
|
+
except Exception:
|
|
531
|
+
pass
|
|
425
532
|
break
|
|
426
533
|
except Exception:
|
|
427
534
|
pass
|
|
@@ -436,9 +543,13 @@ def _esc_listener(stop: threading.Event):
|
|
|
436
543
|
# ── Agent Runners ─────────────────────────────────────────────────────────────
|
|
437
544
|
|
|
438
545
|
def run_worker(prompt: str, workdir: str, model: str | None,
|
|
439
|
-
buf: deque, status_ref: list
|
|
440
|
-
|
|
546
|
+
buf: deque, status_ref: list,
|
|
547
|
+
system_prompt: str | None = None,
|
|
548
|
+
tools: list | None = None) -> tuple[str, int]:
|
|
549
|
+
"""Worker: ChatGPT backend Responses API 직접 호출 + 병렬 도구 실행 루프."""
|
|
441
550
|
model = model or DEFAULT_WORKER_MODEL
|
|
551
|
+
system_prompt = system_prompt or WORKER_SYSTEM
|
|
552
|
+
tools = tools if tools is not None else WORKER_TOOLS
|
|
442
553
|
status_ref[0] = "running"
|
|
443
554
|
|
|
444
555
|
# 입력 히스토리 (user msg + function_call + function_call_output 누적)
|
|
@@ -460,9 +571,9 @@ def run_worker(prompt: str, workdir: str, model: str | None,
|
|
|
460
571
|
|
|
461
572
|
payload = {
|
|
462
573
|
"model": model,
|
|
463
|
-
"instructions":
|
|
574
|
+
"instructions": system_prompt,
|
|
464
575
|
"input": input_history,
|
|
465
|
-
"tools":
|
|
576
|
+
"tools": tools,
|
|
466
577
|
"store": False,
|
|
467
578
|
"stream": True,
|
|
468
579
|
}
|
|
@@ -504,26 +615,33 @@ def run_worker(prompt: str, workdir: str, model: str | None,
|
|
|
504
615
|
if not fc_items:
|
|
505
616
|
break
|
|
506
617
|
|
|
507
|
-
# 도구 실행
|
|
618
|
+
# 도구 병렬 실행
|
|
619
|
+
with ThreadPoolExecutor(max_workers=min(len(fc_items), 8)) as pool:
|
|
620
|
+
futures = {
|
|
621
|
+
pool.submit(
|
|
622
|
+
_execute_tool,
|
|
623
|
+
fc["name"],
|
|
624
|
+
json.loads(fc["arguments"]) if fc["arguments"] else {},
|
|
625
|
+
workdir,
|
|
626
|
+
): fc
|
|
627
|
+
for fc in fc_items
|
|
628
|
+
}
|
|
629
|
+
results: dict[str, str] = {}
|
|
630
|
+
for fut in as_completed(futures):
|
|
631
|
+
fc = futures[fut]
|
|
632
|
+
try:
|
|
633
|
+
results[fc["call_id"]] = fut.result()
|
|
634
|
+
except Exception as e:
|
|
635
|
+
results[fc["call_id"]] = f"Error: {e}"
|
|
636
|
+
|
|
637
|
+
# call_id 순서 보장하며 히스토리 추가
|
|
508
638
|
for fc in fc_items:
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
args = {}
|
|
516
|
-
|
|
517
|
-
arg_preview = raw_args[:80]
|
|
518
|
-
buf.append(f"[cyan]▶ {name}({arg_preview})[/cyan]")
|
|
519
|
-
result = _execute_tool(name, args, workdir)
|
|
520
|
-
short = result[:300].replace('\n', ' ')
|
|
521
|
-
buf.append(f"[dim]{short}[/dim]")
|
|
522
|
-
|
|
523
|
-
# Responses API 형식 히스토리
|
|
524
|
-
input_history.append({"type": "function_call", "call_id": call_id,
|
|
525
|
-
"name": name, "arguments": raw_args})
|
|
526
|
-
input_history.append({"type": "function_call_output", "call_id": call_id,
|
|
639
|
+
result = results[fc["call_id"]]
|
|
640
|
+
buf.append(f"[cyan]▶ {fc['name']}({fc['arguments'][:80]})[/cyan]")
|
|
641
|
+
buf.append(f"[dim]{result[:300].replace(chr(10), ' ')}[/dim]")
|
|
642
|
+
input_history.append({"type": "function_call", "call_id": fc["call_id"],
|
|
643
|
+
"name": fc["name"], "arguments": fc["arguments"]})
|
|
644
|
+
input_history.append({"type": "function_call_output", "call_id": fc["call_id"],
|
|
527
645
|
"output": result})
|
|
528
646
|
|
|
529
647
|
# 잔여 line_buf flush
|
|
@@ -534,12 +652,14 @@ def run_worker(prompt: str, workdir: str, model: str | None,
|
|
|
534
652
|
return "\n".join(all_text_parts), 0
|
|
535
653
|
|
|
536
654
|
|
|
537
|
-
def run_evaluator(prompt: str, workdir: str, model: str | None
|
|
655
|
+
def run_evaluator(prompt: str, workdir: str, model: str | None,
|
|
656
|
+
system_prompt: str | None = None) -> tuple[str, int]:
|
|
538
657
|
"""Evaluator: ChatGPT backend Responses API, 도구 없이 텍스트만."""
|
|
539
658
|
model = model or DEFAULT_EVAL_MODEL
|
|
659
|
+
system_prompt = system_prompt or EVALUATOR_SYSTEM
|
|
540
660
|
payload = {
|
|
541
661
|
"model": model,
|
|
542
|
-
"instructions":
|
|
662
|
+
"instructions": system_prompt,
|
|
543
663
|
"input": [{"role": "user", "content": prompt}],
|
|
544
664
|
"store": False,
|
|
545
665
|
"stream": True,
|
|
@@ -745,10 +865,12 @@ def run_agent_loop(goal: str, workdir: str, worker_model: str | None,
|
|
|
745
865
|
eval_model: str | None, max_iter: int,
|
|
746
866
|
layout: Layout, live: Live,
|
|
747
867
|
initial_history: list | None = None,
|
|
748
|
-
initial_eval_history: list | None = None
|
|
868
|
+
initial_eval_history: list | None = None,
|
|
869
|
+
mode: str = "code",
|
|
870
|
+
eval_every: int = 1) -> str:
|
|
749
871
|
"""
|
|
750
872
|
Worker + Evaluator 반복 루프.
|
|
751
|
-
반환: 'done' | 'max'
|
|
873
|
+
반환: 'done' | 'max' | 'interrupted'
|
|
752
874
|
"""
|
|
753
875
|
global _last_session
|
|
754
876
|
history = list(initial_history or [])
|
|
@@ -757,6 +879,11 @@ def run_agent_loop(goal: str, workdir: str, worker_model: str | None,
|
|
|
757
879
|
worker_status = ["idle"]
|
|
758
880
|
done = False
|
|
759
881
|
|
|
882
|
+
# 모드별 시스템 프롬프트 및 도구 선택
|
|
883
|
+
worker_sys = RESEARCH_WORKER_SYSTEM if mode == "research" else WORKER_SYSTEM
|
|
884
|
+
eval_sys = RESEARCH_EVALUATOR_SYSTEM if mode == "research" else EVALUATOR_SYSTEM
|
|
885
|
+
active_tools = RESEARCH_TOOLS if mode == "research" else WORKER_TOOLS
|
|
886
|
+
|
|
760
887
|
# ESC 리스너 시작
|
|
761
888
|
_interrupt_event.clear()
|
|
762
889
|
_esc_stop = threading.Event()
|
|
@@ -787,7 +914,8 @@ def run_agent_loop(goal: str, workdir: str, worker_model: str | None,
|
|
|
787
914
|
|
|
788
915
|
def _worker():
|
|
789
916
|
worker_result[0], worker_result[1] = run_worker(
|
|
790
|
-
worker_prompt, workdir, worker_model, worker_buf, worker_status
|
|
917
|
+
worker_prompt, workdir, worker_model, worker_buf, worker_status,
|
|
918
|
+
system_prompt=worker_sys, tools=active_tools)
|
|
791
919
|
|
|
792
920
|
t = threading.Thread(target=_worker, daemon=True)
|
|
793
921
|
t.start()
|
|
@@ -823,9 +951,13 @@ def run_agent_loop(goal: str, workdir: str, worker_model: str | None,
|
|
|
823
951
|
"eval_history": eval_history, "workdir": workdir}
|
|
824
952
|
return _finish('interrupted')
|
|
825
953
|
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
954
|
+
if iteration % eval_every == 0 or iteration == max_iter:
|
|
955
|
+
refresh("evaluator", iteration)
|
|
956
|
+
eval_prompt = build_evaluator_prompt(goal, last_msg or "", iteration)
|
|
957
|
+
eval_msg, _ = run_evaluator(eval_prompt, workdir, eval_model,
|
|
958
|
+
system_prompt=eval_sys)
|
|
959
|
+
else:
|
|
960
|
+
eval_msg = "IMPROVE: (evaluation skipped)"
|
|
829
961
|
|
|
830
962
|
decision, feedback = parse_decision(eval_msg)
|
|
831
963
|
history[-1]['decision'] = decision
|
|
@@ -954,6 +1086,10 @@ def main():
|
|
|
954
1086
|
help="Evaluator 모델")
|
|
955
1087
|
parser.add_argument("-n", "--max-iterations", type=int, default=DEFAULT_MAX_ITER,
|
|
956
1088
|
metavar="N", help=f"최대 반복 횟수 (기본: {DEFAULT_MAX_ITER})")
|
|
1089
|
+
parser.add_argument("--mode", choices=["code", "research"], default="code",
|
|
1090
|
+
help="실행 모드: code(기본) / research(웹 검색·분석)")
|
|
1091
|
+
parser.add_argument("--eval-every", type=int, default=1, metavar="N",
|
|
1092
|
+
help="N번 반복마다 Evaluator 실행 (기본: 1, 즉 매번)")
|
|
957
1093
|
args = parser.parse_args()
|
|
958
1094
|
|
|
959
1095
|
# ── auth 서브커맨드 ────────────────────────────────────────────────
|
|
@@ -1004,7 +1140,10 @@ def main():
|
|
|
1004
1140
|
"agentforge auth login 으로 나중에 로그인할 수 있습니다.[/dim]"
|
|
1005
1141
|
)
|
|
1006
1142
|
|
|
1007
|
-
|
|
1143
|
+
current_mode = args.mode
|
|
1144
|
+
eval_every = args.eval_every
|
|
1145
|
+
|
|
1146
|
+
console.print(f"[dim]명령을 입력하세요. /help 로 커맨드 목록 확인. 모드: {current_mode}[/dim]")
|
|
1008
1147
|
|
|
1009
1148
|
_completer = SlashCompleter()
|
|
1010
1149
|
|
|
@@ -1054,18 +1193,39 @@ def main():
|
|
|
1054
1193
|
max_iter, layout2, live2,
|
|
1055
1194
|
initial_history=s["history"],
|
|
1056
1195
|
initial_eval_history=s["eval_history"],
|
|
1196
|
+
mode=current_mode, eval_every=eval_every,
|
|
1057
1197
|
)
|
|
1058
|
-
goal = s["goal"]
|
|
1059
1198
|
if outcome == 'interrupted':
|
|
1060
|
-
|
|
1199
|
+
console.print("\n[yellow]⚠ 중단됨. REPL로 돌아갑니다.[/yellow]")
|
|
1061
1200
|
elif outcome == 'max':
|
|
1062
1201
|
console.print(f"[red]{max_iter}번 반복 후에도 완료되지 않았습니다.[/red]")
|
|
1063
1202
|
if outcome != 'interrupted':
|
|
1064
1203
|
console.print("[dim]다음 명령을 입력하세요. /exit 로 종료.[/dim]")
|
|
1065
1204
|
|
|
1205
|
+
elif cmd_name == 'mode':
|
|
1206
|
+
if cmd_arg in ('code', 'research'):
|
|
1207
|
+
current_mode = cmd_arg
|
|
1208
|
+
console.print(f"[cyan]모드 변경: {current_mode}[/cyan]")
|
|
1209
|
+
else:
|
|
1210
|
+
console.print("[red]사용법: /mode code 또는 /mode research[/red]")
|
|
1211
|
+
|
|
1212
|
+
elif cmd_name == 'eval-every':
|
|
1213
|
+
try:
|
|
1214
|
+
eval_every = int(cmd_arg)
|
|
1215
|
+
console.print(f"[cyan]Evaluator: {eval_every}번마다 실행[/cyan]")
|
|
1216
|
+
except ValueError:
|
|
1217
|
+
console.print("[red]숫자를 입력하세요. 예: /eval-every 3[/red]")
|
|
1218
|
+
|
|
1219
|
+
elif cmd_name == 'status':
|
|
1220
|
+
console.print(f"모드: [cyan]{current_mode}[/cyan] | eval-every: [cyan]{eval_every}[/cyan] | dir: [cyan]{workdir}[/cyan]")
|
|
1221
|
+
|
|
1222
|
+
elif cmd_name == 'help':
|
|
1223
|
+
for cmd, desc in SLASH_COMMANDS:
|
|
1224
|
+
console.print(f" [cyan]{cmd:<25}[/cyan] {desc}")
|
|
1225
|
+
|
|
1066
1226
|
else:
|
|
1067
1227
|
console.print(f"[red]알 수 없는 커맨드: /{cmd_name}[/red]")
|
|
1068
|
-
console.print("[dim]
|
|
1228
|
+
console.print("[dim]/help 로 커맨드 목록을 확인하세요.[/dim]")
|
|
1069
1229
|
|
|
1070
1230
|
else:
|
|
1071
1231
|
# 일반 텍스트 → 바로 Worker에게 목표로 전달
|
|
@@ -1078,9 +1238,10 @@ def main():
|
|
|
1078
1238
|
outcome = run_agent_loop(
|
|
1079
1239
|
goal, workdir, args.worker_model, args.eval_model,
|
|
1080
1240
|
max_iter, layout2, live2,
|
|
1241
|
+
mode=current_mode, eval_every=eval_every,
|
|
1081
1242
|
)
|
|
1082
1243
|
if outcome == 'interrupted':
|
|
1083
|
-
|
|
1244
|
+
console.print("\n[yellow]⚠ 중단됨. REPL로 돌아갑니다.[/yellow]")
|
|
1084
1245
|
elif outcome == 'max':
|
|
1085
1246
|
console.print(f"[red]{max_iter}번 반복 후에도 완료되지 않았습니다.[/red]")
|
|
1086
1247
|
if outcome != 'interrupted':
|