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') if sys.stderr is None: sys.stderr = open(os.devnull, "w") elif hasattr(sys.stderr, 'reconfigure'): sys.stderr.reconfigure(errors='replace') sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) from llmcore import reload_mykeys, ToolClient, MixinSession, NativeToolClient, NativeClaudeSession, NativeOAISession, resolve_client from agent_loop import agent_runner_loop try: from plugins.hooks import discover_and_load; discover_and_load() except Exception: pass from ga import GenericAgentHandler, smart_format, get_global_memory, format_error, consume_file script_dir = os.path.dirname(os.path.abspath(__file__)) BANNED_TOOLS = (['ask_user', 'start_long_term_update'] if '--no-user-tools' in sys.argv else []) def load_tool_schema(suffix=''): global TOOLS_SCHEMA TS = open(os.path.join(script_dir, f'assets/tools_schema{suffix}.json'), 'r', encoding='utf-8').read() TOOLS_SCHEMA = json.loads(TS if os.name == 'nt' else TS.replace('powershell', 'bash')) TOOLS_SCHEMA = [t for t in TOOLS_SCHEMA if t.get('function', {}).get('name') not in BANNED_TOOLS] load_tool_schema() lang_suffix = '_en' if os.environ.get('GA_LANG', '') == 'en' else '' mem_dir = os.path.join(script_dir, 'memory') if not os.path.exists(mem_dir): os.makedirs(mem_dir) mem_txt = os.path.join(mem_dir, 'global_mem.txt') if not os.path.exists(mem_txt): open(mem_txt, 'w', encoding='utf-8').write('# [Global Memory - L2]\n') mem_insight = os.path.join(mem_dir, 'global_mem_insight.txt') if not os.path.exists(mem_insight): t = os.path.join(script_dir, f'assets/global_mem_insight_template{lang_suffix}.txt') open(mem_insight, 'w', encoding='utf-8').write(open(t, encoding='utf-8').read() if os.path.exists(t) else '') cdp_cfg = os.path.join(script_dir, 'assets/tmwd_cdp_bridge/config.js') if not os.path.exists(cdp_cfg): try: os.makedirs(os.path.dirname(cdp_cfg), exist_ok=True) open(cdp_cfg, 'w', encoding='utf-8').write(f"const TID = '__ljq_{hex(random.randint(0, 99999999))[2:8]}';") except Exception as e: print(f'[WARN] CDP config init failed: {e} — advanced web features (tmwebdriver) will be unavailable.') def get_system_prompt(): with open(os.path.join(script_dir, f'assets/sys_prompt{lang_suffix}.txt'), 'r', encoding='utf-8') as f: prompt = f.read() prompt += f"\nToday: {time.strftime('%Y-%m-%d %a')}\n" prompt += get_global_memory() return prompt # SDK: # agent = GenericAgent(); threading.Thread(target=agent.run, daemon=True).start() # output1_queue = agent.put_task(prompt1) # output2_queue = agent.put_task(prompt2) class GenericAgent: def __init__(self): os.makedirs(os.path.join(script_dir, 'temp'), exist_ok=True) self.lock = threading.Lock() self.task_dir = None self.history = []; self.handler = None; self.task_queue = queue.Queue() self.is_running = False; self.stop_sig = False; self.llm_no = 0; self.inc_out = False; self.verbose = True self.peer_hint = True self.force_non_stream = False logid = f'{(time.time_ns() + random.randrange(1_000_000)) % 1_000_000:06d}' self.log_path = os.path.join(script_dir, f'temp/model_responses/model_responses_{logid}.txt') self.load_llm_sessions() self.extra_sys_prompts = [] self.intervene = self.extrakeyinfo = None def load_llm_sessions(self): mykeys, changed = reload_mykeys() if not changed and hasattr(self, 'llmclients'): return try: oldhistory = self.llmclient.backend.history except: oldhistory = None llm_sessions = [] for k, cfg in mykeys.items(): if not any(x in k for x in ['api', 'config', 'cookie']): continue try: if 'mixin' in k: llm_sessions += [{'mixin_cfg': cfg}] elif c := resolve_client(k): llm_sessions += [c] except: pass for i, s in enumerate(llm_sessions): if isinstance(s, dict) and 'mixin_cfg' in s: try: mixin = MixinSession(llm_sessions, s['mixin_cfg']) if isinstance(mixin._sessions[0], (NativeClaudeSession, NativeOAISession)): llm_sessions[i] = NativeToolClient(mixin) else: llm_sessions[i] = ToolClient(mixin) except Exception as e: print(f'\n\n\n[ERROR] Failed to init MixinSession with cfg {s["mixin_cfg"]}: {e}!!!\n\n') self.llmclients = llm_sessions self.llmclient = self.llmclients[self.llm_no%len(self.llmclients)] if oldhistory: self.llmclient.backend.history = oldhistory def next_llm(self, n=-1): self.load_llm_sessions() self.llm_no = ((self.llm_no + 1) if n < 0 else n) % len(self.llmclients) lastc = self.llmclient self.llmclient = self.llmclients[self.llm_no] try: self.llmclient.backend.history = lastc.backend.history except: raise Exception('[ERROR] BAD Mixin config: Check your mykey.py') self.llmclient.last_tools = '' name = self.get_llm_name(model=True) if 'glm' in name or 'minimax' in name or 'kimi' in name: load_tool_schema('_cn') else: load_tool_schema() def list_llms(self): self.load_llm_sessions() return [(i, self.get_llm_name(b), i == self.llm_no) for i, b in enumerate(self.llmclients)] def get_llm_name(self, b=None, model=False): b = self.llmclient if b is None else b if isinstance(b, dict): return 'BADCONFIG_MIXIN' if model: return b.backend.model.lower() return f"{type(b.backend).__name__}/{b.backend.name}" def abort(self): if not self.is_running: return print('Abort current task...') self.stop_sig = True if self.handler is not None: self.handler.code_stop_signal.append(1) def put_task(self, query, source="user", images=None): display_queue = queue.Queue() self.task_queue.put({"query": query, "source": source, "images": images or [], "output": display_queue}) return display_queue # i know it is dangerous, but raw_query is dangerous enough it doesn't enlarge def _handle_slash_cmd(self, raw_query, display_queue): if not raw_query.startswith('/'): return raw_query if _sm := re.match(r'/session\.(\w+)=(.*)', raw_query.strip()): k, v = _sm.group(1), _sm.group(2) vfile = os.path.join(script_dir, 'temp', v) if os.path.isfile(vfile): v = open(vfile, encoding='utf-8').read().strip() try: v = json.loads(v) # cover number parsing except (json.JSONDecodeError, ValueError): pass setattr(self.llmclient.backend, k, v) display_queue.put({'done': smart_format(f"✅ session.{k} = {repr(v)}", max_str_len=500), 'source': 'system'}) return None if raw_query.strip() == '/resume': return r'帮我看看最近有哪些会话可以恢复。读model_responses/目录,按修改时间取最近10个文件,从每个文件里找最后一个...块,用一句话总结每个会话在聊什么,列表给我选。注意读文件后要把字面的\n替换成真换行才能正确匹配。' return raw_query def run(self): while True: task = self.task_queue.get() if isinstance(task, str): break raw_query, source, display_queue = task["query"], task["source"], task["output"] raw_query = self._handle_slash_cmd(raw_query, display_queue) if raw_query is None: self.task_queue.task_done(); continue self.is_running = True if len(raw_query) > 2000: task_file = os.path.join(script_dir, 'temp', f'user_prompt_{int(time.time())}.md') with open(task_file, 'w', encoding='utf-8') as f: f.write(raw_query) raw_query = f'Long user prompt saved to {task_file}. Read and execute.' rquery = smart_format(raw_query.replace('\n', ' '), max_str_len=200) self.history.append(f"[USER]: {rquery}") sys_prompt = get_system_prompt() + '\n'.join(self.extra_sys_prompts) + getattr(self.llmclient.backend, 'extra_sys_prompt', '') if self.peer_hint: sys_prompt += f"\n[Peer] 用户提及其他会话/后台任务状态时: temp/model_responses/ (只找近期修改的文件尾部)\n" handler = GenericAgentHandler(self, self.history, os.path.join(script_dir, 'temp')) if getattr(self, 'no_print', False): handler.print = lambda *a, **k: None if self.handler and 'key_info' in self.handler.working: ki = re.sub(r'\n\[SYSTEM\] 此为.*?工作记忆[。\n]*', '', self.handler.working['key_info']) # 去旧 handler.working['key_info'] = ki handler.working['passed_sessions'] = ps = self.handler.working.get('passed_sessions', 0) + 1 if ps > 0: handler.working['key_info'] += f'\n[SYSTEM] 此为 {ps} 个对话前设置的key_info,若已在新任务,先更新或清除工作记忆。\n' self.handler = handler # although new handler, the **full** history is in llmclient, so it is full history! self.llmclient.log_path = self.log_path if self.force_non_stream: self.llmclient.backend.stream = False self.llmclient.backend.read_timeout = max(self.llmclient.backend.read_timeout, 1200) gen = agent_runner_loop(self.llmclient, sys_prompt, raw_query, handler, TOOLS_SCHEMA, max_turns=180, verbose=self.verbose, yield_info=True) try: full_resp = ""; last_pos = 0; curr_turn = 0; turn_resps = [] for chunk in gen: if consume_file(self.task_dir, '_stop'): self.abort() if self.stop_sig: break if isinstance(chunk, dict) and 'turn' in chunk: curr_turn = chunk['turn']; turn_resps.append(''); continue full_resp += chunk; turn_resps[-1] += chunk if len(full_resp) - last_pos > 30 or 'LLM Running' in chunk: display_queue.put({'next': full_resp[last_pos:] if self.inc_out else full_resp, 'source': source, 'turn': curr_turn, 'outputs': turn_resps[-2:]}) last_pos = len(full_resp) 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:]}) display_queue.put({'done': full_resp, 'source': source, 'turn': curr_turn, 'outputs': turn_resps.copy()}) self.history = handler.history_info except Exception as e: print(f"Backend Error: {format_error(e)}") display_queue.put({'done': full_resp + f'\n```\n{format_error(e)}\n```', 'source': source, 'turn': curr_turn, 'outputs': turn_resps.copy()}) finally: if self.stop_sig: print('User aborted the task.') self.is_running = self.stop_sig = False self.task_queue.task_done() if self.handler is not None: self.handler.code_stop_signal.append(1) GeneraticAgent = GenericAgent if __name__ == '__main__': import argparse 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('--history', help='history json file') parser.add_argument('--llm_no', type=int, default=0) parser.add_argument('--verbose', action='store_true') parser.add_argument('--nobg', action='store_true') parser.add_argument('--nolog', action='store_true') parser.add_argument('--no-user-tools', action='store_true') args, _unknown = parser.parse_known_args() _extra_args = dict(zip([k.lstrip('-') for k in _unknown[::2]], _unknown[1::2])) if _unknown else {} 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'] 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=out, stderr=err) print('PID:', p.pid); sys.exit(0) agent = GenericAgent() if args.nolog: agent.log_path = False agent.next_llm(args.llm_no) agent.verbose = args.verbose threading.Thread(target=agent.run, daemon=True).start() histfile = args.history if args.task: agent.task_dir = d = os.path.join(script_dir, f'temp/{args.task}'); nround = '' infile = os.path.join(d, 'input.txt'); outfile = f'{d}/output{nround}.txt' if args.input: os.makedirs(d, exist_ok=True) [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) histfile = histfile or os.path.join(d, '_history.json') elif args.func: infile = args.func; outfile = os.path.splitext(args.func)[0] + '.out.txt' if histfile and os.path.isfile(histfile): agent.llmclient.backend.history = json.loads(open(histfile, encoding='utf-8').read()) 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='func' if args.func else 'task') while 'done' not in (item := dq.get(timeout=2200)): 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 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) if hasattr(mod, 'init'): mod.init(_extra_args) _mt = os.path.getmtime(args.reflect) print(f'[Reflect] loaded {args.reflect}' + (f' args={_extra_args}' if _extra_args else '')) while True: if os.path.getmtime(args.reflect) != _mt: try: spec.loader.exec_module(mod); _mt = os.path.getmtime(args.reflect) if hasattr(mod, 'init'): mod.init(_extra_args) print('[Reflect] reloaded') except Exception as e: print(f'[Reflect] reload error: {e}') try: task = mod.check() except Exception as e: print(f'[Reflect] check() error: {e}'); task = None if task and task == '/exit': break if task: print(f'[Reflect] triggered: {task[:80]}') dq = agent.put_task(task, source='reflect') try: while 'done' not in (item := dq.get(timeout=2200)): pass result = item['done'] print(result) except Exception as e: if getattr(mod, 'ONCE', False): raise print(f'[Reflect] drain error: {e}'); result = f'[ERROR] {e}' log_dir = os.path.join(script_dir, 'temp/reflect_logs'); os.makedirs(log_dir, exist_ok=True) script_name = os.path.splitext(os.path.basename(args.reflect))[0] open(os.path.join(log_dir, f'{script_name}_{datetime.now():%Y-%m-%d}.log'), 'a', encoding='utf-8').write(f'[{datetime.now():%m-%d %H:%M}]\n{result}\n\n') if (on_done := getattr(mod, 'on_done', None)): try: on_done(result) except Exception as e: print(f'[Reflect] on_done error: {e}') if getattr(mod, 'ONCE', False): print('[Reflect] ONCE=True, exiting.'); break time.sleep(getattr(mod, 'INTERVAL', 5)) else: try: import readline except Exception: pass agent.inc_out = True if sys.stdout.isatty(): try: model = agent.get_llm_name(model=True) or '?' except Exception: model = '?' try: sys.stdout.write(f'\x1b[92m✦\x1b[0m \x1b[1mGenericAgent\x1b[0m ' f'\x1b[90m· cli · model:\x1b[0m {model}\n') sys.stdout.flush() except Exception: pass while True: q = input('> ').strip() if not q: continue try: dq = agent.put_task(q, source='user') while True: item = dq.get() if 'next' in item: print(item['next'], end='', flush=True) if 'done' in item: print(); break except KeyboardInterrupt: agent.abort(); print('\n[Interrupted]')