agentforge-multi 0.1.3 → 0.1.5
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 +98 -32
- package/package.json +1 -1
package/agentforge
CHANGED
|
@@ -76,11 +76,16 @@ WORKER_TOOLS = [
|
|
|
76
76
|
{
|
|
77
77
|
"type": "function",
|
|
78
78
|
"name": "shell",
|
|
79
|
-
"description":
|
|
79
|
+
"description": (
|
|
80
|
+
"Execute a shell command. Default timeout is 120s. "
|
|
81
|
+
"For long-running training/monitoring commands, set timeout_seconds (max 7200). "
|
|
82
|
+
"For fire-and-forget background jobs use '... &' and a short follow-up tail command."
|
|
83
|
+
),
|
|
80
84
|
"parameters": {
|
|
81
85
|
"type": "object",
|
|
82
86
|
"properties": {
|
|
83
87
|
"command": {"type": "string", "description": "Shell command to run"},
|
|
88
|
+
"timeout_seconds": {"type": "integer", "description": "Max seconds to wait (default 120, max 7200)"},
|
|
84
89
|
},
|
|
85
90
|
"required": ["command"],
|
|
86
91
|
},
|
|
@@ -129,14 +134,12 @@ WORKER_TOOLS = [
|
|
|
129
134
|
]
|
|
130
135
|
|
|
131
136
|
|
|
132
|
-
def
|
|
137
|
+
def _iter_events(payload: dict):
|
|
133
138
|
"""
|
|
134
139
|
ChatGPT backend-api/codex/responses 스트리밍 호출.
|
|
135
|
-
SSE 이벤트를
|
|
136
|
-
_interrupt_event가 set되면 조기 종료.
|
|
140
|
+
SSE 이벤트를 실시간으로 yield. _interrupt_event가 set되면 조기 종료.
|
|
137
141
|
"""
|
|
138
142
|
headers = _get_auth_headers()
|
|
139
|
-
events = []
|
|
140
143
|
try:
|
|
141
144
|
r = _requests.post(
|
|
142
145
|
CHATGPT_RESPONSES_URL, headers=headers,
|
|
@@ -151,23 +154,22 @@ def _stream_response(payload: dict) -> list:
|
|
|
151
154
|
decoded = line.decode("utf-8", errors="replace")
|
|
152
155
|
if decoded.startswith("data: "):
|
|
153
156
|
try:
|
|
154
|
-
|
|
155
|
-
events.append(ev)
|
|
157
|
+
yield json.loads(decoded[6:])
|
|
156
158
|
except Exception:
|
|
157
159
|
pass
|
|
158
160
|
except Exception as e:
|
|
159
|
-
|
|
160
|
-
return events
|
|
161
|
+
yield {"type": "_error", "message": str(e)}
|
|
161
162
|
|
|
162
163
|
|
|
163
164
|
def _execute_tool(name: str, args: dict, workdir: str) -> str:
|
|
164
165
|
"""Worker 도구 실행."""
|
|
165
166
|
try:
|
|
166
167
|
if name == "shell":
|
|
168
|
+
timeout = min(int(args.get("timeout_seconds", 120)), 7200)
|
|
167
169
|
result = subprocess.run(
|
|
168
170
|
args["command"], shell=True,
|
|
169
171
|
capture_output=True, text=True, errors='replace',
|
|
170
|
-
cwd=workdir, timeout=
|
|
172
|
+
cwd=workdir, timeout=timeout,
|
|
171
173
|
)
|
|
172
174
|
out = (result.stdout + result.stderr).strip()
|
|
173
175
|
return (out[:6000] if out else "(no output)") + (
|
|
@@ -465,28 +467,25 @@ def run_worker(prompt: str, workdir: str, model: str | None,
|
|
|
465
467
|
"stream": True,
|
|
466
468
|
}
|
|
467
469
|
|
|
468
|
-
|
|
470
|
+
# 실시간 스트리밍 처리
|
|
471
|
+
text_parts: list[str] = []
|
|
472
|
+
fc_items: list[dict] = [] # {call_id, name, arguments}
|
|
473
|
+
got_error = False
|
|
469
474
|
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
if
|
|
475
|
+
for ev in _iter_events(payload):
|
|
476
|
+
evtype = ev["type"]
|
|
477
|
+
if evtype == "_error":
|
|
473
478
|
buf.append(f"[red]API 오류: {ev['message']}[/red]")
|
|
474
479
|
status_ref[0] = "error"
|
|
475
480
|
if line_buf.strip():
|
|
476
481
|
buf.append(colorize_line(line_buf))
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
text_parts: list[str] = []
|
|
481
|
-
fc_items: list[dict] = [] # {call_id, name, arguments}
|
|
482
|
-
|
|
483
|
-
for ev in events:
|
|
484
|
-
t = ev["type"]
|
|
485
|
-
if t == "response.output_text.delta":
|
|
482
|
+
got_error = True
|
|
483
|
+
break
|
|
484
|
+
elif evtype == "response.output_text.delta":
|
|
486
485
|
delta = ev.get("delta", "")
|
|
487
486
|
text_parts.append(delta)
|
|
488
487
|
_flush_line(delta)
|
|
489
|
-
elif
|
|
488
|
+
elif evtype == "response.output_item.done" and ev.get("item", {}).get("type") == "function_call":
|
|
490
489
|
item = ev["item"]
|
|
491
490
|
fc_items.append({
|
|
492
491
|
"call_id": item["call_id"],
|
|
@@ -494,6 +493,9 @@ def run_worker(prompt: str, workdir: str, model: str | None,
|
|
|
494
493
|
"arguments": item["arguments"],
|
|
495
494
|
})
|
|
496
495
|
|
|
496
|
+
if got_error:
|
|
497
|
+
return "\n".join(all_text_parts), 1
|
|
498
|
+
|
|
497
499
|
round_text = "".join(text_parts)
|
|
498
500
|
if round_text:
|
|
499
501
|
all_text_parts.append(round_text)
|
|
@@ -542,15 +544,13 @@ def run_evaluator(prompt: str, workdir: str, model: str | None) -> tuple[str, in
|
|
|
542
544
|
"store": False,
|
|
543
545
|
"stream": True,
|
|
544
546
|
}
|
|
545
|
-
|
|
546
|
-
for ev in
|
|
547
|
+
parts: list[str] = []
|
|
548
|
+
for ev in _iter_events(payload):
|
|
547
549
|
if ev["type"] == "_error":
|
|
548
550
|
return f"[Evaluator error] {ev['message']}", 1
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
)
|
|
553
|
-
return text, 0
|
|
551
|
+
if ev["type"] == "response.output_text.delta":
|
|
552
|
+
parts.append(ev.get("delta", ""))
|
|
553
|
+
return "".join(parts), 0
|
|
554
554
|
|
|
555
555
|
|
|
556
556
|
def parse_decision(text: str) -> tuple[str, str]:
|
|
@@ -689,6 +689,56 @@ def render_evaluator_panel(eval_history: list, iteration: int,
|
|
|
689
689
|
)
|
|
690
690
|
|
|
691
691
|
|
|
692
|
+
# ── tmux 자동 래핑 ────────────────────────────────────────────────────────────
|
|
693
|
+
|
|
694
|
+
import shlex as _shlex
|
|
695
|
+
import shutil as _shutil
|
|
696
|
+
|
|
697
|
+
TMUX_SESSION = "agentforge"
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _in_tmux() -> bool:
|
|
701
|
+
return bool(os.environ.get("TMUX"))
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def _tmux_session_exists() -> bool:
|
|
705
|
+
r = subprocess.run(["tmux", "has-session", "-t", TMUX_SESSION],
|
|
706
|
+
capture_output=True)
|
|
707
|
+
return r.returncode == 0
|
|
708
|
+
|
|
709
|
+
|
|
710
|
+
def _tmux_launch(extra_args: list):
|
|
711
|
+
"""현재 프로세스를 tmux 세션 안의 agentforge로 교체."""
|
|
712
|
+
if _tmux_session_exists():
|
|
713
|
+
console.print(
|
|
714
|
+
f"[yellow]이미 실행 중인 agentforge 세션이 있습니다.[/yellow]\n"
|
|
715
|
+
f" 재접속: [cyan]agentforge -r[/cyan]\n"
|
|
716
|
+
f" 새 세션: [cyan]agentforge --new-session[/cyan]"
|
|
717
|
+
)
|
|
718
|
+
sys.exit(0)
|
|
719
|
+
|
|
720
|
+
exe = _shutil.which("agentforge") or os.path.abspath(sys.argv[0])
|
|
721
|
+
cmd_str = " ".join(_shlex.quote(p) for p in [exe] + extra_args)
|
|
722
|
+
try:
|
|
723
|
+
os.execvp("tmux", ["tmux", "new-session", "-s", TMUX_SESSION, cmd_str])
|
|
724
|
+
except OSError:
|
|
725
|
+
pass # tmux 실행 실패 → 그냥 일반 모드로 계속
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
def _tmux_reconnect():
|
|
729
|
+
"""기존 agentforge tmux 세션에 재접속."""
|
|
730
|
+
if not _shutil.which("tmux"):
|
|
731
|
+
console.print("[red]tmux가 설치되지 않았습니다.[/red]")
|
|
732
|
+
sys.exit(1)
|
|
733
|
+
if not _tmux_session_exists():
|
|
734
|
+
console.print(
|
|
735
|
+
"[yellow]실행 중인 agentforge 세션이 없습니다.[/yellow]\n"
|
|
736
|
+
"새 세션 시작: [cyan]agentforge[/cyan]"
|
|
737
|
+
)
|
|
738
|
+
sys.exit(1)
|
|
739
|
+
os.execvp("tmux", ["tmux", "attach-session", "-t", TMUX_SESSION])
|
|
740
|
+
|
|
741
|
+
|
|
692
742
|
# ── Agent Loop ────────────────────────────────────────────────────────────────
|
|
693
743
|
|
|
694
744
|
def run_agent_loop(goal: str, workdir: str, worker_model: str | None,
|
|
@@ -746,7 +796,7 @@ def run_agent_loop(goal: str, workdir: str, worker_model: str | None,
|
|
|
746
796
|
break # worker 스레드는 _interrupt_event 확인 후 자체 중단
|
|
747
797
|
refresh("worker", iteration)
|
|
748
798
|
time.sleep(0.1)
|
|
749
|
-
t.join()
|
|
799
|
+
t.join(timeout=10) # 최대 10초 대기 후 포기
|
|
750
800
|
refresh("worker", iteration)
|
|
751
801
|
|
|
752
802
|
# ESC로 중단된 경우
|
|
@@ -865,6 +915,22 @@ def _handle_interrupt(goal: str, workdir: str, args, max_iter: int) -> str:
|
|
|
865
915
|
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
866
916
|
|
|
867
917
|
def main():
|
|
918
|
+
# ── tmux 자동 래핑 ─────────────────────────────────────────────────
|
|
919
|
+
# -r / --reconnect: 기존 세션 재접속
|
|
920
|
+
if len(sys.argv) >= 2 and sys.argv[1] in ("-r", "--reconnect"):
|
|
921
|
+
_tmux_reconnect()
|
|
922
|
+
return
|
|
923
|
+
|
|
924
|
+
# --new-session: 기존 세션 무시하고 강제로 새 세션
|
|
925
|
+
force_new = len(sys.argv) >= 2 and sys.argv[1] == "--new-session"
|
|
926
|
+
extra_args = [a for a in sys.argv[1:] if a != "--new-session"]
|
|
927
|
+
|
|
928
|
+
if not _in_tmux() and _shutil.which("tmux"):
|
|
929
|
+
if force_new and _tmux_session_exists():
|
|
930
|
+
subprocess.run(["tmux", "kill-session", "-t", TMUX_SESSION])
|
|
931
|
+
_tmux_launch(extra_args)
|
|
932
|
+
# execvp 실패 또는 세션 이미 존재 시 여기까지 옴 → 일반 모드로 계속
|
|
933
|
+
|
|
868
934
|
parser = argparse.ArgumentParser(
|
|
869
935
|
prog="agentforge",
|
|
870
936
|
description="Worker + Evaluator 인터랙티브 멀티에이전트 CLI",
|