diff --git a/.gitignore b/.gitignore
index cb27d9a5..e699cc17 100644
--- a/.gitignore
+++ b/.gitignore
@@ -94,6 +94,9 @@ memory/L4_raw_sessions/*
# Memory management
!memory/memory_cleanup_sop.md
+# Code Review Principles
+!memory/code_review_principles.md
+
# Visual Studio
.vs/
restore_commit.txt
diff --git a/frontends/conductor.html b/frontends/conductor.html
new file mode 100644
index 00000000..9c3b557e
--- /dev/null
+++ b/frontends/conductor.html
@@ -0,0 +1,421 @@
+
+
+
+
+
+ Conductor Streamlit
+
+
+
+
+
+
+
Conductor
+
subagents · live
+
+
+
暂无 subagent。你可以在右侧告诉 Conductor 要做什么。
+
+
+
+
+
+
Conversation
+
connecting...
+
+
+
Conductor 正在启动,等待 WebSocket 连接。
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontends/conductor.py b/frontends/conductor.py
new file mode 100644
index 00000000..00681234
--- /dev/null
+++ b/frontends/conductor.py
@@ -0,0 +1,434 @@
+import os
+import sys
+import re
+import time
+import json
+import uuid
+import queue
+import asyncio
+import threading
+import builtins
+from dataclasses import dataclass, field
+from typing import Dict, Any, Optional, List
+
+# Silence print() from subagent threads (they share stdout with conductor)
+_original_print = builtins.print
+def _filtered_print(*args, **kwargs):
+ t = threading.current_thread()
+ if t.name.startswith('subagent-'):
+ return
+ return _original_print(*args, **kwargs)
+builtins.print = _filtered_print
+
+from fastapi import FastAPI, WebSocket, WebSocketDisconnect
+from fastapi.responses import FileResponse, PlainTextResponse, JSONResponse
+from pydantic import BaseModel
+
+# allow: python frontends/conductor.py
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+if ROOT not in sys.path:
+ sys.path.insert(0, ROOT)
+
+from agentmain import GenericAgent
+
+HOST = "127.0.0.1"
+PORT = 8900
+HTML_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "conductor.html")
+
+app = FastAPI(title="Conductor")
+
+
+class ChatIn(BaseModel):
+ msg: str
+ role: str = "conductor" # conductor | system | user
+
+
+class StartSubagentIn(BaseModel):
+ prompt: str
+
+
+class SubagentActionIn(BaseModel):
+ action: str = "intervene" # intervene | abort | kill
+ msg: str = ""
+
+
+@dataclass
+class SubAgentState:
+ id: str
+ agent: GenericAgent
+ prompt: str
+ reply: str = ""
+ status: str = "running" # running | stopped | failed | aborted
+ created_at: float = field(default_factory=time.time)
+ updated_at: float = field(default_factory=time.time)
+ last_done: str = ""
+ monitor_threads: List[threading.Thread] = field(default_factory=list)
+
+
+subagents: Dict[str, SubAgentState] = {}
+sub_lock = threading.RLock()
+ws_clients: set[WebSocket] = set()
+main_loop: Optional[asyncio.AbstractEventLoop] = None
+
+# conductor event queue: only user messages and subagent-done events enter here.
+conductor_events: "queue.Queue[dict]" = queue.Queue()
+conductor_agent: Optional[GenericAgent] = None
+conductor_started = False
+
+chat_messages: List[dict] = []
+
+
+def now_ms() -> int:
+ return int(time.time() * 1000)
+
+
+def short_id() -> str:
+ return uuid.uuid4().hex[:8]
+
+
+_TURN_SPLIT_RE = re.compile(r'\**LLM Running \(Turn \d+\) \.\.\.\**')
+_SUMMARY_RE = re.compile(r'(.*?)\s*', re.DOTALL)
+
+
+def extract_last_summary(full: str) -> str:
+ """Extract the latest content for in-progress display."""
+ matches = _SUMMARY_RE.findall(full or "")
+ if not matches:
+ return ""
+ s = matches[-1].strip()
+ return s[-1000:] if len(s) > 1000 else s
+
+
+def extract_last_text_reply(full: str) -> str:
+ """Extract only the last turn's text reply (like stapp.py fold_turns logic)."""
+ # Split by turn markers, take last segment
+ parts = _TURN_SPLIT_RE.split(full)
+ last = parts[-1] if parts else full
+ # Strip tags
+ last = _SUMMARY_RE.sub('', last)
+ # Strip [Status] and [Info] lines
+ last = re.sub(r'\[(Status|Info)\][^\n]*\n?', '', last)
+ # Strip trailing whitespace
+ last = last.strip()
+ # Cap length
+ return last[-3000:] if len(last) > 3000 else last
+
+
+def subagent_snapshot() -> list[dict]:
+ with sub_lock:
+ return [
+ {
+ "id": s.id,
+ "prompt": s.prompt,
+ "reply": (extract_last_summary(s.reply) if s.status == "running" else extract_last_text_reply(s.reply)) if s.reply else "",
+ "status": s.status,
+ "created_at": s.created_at,
+ "updated_at": s.updated_at,
+ }
+ for s in subagents.values()
+ ]
+
+
+def schedule_broadcast(payload: dict):
+ if main_loop and main_loop.is_running():
+ asyncio.run_coroutine_threadsafe(broadcast(payload), main_loop)
+
+
+async def broadcast(payload: dict):
+ dead = []
+ for ws in list(ws_clients):
+ try:
+ await ws.send_json(payload)
+ except Exception:
+ dead.append(ws)
+ for ws in dead:
+ ws_clients.discard(ws)
+
+
+def push_cards():
+ schedule_broadcast({"type": "subagents", "items": subagent_snapshot()})
+
+
+def add_chat(msg: str, role: str = "conductor"):
+ item = {"id": short_id(), "role": role, "msg": msg, "ts": now_ms(), "read": role != "user"}
+ chat_messages.append(item)
+ if len(chat_messages) > 200:
+ del chat_messages[:-200]
+ schedule_broadcast({"type": "chat", "item": item})
+ return item
+
+
+def start_agent_runner(agent: GenericAgent, name: str):
+ t = threading.Thread(target=agent.run, name=name, daemon=True)
+ t.start()
+ return t
+
+
+def monitor_display_queue(agent_id: str, dq: "queue.Queue", trigger_when_done: bool):
+ """Consume one GenericAgent display_queue.
+
+ next: update card only, never wake conductor.
+ done: update card/chat, then wake conductor if this is subagent queue.
+ """
+ acc = ""
+ while True:
+ item = dq.get()
+ if "next" in item:
+ chunk = item.get("next") or ""
+ # agent.inc_out=True means next is delta.
+ acc += chunk
+ with sub_lock:
+ s = subagents.get(agent_id)
+ if s:
+ s.reply = acc
+ s.status = "running"
+ s.updated_at = time.time()
+ push_cards()
+ if "done" in item:
+ done = item.get("done") or acc
+ with sub_lock:
+ s = subagents.get(agent_id)
+ if s:
+ s.reply = done
+ s.last_done = done
+ if s.status != "aborted":
+ s.status = "stopped"
+ s.updated_at = time.time()
+ push_cards()
+ if trigger_when_done:
+ conductor_events.put({"type": "subagent_done", "id": agent_id, "reply": done})
+ break
+
+
+def start_subagent(prompt: str) -> dict:
+ sid = short_id()
+ agent = GenericAgent()
+ agent.inc_out = True
+ agent.verbose = False
+
+ start_agent_runner(agent, f"subagent-{sid}")
+ state = SubAgentState(id=sid, agent=agent, prompt=prompt, status="running")
+ with sub_lock:
+ subagents[sid] = state
+ dq = agent.put_task(prompt, source=f"subagent:{sid}")
+ mt = threading.Thread(target=monitor_display_queue, args=(sid, dq, True), name=f"monitor-{sid}", daemon=True)
+ mt.start()
+ state.monitor_threads.append(mt)
+ push_cards()
+ return {"id": sid, "status": "running"}
+
+
+def keyinfo_subagent(sid: str, msg: str) -> dict:
+ """Inject into agent's working key_info; visible from next turn onward."""
+ with sub_lock:
+ s = subagents.get(sid)
+ if not s:
+ return {"error": "subagent not found", "id": sid}
+ h = s.agent.handler
+ h.working['key_info'] = h.working.get('key_info', '') + f"\n[MASTER] {msg}"
+ s.updated_at = time.time()
+ return {"id": sid, "status": "keyinfo_injected"}
+
+
+def input_subagent(sid: str, msg: str) -> dict:
+ """Start a new task round (used for input/reply when agent is stopped)."""
+ with sub_lock:
+ s = subagents.get(sid)
+ if not s:
+ return {"error": "subagent not found", "id": sid}
+ if s.status == "running":
+ return {"error": "subagent is still running, cannot input/reply. Start a new subagent instead.", "id": sid}
+ s.prompt = msg
+ s.reply = ""
+ s.status = "running"
+ s.updated_at = time.time()
+ dq = s.agent.put_task(msg, source=f"subagent:{sid}")
+ mt = threading.Thread(target=monitor_display_queue, args=(sid, dq, True), name=f"monitor-{sid}", daemon=True)
+ mt.start()
+ s.monitor_threads.append(mt)
+ push_cards()
+ return {"id": sid, "status": "running"}
+
+
+def conductor_readme() -> str:
+ base = f"http://{HOST}:{PORT}"
+ return "\n".join([
+ f"Conductor API\tBase: {base}",
+ "",
+ "POST /chat\tbody: {\"msg\": \"...\"}\t给用户发消息",
+ "POST /subagent\tbody: {\"prompt\": \"...\"}\t启动新subagent,返回 {\"id\": \"xxx\"}",
+ 'POST /subagent/{id}\tbody: {\"action\": \"keyinfo\", \"msg\": \"...\"}\t注入key_info(agent下轮可见)',
+ 'POST /subagent/{id}\tbody: {\"action\": \"input\", \"msg\": \"...\"}\t开新一轮任务(agent停下后追加)',
+ 'POST /subagent/{id}\tbody: {\"action\": \"abort\"}\t停止该subagent',
+ "GET /chat?last=N\t返回最近N条对话(默认20)",
+ "GET /subagent\t返回 {\"items\": [...]}\t查看所有subagent状态",
+ "GET /readme\t本文档",
+ "",
+ "触发时机: 用户新消息 | subagent done",
+ ])
+
+
+
+def conductor_prompt_from_events(events: list) -> str:
+ # 极简摘要:subagent数量和状态
+ with sub_lock:
+ running = sum(1 for s in subagents.values() if s.status == "running")
+ stopped = sum(1 for s in subagents.values() if s.status != "running")
+ unread = sum(1 for m in chat_messages if m.get("role") == "user" and not m.get("read"))
+ done_count = sum(1 for e in events if e.get("type") == "subagent_done")
+ summary = f"subagents: {running} running, {stopped} stopped | {unread}条用户未读消息, {done_count}个subagent完成报告"
+ base = f"http://{HOST}:{PORT}"
+ return f"""你是agent总管。用户只和你对话,你负责一切执行细节。
+API: {base}
+
+铁律:
+- 你绝不亲自执行任何任务,一切工作必须通过POST /subagent分派。你只做调度、审查、沟通。
+- 分派完毕后立刻结束本轮回复(停下来),等待下次轮询唤醒你。绝不阻塞等待subagent结果。
+- 每次唤醒只做最小必要动作(发消息/开subagent/reply subagent),然后立刻停。
+
+职责:
+- 理解用户意图,从记忆和上下文揣摩真实需要,不反复确认显而易见的事
+- 涉及改源码、删数据、安全敏感操作,需要改写prompt让subagent先产生方案,然后二次确认执行
+- 不准自己探测环境!!!快速转派给subagent以处理用户下一个指令
+
+- 判断用户意图是新任务还是对现有任务的延续,小心分派subagent
+- 必要时对subagent追加指令或keyinfo引导
+- 有无法确定/无法代劳的事项,列精简checklist一次性问用户(不要一个个问)
+- 任务完成后给用户简洁报告
+
+原则:
+- 能自己决定的自己决定,只在真正需要用户判断时才打扰
+- 验收优先:subagent完成后,先自己审查结果,有问题直接reply让它继续修,反复直到质量过关再报告用户
+- POST /chat 是你和用户说话的唯一方式(必须调接口,不要只在回复里写)
+- GET /readme 查API用法,GET /chat 看对话历史,GET /subagent 看状态
+{summary}"""
+
+
+def monitor_conductor_queue(dq: "queue.Queue"):
+ """Conductor output is discarded. Visible reply must go through POST /chat only."""
+ while True:
+ item = dq.get()
+ if "done" in item:
+ break
+
+
+def conductor_loop():
+ global conductor_agent, conductor_started
+ conductor_agent = GenericAgent()
+ conductor_agent.inc_out = True
+ start_agent_runner(conductor_agent, "conductor-agent")
+ conductor_started = True
+ while True:
+ # Block until first event arrives
+ first = conductor_events.get()
+ conductor_events.task_done()
+ # Short debounce: collect any additional events that arrived meanwhile
+ time.sleep(0.3)
+ events = [first]
+ while not conductor_events.empty():
+ try:
+ events.append(conductor_events.get_nowait())
+ conductor_events.task_done()
+ except Exception:
+ break
+ try:
+ prompt = conductor_prompt_from_events(events)
+ dq = conductor_agent.put_task(prompt, source="conductor")
+ # Block here until conductor finishes — serializes execution
+ monitor_conductor_queue(dq)
+ except Exception as e:
+ add_chat(f"Conductor error: {e}", role="system")
+
+
+@app.on_event("startup")
+async def on_startup():
+ global main_loop
+ main_loop = asyncio.get_running_loop()
+ threading.Thread(target=conductor_loop, name="conductor-loop", daemon=True).start()
+
+
+@app.get("/")
+def index():
+ return FileResponse(HTML_PATH)
+
+
+@app.get("/readme")
+def readme():
+ return PlainTextResponse(conductor_readme())
+
+
+@app.get("/subagent")
+def list_subagents():
+ return {"items": subagent_snapshot()}
+
+
+@app.post("/subagent")
+def api_start_subagent(body: StartSubagentIn):
+ return start_subagent(body.prompt)
+
+
+@app.post("/subagent/{sid}")
+def api_subagent_action(sid: str, body: SubagentActionIn):
+ with sub_lock:
+ s = subagents.get(sid)
+ if not s:
+ return JSONResponse({"error": "subagent not found", "id": sid}, status_code=404)
+ action = body.action.lower().strip()
+ if action == "keyinfo":
+ return keyinfo_subagent(sid, body.msg)
+ if action in ("input", "reply", "append", "message", "msg"):
+ return input_subagent(sid, body.msg)
+ if action == "abort":
+ s.agent.abort()
+ s.status = "aborted"
+ s.updated_at = time.time()
+ push_cards()
+ conductor_events.put({"type": "subagent_aborted", "id": sid})
+ return {"id": sid, "status": "aborted"}
+ if action == "kill":
+ s.agent.abort()
+ s.status = "aborted"
+ s.updated_at = time.time()
+ push_cards()
+ return {"id": sid, "status": "aborted"}
+ return JSONResponse({"error": f"unknown action: {body.action}"}, status_code=400)
+
+
+@app.get("/chat")
+def api_get_chat(last: int = 20):
+ """按需拉取最近N条对话,同时标记所有用户消息为已读"""
+ for m in chat_messages:
+ if m.get("role") == "user" and not m.get("read"):
+ m["read"] = True
+ schedule_broadcast({"type": "chat_read"})
+ return {"items": chat_messages[-last:]}
+
+
+@app.post("/chat")
+def api_chat(body: ChatIn):
+ return add_chat(body.msg, role=body.role)
+
+
+@app.websocket("/ws")
+async def websocket(ws: WebSocket):
+ await ws.accept()
+ ws_clients.add(ws)
+ try:
+ await ws.send_json({"type": "hello", "subagents": subagent_snapshot(), "chat": chat_messages})
+ while True:
+ data = await ws.receive_json()
+ msg = (data.get("msg") or "").strip()
+ if not msg:
+ continue
+ add_chat(msg, role="user")
+ conductor_events.put({"type": "user_message", "msg": msg})
+ except WebSocketDisconnect:
+ pass
+ finally:
+ ws_clients.discard(ws)
+
+
+if __name__ == "__main__":
+ import uvicorn, webbrowser, threading
+ threading.Timer(1.0, lambda: webbrowser.open(f"http://{HOST}:{PORT}")).start()
+ uvicorn.run("conductor:app", host=HOST, port=PORT, reload=False)
diff --git a/llmcore.py b/llmcore.py
index 17f376ea..0a84165f 100644
--- a/llmcore.py
+++ b/llmcore.py
@@ -373,7 +373,9 @@ def _stream_with_retry(sess, url, headers, payload, parse_fn):
gen = parse_fn(r)
try:
while True: streamed = True; yield next(gen)
- except StopIteration as e: return e.value or []
+ except StopIteration as e:
+ if not e.value and not streamed: raise requests.ConnectionError("empty response")
+ return e.value or []
except (requests.Timeout, requests.ConnectionError) as e:
err = f"!!!Error: {type(e).__name__}"
if attempt < sess.max_retries:
diff --git a/memory/code_review_principles.md b/memory/code_review_principles.md
new file mode 100644
index 00000000..c79d8cdc
--- /dev/null
+++ b/memory/code_review_principles.md
@@ -0,0 +1,42 @@
+# 什么是好的代码
+好的代码不是"能跑就行",而是在长期演化中保持**压缩性、局部性、可组合性与可证伪性**。
+一句话判断:同样的功能,用最小必要的结构实现——概念少,覆盖广,变化不扩散。
+---
+## 一、模块边界清晰
+每个模块只做一件事,依赖方向稳定,指向抽象而非细节。改 A 不需要连带动 B、C、D,没有循环依赖,没有到处互相 import。
+## 二、局部可推理
+看一个文件、一个函数,就能判断它的行为、代价和失败模式。不需要全局搜索隐式约定、全局状态或魔法配置才能读懂。
+## 三、可组合
+小组件能自然地组合成大能力,接口一致、正交,不需要为组合写特例。"这个只能在那种情况下用"是坏信号。
+## 四、变化半径小
+改一个需求,改动集中、可预测,回归风险可控。而不是改动像地震,到处打补丁,回归测试覆盖不了心里的担忧。
+## 五、复杂度线性增长
+新增一个功能,新增代码量近似线性,重复少。而不是功能越多代码越膨胀,出现大量相似分支和 if-else 森林。
+## 六、约束写进代码
+关键不变量和约束写进类型、接口、校验、状态机,错误尽早暴露。而不是靠调用者"记得要先做 X 再做 Y",靠注释和口口相传。
+## 七、可测试、可观测
+依赖可注入,单测容易写,日志和指标能定位因果链。而不是只能端到端测,一出问题就黑盒,难以复现。
+## 八、一致且不意外
+命名、错误处理、资源管理、并发模型全局一致,很少有反直觉的地方。而不是同一类问题三套解法,新人踩坑全靠运气。
+## 九、自解释,注释极简
+代码本身就是文档。命名、结构、流程足以说明意图,注释只出现在真正难以一眼看懂或容易误读的地方。如果一段代码需要大段注释才能读懂,说明代码本身该重写。
+## 十、代码极简,视觉均匀
+行数尽量少,不写多余的代码。每行长度大致平均,避免忽长忽短的锯齿感。简洁不是压缩,是没有废话。
+## 十一、函数式倾向,减少副作用
+优先纯函数,输入决定输出,减少隐藏的状态突变。但不教条——如果一个全局变量能显著降低整体复杂度,接受它。目标是整体简单,不是局部纯粹。
+## 十二、功能越多,代码应该越短
+好的抽象让新功能复用已有结构,而不是堆砌新代码。功能翻倍但代码量没怎么涨,说明抽象到位了。反过来,功能越加代码越膨胀,是架构在退化。
+## 十三、为未来的接入性设计
+写代码时设想:这段逻辑未来会被别的模块调用吗?会被外部系统接入吗?好代码天然留有干净的调用入口,而不是写死在某个特定场景里,等需要复用时才发现得大改。
+## 十四、Let it crash——按失败半径决定防御策略
+半径大的错误显式报错、快速中断;半径为零的静默放过。不分轻重地到处 try-catch,反而把真正需要暴露的问题吞掉了。
+## 十五、篇幅分布跟着功能分布走
+一个函数里,主功能占大部分代码,兜底和错误处理压到最短。如果防御性代码比正事还长,说明结构有问题。读代码应该一眼看出主线,而不是在 fallback 里找。
+---
+## 快速自检
+拿到一段代码,问自己四个问题:
+1. **我能不能不看全局,就安全地改一个局部?**
+2. **有没有一个清晰的核心抽象,让新功能主要是"加新实现"而不是"改旧逻辑"?**
+3. **变化点是收敛在边界上,还是散落在各处?**
+4. **出故障时,能快速定位到责任模块,还是全员背锅?**
+四个问题都能干脆地答"是",就是好代码。
\ No newline at end of file
diff --git a/memory/supervisor_sop.md b/memory/supervisor_sop.md
index e24d66f9..96f68875 100644
--- a/memory/supervisor_sop.md
+++ b/memory/supervisor_sop.md
@@ -13,7 +13,7 @@
1. **无SOP时**:根据任务性质和进度,预估未来会遇到的关键风险点
2. **启动subagent**(cwd=代码根):
```
- python agentmain.py --task {name} --bg --verbose
+ python agentmain.py --task {name} --verbose
```
input.txt:`用{SOP名}完成{用户任务}`(只给目标,不复述步骤)
diff --git a/reflect/goal_mode.py b/reflect/goal_mode.py
index 0ba3710e..f4dd45e0 100644
--- a/reflect/goal_mode.py
+++ b/reflect/goal_mode.py
@@ -36,6 +36,7 @@ CONTINUATION_PROMPT = """[Goal Mode — 持续推进]
2. 在 cwd 下建立工作文件夹存放成果和进度,复杂任务可使用 plan 模式。
3. 如果当前方向做完了,主动找下一个改进点:测试/边界case/性能/安全/文档/代码质量。
4. 找不到改进点?扩大视野:关联模块、上下游依赖、用户体验、错误提示、日志可观测性、上网搜索、找其他路径、翻记忆里面有无相关。
+5. 要为了目标持续推进,在工作文件夹中记录进度,不要更新全局记忆。
"""
BUDGET_LIMIT_PROMPT = """[Goal Mode — 预算耗尽,收口]