feat: --func pure function mode for subagent; merge bg launch; refine SOP

This commit is contained in:
Liang Jiaqing committed 2026-06-26 12:03:36 +08:00
1 parent 66b964bdf0
commit 5bc4945dcb
4 files changed
+49 -44

No files matched your search

+22 -16
View File
@@ -1,4 +1,4 @@
import os, sys, threading, queue, time, json, re, random, locale
import os, sys, threading, queue, time, json, re, random, locale, glob
os.environ.setdefault('GA_LANG', 'zh' if any(k in (locale.getlocale()[0] or '').lower() for k in ('zh', 'chinese')) else 'en')
if sys.stdout is None: sys.stdout = open(os.devnull, "w")
elif hasattr(sys.stdout, 'reconfigure'): sys.stdout.reconfigure(errors='replace')
@@ -178,8 +178,6 @@ class GenericAgent:
if self.inc_out and last_pos < len(full_resp):
display_queue.put({'next': full_resp[last_pos:], 'source': source,
'turn': curr_turn, 'outputs': turn_resps[-2:]})
#if '</summary>' in full_resp: full_resp = full_resp.replace('</summary>', '</summary>\n\n')
#if '</file_content>' in full_resp: full_resp = re.sub(r'<file_content>\s*(.*?)\s*</file_content>', r'\n````\n<file_content>\n\1\n</file_content>\n````', full_resp, flags=re.DOTALL)
display_queue.put({'done': full_resp, 'source': source, 'turn': curr_turn, 'outputs': turn_resps.copy()})
self.history = handler.history_info
except Exception as e:
@@ -198,6 +196,7 @@ if __name__ == '__main__':
from datetime import datetime
parser = argparse.ArgumentParser()
parser.add_argument('--task', metavar='IODIR', help='一次性任务模式,先看subagent.md')
parser.add_argument('--func', metavar='PROMPT_FILE', help='纯函数模式:读prompt文件→结果写prompt.out.txt→退出')
parser.add_argument('--reflect', metavar='SCRIPT', help='反射模式:加载监控脚本,check()触发时发任务')
parser.add_argument('--input', help='prompt')
parser.add_argument('--llm_no', type=int, default=0)
@@ -206,14 +205,17 @@ if __name__ == '__main__':
args, _unknown = parser.parse_known_args()
_reflect_args = dict(zip([k.lstrip('-') for k in _unknown[::2]], _unknown[1::2])) if _unknown else {}
if args.task and not args.nobg:
if (args.func or args.task) and not args.nobg:
import subprocess, platform
cmd = [sys.executable, os.path.abspath(__file__)] + [a for a in sys.argv[1:]] + ['--nobg']
d = os.path.join(script_dir, f'temp/{args.task}'); os.makedirs(d, exist_ok=True)
if args.task:
d = os.path.join(script_dir, f'temp/{args.task}'); os.makedirs(d, exist_ok=True)
out = open(os.path.join(d, 'stdout.log'), 'w', encoding='utf-8')
err = open(os.path.join(d, 'stderr.log'), 'w', encoding='utf-8')
else: out, err = subprocess.DEVNULL, subprocess.DEVNULL
p = subprocess.Popen(cmd, cwd=script_dir,
creationflags=0x08000000 if platform.system() == 'Windows' else 0,
stdout=open(os.path.join(d, 'stdout.log'), 'w', encoding='utf-8'),
stderr=open(os.path.join(d, 'stderr.log'), 'w', encoding='utf-8'))
stdout=out, stderr=err)
print('PID:', p.pid); sys.exit(0)
agent = GeneraticAgent()
@@ -222,31 +224,35 @@ if __name__ == '__main__':
threading.Thread(target=agent.run, daemon=True).start()
if args.task:
agent.peer_hint = False
agent.force_non_stream = True
agent.task_dir = d = os.path.join(script_dir, f'temp/{args.task}'); nround = ''
infile = os.path.join(d, 'input.txt')
infile = os.path.join(d, 'input.txt'); outfile = f'{d}/output{nround}.txt'
if args.input:
os.makedirs(d, exist_ok=True)
import glob; [os.remove(f) for f in glob.glob(os.path.join(d, 'output*.txt'))]
[os.remove(f) for f in glob.glob(os.path.join(d, 'output*.txt'))]
with open(infile, 'w', encoding='utf-8') as f: f.write(args.input)
if (fh := consume_file(d, '_history.json')): agent.llmclient.backend.history = json.loads(fh)
elif args.func:
infile = args.func; outfile = os.path.splitext(args.func)[0] + '.out.txt'
if args.func or args.task:
agent.peer_hint = False
with open(infile, encoding='utf-8') as f: raw = f.read()
while True:
dq = agent.put_task(raw, source='task')
dq = agent.put_task(raw, source='func' if args.func else 'task')
while 'done' not in (item := dq.get(timeout=1200)):
if 'next' in item and random.random() < 0.95: # 概率写一次中间结果
with open(f'{d}/output{nround}.txt', 'w', encoding='utf-8') as f: f.write(item.get('next', ''))
with open(f'{d}/output{nround}.txt', 'w', encoding='utf-8') as f: f.write(item['done'] + '\n\n[ROUND END]\n')
if 'next' in item:
with open(outfile, 'w', encoding='utf-8') as f: f.write(item.get('next', ''))
with open(outfile, 'w', encoding='utf-8') as f: f.write(item['done'] + '\n\n[ROUND END]\n')
if not args.task: break
consume_file(d, '_stop') # 已经成功停下来了,避免打断下次reply
for _ in range(300): # 等reply.txt,10分钟超时
time.sleep(2)
if (raw := consume_file(d, 'reply.txt')): break
else: break
nround = nround + 1 if isinstance(nround, int) else 1
outfile = f'{d}/output{nround}.txt'
elif args.reflect:
agent.peer_hint = False
agent.force_non_stream = True
import importlib.util
spec = importlib.util.spec_from_file_location('reflect_script', args.reflect)
mod = importlib.util.module_from_spec(spec); spec.loader.exec_module(mod)
+1 -1
View File
@@ -17,7 +17,7 @@
"width": 1280,
"height": 800,
"resizable": true,
"maximized": true,
"maximized": false,
"visible": false,
"url": "loading.html"
},
+2 -1
View File
@@ -12,7 +12,8 @@
| 3 vision(VLM) | 2仍不足时才用 | 仅语义理解、确认界面状态、辅助判断目标 | 不可信其坐标 |
Windows 下窗口截图和操作使用 ljqCtrl:严禁 pyautogui;记得先 Activate 到前台;
坑1-DPI:一律物理坐标;坑2-遮盖/失焦:混乱先枚举窗口确认前台;
ui_detect附送OCR,不要单独使用OCR
坑1-DPI:必须先import ljqCtrl,之后一律使用物理坐标;坑2-遮盖/失焦:混乱先枚举窗口确认前台;
ui_detect 的 bbox 是截图内坐标,点击前必须用 `ClientToScreen(hwnd,(0,0))/dpi_scale + bbox中心` 转屏幕物理坐标
坐标转换禁用 `GetWindowRect` 或 DWM 窗口矩形直接加截图坐标(含标题栏/边框/阴影会错位)
ljqCtrl.Click 后会返回像素/前台变化,0% 或近 0% 变化立即停下诊断,禁止盲目重试。
+24 -26
View File
@@ -1,40 +1,38 @@
# Subagent 调用 SOP
## 文件IO协议
## 两种模式
- 目录:`temp/{task_name}/`(cwd在temp/时即`./{task_name}/`)
- 启动:`python agentmain.py --task {name} [--input "短文本"] [--llm_no N]`(cwd=代码根)
### --func 纯函数模式
- `python agentmain.py --func prompt.txt [--llm_no N]`(cwd=代码根)
- 读prompt文件→执行→结果写`prompt.out.txt`→退出,主agent读完可删
- 后台启动(print PID),加`--nobg`前台同步等结果
- 适用:单次任务、并行map、不需要追问的场景
### --task 持续协作模式
- `python agentmain.py --task {name} [--input "短文本"] [--llm_no N]`(cwd=代码根)
- `--input`自动建目录+清旧output+写input.txt;长文本先手动写input.txt再启动(不带--input)
- 自动后台启动,print PID then exit
- 统一设定:所有agent的cwd都是temp,方便文件共享,不是task目录
- input:目标+约束即可,subagent同等智能。**禁写步骤/过度描述**,大量数据给路径
- 通信:output.txt(append,`[ROUND END]`=轮完成) → 写reply.txt继续 → 不写10min退出。reply后输出为output1/2/3.txt(同格式)
- **不要--nobg**(会卡在等reply循环),只能后台启动
- 通信:output.txt(`[ROUND END]`=轮完成) → 写reply.txt继续 → 不写10min退出。reply后输出为output1/2/3.txt
- 干预文件:`_stop`(当轮结束) | `_keyinfo`(注入working memory) | `_intervene`(追加指令)
- [[可选fork功能]](继承对话上下文): 事先code_run(inline_eval=True),将变量history(自动注入,str)写入task目录下_history.json
- [[可选监察者模式]]:**主agent空闲时应读output观察进度,必要时用干预文件纠偏,禁止无脑长时间sleep**
若加`--verbose`,output将包含工具执行结果,主agent可直接审查原始数据而非仅信任摘要
- [[可选fork]]:将变量history(str)写入task目录下`_history.json`继承对话上下文
- [[可选监察者]]:主agent空闲时读output观察进度,必要时干预文件纠偏。加`--verbose`可审查原始数据
## 共通规则
- 所有agent的cwd=temp,方便文件共享
- input:目标+约束即可,subagent同等智能。**禁写步骤/过度描述**,大量数据给路径
## 场景1:测试模式 - 行为验证
**用途**:观察agent真实行为,修正RULES/L2/L3/SOP
**流程**:创建test_path/写input.txt→启动subagent→轮询output.txt(2秒间隔)→验证→清理重复
**测试原则**:只给目标,不提示位置/不诱导做法,观察自主选择
**修正闭环**:发现问题→设计测试→定位根源(RULES/L2/L3/SOP)→patch修正→验证
**技术要点**:Insight优先级>SOP;subagent的cwd=temp/
**流程**:写prompt→启动subagent→轮询结果→验证→清理
**原则**:只给目标,不提示位置/不诱导做法;Insight优先级>SOP;subagent的cwd=temp/
**两种测试**:
- 测SOP质量:input指定SOP名(如"用ezgmail_sop查看最近3封未读邮件"),排除导航干扰,失败即SOP问题
- 测导航能力:input只写目标,验证subagent能自主从insight找到正确SOP。禁止内联SOP内容
- 测SOP质量:input指定SOP名,排除导航干扰,失败即SOP问题
- 测导航能力:input只写目标,验证能自主从insight找到正确SOP
## 场景2:Map模式 - 并行处理
**用途**:将N个独立同构子任务分发给各自的subagent处理
**核心优势**:独立上下文。避免处理文档A的长上下文污染处理文档B的质量
**约束**:
- 文件系统共享是优点:不同agent处理不同输入文件,产生不同输出文件
- 共享资源冲突:键鼠不可共享;浏览器避免操作同一tab
- 不满足map模式的任务 → 主agent顺序执行即可,别用subagent
**标准流程(map-reduce)**:
1. 主agent准备阶段:爬取/dump数据,存为多个独立输入文件
2. 分发:对每个文件启动一个subagent处理(主agent自己也可以处理其中一个)
3. 收集:等所有subagent完成,主agent读取各输出文件,汇总结果
**用途**:N个独立同构子任务分发,独立上下文避免交叉污染
**约束**:文件系统共享(优点);键鼠不可共享;浏览器避免同tab
**流程**:准备独立输入文件→每个启动subagent(--func优先)→收集输出汇总
## subagent内部plan_mode使用
**原则**:subagent本身是完整agent,接收多步骤任务时应在内部创建plan管理执行