agentforge-multi 0.1.3 → 0.1.4

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.
Files changed (2) hide show
  1. package/agentforge +32 -32
  2. 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": "Execute a shell command. Use for running scripts, compiling, testing, installing packages, etc.",
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 _stream_response(payload: dict) -> list:
137
+ def _iter_events(payload: dict):
133
138
  """
134
139
  ChatGPT backend-api/codex/responses 스트리밍 호출.
135
- SSE 이벤트를 파싱하여 (event_type, data_dict) 리스트 반환.
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
- ev = json.loads(decoded[6:])
155
- events.append(ev)
157
+ yield json.loads(decoded[6:])
156
158
  except Exception:
157
159
  pass
158
160
  except Exception as e:
159
- events.append({"type": "_error", "message": str(e)})
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=120,
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
- events = _stream_response(payload)
470
+ # 실시간 스트리밍 처리
471
+ text_parts: list[str] = []
472
+ fc_items: list[dict] = [] # {call_id, name, arguments}
473
+ got_error = False
469
474
 
470
- # 에러 처리
471
- for ev in events:
472
- if ev["type"] == "_error":
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
- return "\n".join(all_text_parts), 1
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 t == "response.output_item.done" and ev.get("item", {}).get("type") == "function_call":
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
- events = _stream_response(payload)
546
- for ev in events:
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
- text = "".join(
550
- ev.get("delta", "") for ev in events
551
- if ev["type"] == "response.output_text.delta"
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]:
@@ -746,7 +746,7 @@ def run_agent_loop(goal: str, workdir: str, worker_model: str | None,
746
746
  break # worker 스레드는 _interrupt_event 확인 후 자체 중단
747
747
  refresh("worker", iteration)
748
748
  time.sleep(0.1)
749
- t.join()
749
+ t.join(timeout=10) # 최대 10초 대기 후 포기
750
750
  refresh("worker", iteration)
751
751
 
752
752
  # ESC로 중단된 경우
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentforge-multi",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "Multi-agent CLI: Worker + Evaluator agents collaborate in a loop to achieve your goal",
5
5
  "keywords": [
6
6
  "ai",