Files

1 line
230 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "chfaithwy/lumo", "files": {"lumo/cli.py": {"72501e916c4d4630792e37b65c08edcb4974bd3c": {"t": 1785746717.7043703, "n": 19512, "h": "424a12da01d2f486", "c": "\n\nimport argparse\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nimport unicodedata\nfrom pathlib import Path\n\nfrom .config import load_project_env, provider_env\nfrom .providers.clients import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient\nfrom .runtime import Pico, SessionStore\nfrom . import skills as skilllib\nfrom .workspace import AGENT_STATE_DIR, WorkspaceContext\n\nDEFAULT_SECRET_ENV_NAMES = (\n \"LUMO_OPENAI_API_KEY\",\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENAI_API_TOKEN\",\n \"LUMO_ANTHROPIC_API_KEY\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"LUMO_DEEPSEEK_API_KEY\",\n \"PICO_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"LUMO_RIGHT_CODES_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"GITHUB_PAT\",\n \"GH_PAT\",\n)\n\nWELCOME_ART = tuple(\n \" \".join(parts)\n for parts in (\n (\"██ \", \"██ ██\", \"██ ██\", \"███████\"),\n (\"██ \", \"██ ██\", \"███ ███\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"███████\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ █ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"███████\", \"███████\", \"██ ██\", \"███████\"),\n )\n)\nWELCOME_ART = (\n \"██ ██ ██ ███ ███ ███████\",\n \"██ ██ ██ ████ ████ ██ ██\",\n \"██ ██ ██ ██ ███ ██ ██ ██\",\n \"██ ██ ██ ██ █ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"████████ ███████ ██ ██ ███████\",\n)\nWELCOME_NAME = \"Lumo\"\nWELCOME_SUBTITLE = \"Code is cheap, Show me your talk\"\n\nHELP_DETAILS = textwrap.dedent(\n \"\"\"\\\n Commands:\n /help Show this help message.\n /memory Show durable memory loaded from .lumo/memory.\n /skills List workspace skills grouped by category.\n /session Show the path to the saved session file.\n /reset Clear the current session history and memory.\n /exit Exit the agent.\n \"\"\"\n).strip()\n\n\nDEFAULT_OLLAMA_MODEL = \"qwen3.5:4b\"\nDEFAULT_OLLAMA_HOST = \"http://127.0.0.1:11434\"\nDEFAULT_OPENAI_MODEL = \"gpt-5.4\"\nDEFAULT_OPENAI_BASE_URL = \"https://www.right.codes/codex/v1\"\nDEFAULT_ANTHROPIC_MODEL = \"claude-sonnet-4-6\"\nDEFAULT_ANTHROPIC_BASE_URL = \"https://www.right.codes/claude/v1\"\nDEFAULT_DEEPSEEK_MODEL = \"deepseek-v4-pro\"\nDEFAULT_DEEPSEEK_BASE_URL = \"https://api.deepseek.com/anthropic\"\nOPENAI_REASONING_EFFORTS = {\"none\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"}\nSECRET_ENV_NAMES_VARS = (\"LUMO_SECRET_ENV_NAMES\", \"PICO_SECRET_ENV_NAMES\")\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-9;]*m\")\nANSI_RESET = \"\\x1b[0m\"\nWELCOME_BORDER_COLOR = \"\\x1b[38;2;125;211;252m\"\nWELCOME_GRADIENT_START = (255, 255, 255)\nWELCOME_GRADIENT_END = (125, 211, 252)\n\n\ndef _strip_ansi(text):\n return ANSI_PATTERN.sub(\"\", str(text))\n\n\ndef _ansi_rgb(red, green, blue):\n return f\"\\x1b[38;2;{red};{green};{blue}m\"\n\n\ndef _color(text, ansi_color):\n return f\"{ansi_color}{text}{ANSI_RESET}\"\n\n\ndef _stderr_tool_call_reporter(name, summary):\n print(f\"[tool] {name} {summary}\", file=sys.stderr)\n\n\ndef _stderr_assistant_message_reporter(message):\n print(f\"[assistant] {message}\", file=sys.stderr)\n\n\ndef _stderr_retry_reporter(message):\n print(f\"[retry] {message}\", file=sys.stderr)\n\n\ndef _gradient_text(text, start_rgb=WELCOME_GRADIENT_START, end_rgb=WELCOME_GRADIENT_END):\n text = str(text)\n span = max(1, len(text) - 1)\n parts = []\n for index, char in enumerate(text):\n if char == \" \":\n parts.append(char)\n continue\n ratio = index / span\n rgb = tuple(round(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb))\n parts.append(_color(char, _ansi_rgb(*rgb)))\n return \"\".join(parts)\n\n\ndef _terminal_char_width(char):\n return 2 if unicodedata.east_asian_width(char) in (\"F\", \"W\") else 1\n\n\ndef _terminal_width(text):\n return sum(_terminal_char_width(char) for char in _strip_ansi(text))\n\n\ndef _terminal_ljust(text, width):\n text = str(text)\n return text + \" \" * max(0, width - _terminal_width(text))\n\n\ndef _terminal_center(text, width):\n text = str(text)\n padding = max(0, width - _terminal_width(text))\n left = padding // 2\n right = padding - left\n return \" \" * left + text + \" \" * right\n\n\ndef _take_terminal_width(text, width, from_end=False):\n chars = reversed(str(text)) if from_end else str(text)\n selected = []\n used = 0\n for char in chars:\n char_width = _terminal_char_width(char)\n if used + char_width > width:\n break\n selected.append(char)\n used += char_width\n if from_end:\n selected.reverse()\n return \"\".join(selected)\n\n\ndef _terminal_middle(text, limit):\n text = str(text).replace(\"\\n\", \" \")\n if _terminal_width(text) <= limit:\n return text\n if limit <= 3:\n return _take_terminal_width(text, limit)\n left = (limit - 3) // 2\n right = limit - 3 - left\n return _take_terminal_width(text, left) + \"...\" + _take_terminal_width(text, right, from_end=True)\n\n\ndef _effective_model(args, provider):\n\n\n\n\n explicit_model = getattr(args, \"model\", None)\n if explicit_model:\n return explicit_model\n if provider == \"openai\":\n model = provider_env(\"LUMO_OPENAI_MODEL\", (\"PICO_OPENAI_MODEL\", \"OPENAI_MODEL\"))\n if model:\n return model\n return DEFAULT_OPENAI_MODEL\n if provider == \"anthropic\":\n model = provider_env(\"LUMO_ANTHROPIC_MODEL\", (\"PICO_ANTHROPIC_MODEL\", \"ANTHROPIC_MODEL\"))\n if model:\n return model\n return DEFAULT_ANTHROPIC_MODEL\n if provider == \"deepseek\":\n model = provider_env(\"LUMO_DEEPSEEK_MODEL\", (\"PICO_DEEPSEEK_MODEL\", \"DEEPSEEK_MODEL\"))\n if model:\n return model\n return DEFAULT_DEEPSEEK_MODEL\n return DEFAULT_OLLAMA_MODEL\n\n\ndef _openai_reasoning_effort():\n effort = provider_env(\"LUMO_OPENAI_MODEL_EFFORT\").strip().lower()\n if effort and effort not in OPENAI_REASONING_EFFORTS:\n allowed = \", \".join(sorted(OPENAI_REASONING_EFFORTS))\n raise ValueError(f\"LUMO_OPENAI_MODEL_EFFORT must be one of: {allowed}\")\n return effort or None\n\n\ndef _openai_temperature():\n value = provider_env(\"LUMO_OPENAI_TEMPERATURE\").strip()\n if not value:\n return None\n try:\n temperature = float(value)\n except ValueError as exc:\n raise ValueError(\"LUMO_OPENAI_TEMPERATURE must be a number between 0 and 2\") from exc\n if not 0 <= temperature <= 2:\n raise ValueError(\"LUMO_OPENAI_TEMPERATURE must be a number between 0 and 2\")\n return temperature\n\n\ndef _configured_secret_names(args):\n configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)\n configured_secret_names.update(str(name).upper() for name in args.secret_env_names)\n for env_var in SECRET_ENV_NAMES_VARS:\n extra_names = os.environ.get(env_var, \"\")\n if extra_names.strip():\n configured_secret_names.update(\n item.strip().upper()\n for item in extra_names.split(\",\")\n if item.strip()\n )\n return sorted(configured_secret_names)\n\n\ndef _build_model_client(args, retry_reporter=None):\n provider = getattr(args, \"provider\", \"deepseek\")\n\n\n if provider == \"openai\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\n \"LUMO_OPENAI_API_BASE\",\n (\"PICO_OPENAI_API_BASE\", \"OPENAI_API_BASE\"),\n DEFAULT_OPENAI_BASE_URL,\n )\n api_key = provider_env(\n \"LUMO_OPENAI_API_KEY\",\n (\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"LUMO_RIGHT_CODES_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"LUMO_ANTHROPIC_API_KEY\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n ),\n )\n return OpenAICompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=_openai_temperature(),\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n reasoning_effort=_openai_reasoning_effort(),\n retry_reporter=retry_reporter,\n )\n if provider == \"anthropic\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\n \"LUMO_ANTHROPIC_API_BASE\",\n (\"PICO_ANTHROPIC_API_BASE\", \"ANTHROPIC_API_BASE\"),\n DEFAULT_ANTHROPIC_BASE_URL,\n )\n api_key = provider_env(\n \"LUMO_ANTHROPIC_API_KEY\",\n (\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"LUMO_RIGHT_CODES_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"LUMO_OPENAI_API_KEY\",\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n ),\n )\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n retry_reporter=retry_reporter,\n )\n if provider == \"deepseek\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\n \"LUMO_DEEPSEEK_API_BASE\",\n (\"PICO_DEEPSEEK_API_BASE\", \"DEEPSEEK_API_BASE\"),\n DEFAULT_DEEPSEEK_BASE_URL,\n )\n api_key = provider_env(\"LUMO_DEEPSEEK_API_KEY\", (\"PICO_DEEPSEEK_API_KEY\", \"DEEPSEEK_API_KEY\"))\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n retry_reporter=retry_reporter,\n )\n\n model = _effective_model(args, provider)\n host = getattr(args, \"host\", DEFAULT_OLLAMA_HOST)\n return OllamaModelClient(\n model=model,\n host=host,\n temperature=args.temperature,\n top_p=args.top_p,\n timeout=args.ollama_timeout,\n retry_reporter=retry_reporter,\n )\n\n\ndef build_welcome(agent, model, host):\n width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))\n inner = width - 4\n gap = 3\n\n def row(text):\n body = _terminal_middle(text, width - 4)\n return _border_row(_terminal_ljust(body, width - 4))\n\n def divider(style=\"solid\"):\n char = \"═\" if style == \"strong\" else \"─\"\n left = \"╔\" if style == \"strong\" else \"├\"\n right = \"╗\" if style == \"strong\" else \"┤\"\n return _color(left + char * (width - 2) + right, WELCOME_BORDER_COLOR)\n\n def bottom_divider():\n return _color(\"╚\" + \"═\" * (width - 2) + \"╝\", WELCOME_BORDER_COLOR)\n\n def _border_row(body):\n return f\"{_color('║', WELCOME_BORDER_COLOR)} {body} {_color('║', WELCOME_BORDER_COLOR)}\"\n\n def center(text):\n body = _terminal_middle(text, inner)\n return _border_row(_terminal_center(body, inner))\n\n def split_row(left_text, right_text, left_size, right_size):\n left_body = _terminal_ljust(str(left_text), left_size)\n right_body = _terminal_ljust(str(right_text), right_size)\n return _border_row(f\"{left_body}{' ' * gap}{right_body}\")\n\n top_gap = gap\n art_width = max(_terminal_width(_gradient_text(text)) for text in WELCOME_ART)\n top_left_width = min(max(art_width, 24), max(24, inner - 34))\n top_right_width = max(28, inner - top_gap - top_left_width)\n label_width = 11\n value_width = max(12, top_right_width - label_width)\n right_block = [\n f\"{'WORKSPACE':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.cwd, value_width), value_width).lstrip()}\",\n f\"{'MODEL':<{label_width}}{_terminal_ljust(_terminal_middle(model, value_width), value_width).lstrip()}\",\n f\"{'APPROVAL':<{label_width}}{_terminal_ljust(_terminal_middle(agent.approval_policy, value_width), value_width).lstrip()}\",\n f\"{'BRANCH':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.branch, value_width), value_width).lstrip()}\",\n f\"{'SESSION':<{label_width}}{_terminal_ljust(_terminal_middle(agent.session['id'], value_width), value_width).lstrip()}\",\n ]\n top_height = max(len(WELCOME_ART), len(right_block))\n left_block = list(WELCOME_ART) + [\"\"] * max(0, top_height - len(WELCOME_ART))\n right_padding_top = max(0, (top_height - len(right_block)) // 2)\n right_block = [\"\"] * right_padding_top + right_block\n right_block.extend([\"\"] * max(0, top_height - len(right_block)))\n\n line = divider(\"strong\")\n rows = [\n split_row(\n _terminal_ljust(_gradient_text(left_block[index]) if left_block[index] else \"\", top_left_width),\n _terminal_ljust(right_block[index], top_right_width) if right_block[index] else \" \" * top_right_width,\n top_left_width,\n top_right_width,\n )\n for index in range(top_height)\n ]\n rows.extend(\n [\n divider(),\n center(WELCOME_NAME),\n center(WELCOME_SUBTITLE),\n ]\n )\n return \"\\n\".join([line, *rows, bottom_divider()])\n\n\ndef build_agent(args):\n\n\n\n workspace = WorkspaceContext.build(args.cwd)\n\n load_project_env(workspace.repo_root)\n\n\n configured_secret_names = _configured_secret_names(args)\n store = SessionStore(Path(workspace.repo_root) / AGENT_STATE_DIR / \"sessions\")\n\n model = _build_model_client(args, retry_reporter=_stderr_retry_reporter)\n session_id = args.resume\n if session_id == \"latest\":\n session_id = store.latest()\n if session_id:\n return Pico.from_session(\n model_client=model,\n workspace=workspace,\n session_store=store,\n session_id=session_id,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n tool_call_reporter=_stderr_tool_call_reporter,\n assistant_message_reporter=_stderr_assistant_message_reporter,\n )\n return Pico(\n model_client=model,\n workspace=workspace,\n session_store=store,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n tool_call_reporter=_stderr_tool_call_reporter,\n assistant_message_reporter=_stderr_assistant_message_reporter,\n )\n\n\ndef _maybe_evolve_durable_memory(agent, reason):\n result = agent.evolve_durable_memory(reason=reason)\n if result.get(\"status\") == \"failed\":\n print(f\"durable memory evolution failed: {result.get('error', '')}\", file=sys.stderr)\n return result\n\n\ndef build_arg_parser():\n parser = argparse.ArgumentParser(\n formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n description=\"Minimal coding agent for DeepSeek, OpenAI-compatible, Anthropic-compatible, or Ollama models.\",\n )\n parser.add_argument(\"prompt\", nargs=\"*\", help=\"Optional one-shot prompt.\")\n parser.add_argument(\"--cwd\", default=\".\", help=\"Workspace directory.\")\n parser.add_argument(\"--provider\", choices=(\"ollama\", \"openai\", \"anthropic\", \"deepseek\"), default=\"openai\", help=\"Model backend to use.\")\n parser.add_argument(\n \"--model\",\n default=None,\n help=\"Model name override. Defaults to qwen3.5:4b for Ollama, LUMO_OPENAI_MODEL for openai, LUMO_ANTHROPIC_MODEL for anthropic, and LUMO_DEEPSEEK_MODEL for deepseek when set.\",\n )\n parser.add_argument(\"--host\", default=DEFAULT_OLLAMA_HOST, help=\"Ollama server URL.\")\n parser.add_argument(\"--base-url\", default=None, help=\"Provider API base URL for deepseek, openai, or anthropic.\")\n parser.add_argument(\"--ollama-timeout\", type=int, default=300, help=\"Ollama request timeout in seconds.\")\n parser.add_argument(\"--openai-timeout\", type=int, default=300, help=\"OpenAI-compatible request timeout in seconds.\")\n parser.add_argument(\"--resume\", default=None, help=\"Session id to resume or 'latest'.\")\n parser.add_argument(\"--approval\", choices=(\"ask\", \"auto\", \"never\"), default=\"ask\", help=\"Approval policy for risky tools.\")\n parser.add_argument(\n \"--secret-env-name\",\n dest=\"secret_env_names\",\n action=\"append\",\n default=[],\n help=\"Extra environment variable names to treat as secrets for trace/report redaction.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=12, help=\"Maximum tool/model iterations per request.\")\n parser.add_argument(\"--max-new-tokens\", type=int, default=8192, help=\"Maximum model output tokens per step.\")\n parser.add_argument(\"--no-memory-evolution\", action=\"store_true\", help=\"Disable session-end durable memory evolution.\")\n parser.add_argument(\"--temperature\", type=float, default=0.2, help=\"Sampling temperature sent to Ollama.\")\n parser.add_argument(\"--top-p\", type=float, default=0.9, help=\"Top-p sampling value sent to Ollama.\")\n return parser\n\n\ndef main(argv=None):\n args = build_arg_parser().parse_args(argv)\n agent = build_agent(args)\n\n model = getattr(agent.model_client, \"model\", getattr(args, \"model\", DEFAULT_OLLAMA_MODEL))\n\n host = getattr(agent.model_client, \"host\", getattr(agent.model_client, \"base_url\", getattr(args, \"host\", DEFAULT_OLLAMA_HOST)))\n print(build_welcome(agent, model=model, host=host))\n\n if args.prompt:\n\n prompt = \" \".join(args.prompt).strip()\n if prompt:\n print()\n try:\n print(agent.ask(prompt))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n return 1\n finally:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n while True:\n\n\n try:\n user_input = input(\"\\nLUMO> \").strip()\n except (EOFError, KeyboardInterrupt):\n print(\"\")\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n if not user_input:\n continue\n if user_input in {\"/exit\", \"/quit\"}:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n if user_input == \"/help\":\n print(HELP_DETAILS)\n continue\n if user_input == \"/memory\":\n print(agent.memory_text())\n continue\n if user_input == \"/skills\":\n print(skilllib.render_skill_catalog(agent.refresh_skill_catalog()))\n continue\n if user_input == \"/session\":\n print(agent.session_path)\n continue\n if user_input == \"/reset\":\n agent.reset()\n print(\"session reset\")\n continue\n\n print()\n try:\n print(agent.ask(user_input))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n"}, "b0c4927090ea4ae6789ac69ee54b9138f92562ad": {"t": 1785767239.3484936, "n": 19512, "h": "424a12da01d2f486", "c": "\n\nimport argparse\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nimport unicodedata\nfrom pathlib import Path\n\nfrom .config import load_project_env, provider_env\nfrom .providers.clients import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient\nfrom .runtime import Pico, SessionStore\nfrom . import skills as skilllib\nfrom .workspace import AGENT_STATE_DIR, WorkspaceContext\n\nDEFAULT_SECRET_ENV_NAMES = (\n \"LUMO_OPENAI_API_KEY\",\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENAI_API_TOKEN\",\n \"LUMO_ANTHROPIC_API_KEY\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"LUMO_DEEPSEEK_API_KEY\",\n \"PICO_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"LUMO_RIGHT_CODES_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"GITHUB_PAT\",\n \"GH_PAT\",\n)\n\nWELCOME_ART = tuple(\n \" \".join(parts)\n for parts in (\n (\"██ \", \"██ ██\", \"██ ██\", \"███████\"),\n (\"██ \", \"██ ██\", \"███ ███\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"███████\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ █ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"███████\", \"███████\", \"██ ██\", \"███████\"),\n )\n)\nWELCOME_ART = (\n \"██ ██ ██ ███ ███ ███████\",\n \"██ ██ ██ ████ ████ ██ ██\",\n \"██ ██ ██ ██ ███ ██ ██ ██\",\n \"██ ██ ██ ██ █ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"████████ ███████ ██ ██ ███████\",\n)\nWELCOME_NAME = \"Lumo\"\nWELCOME_SUBTITLE = \"Code is cheap, Show me your talk\"\n\nHELP_DETAILS = textwrap.dedent(\n \"\"\"\\\n Commands:\n /help Show this help message.\n /memory Show durable memory loaded from .lumo/memory.\n /skills List workspace skills grouped by category.\n /session Show the path to the saved session file.\n /reset Clear the current session history and memory.\n /exit Exit the agent.\n \"\"\"\n).strip()\n\n\nDEFAULT_OLLAMA_MODEL = \"qwen3.5:4b\"\nDEFAULT_OLLAMA_HOST = \"http://127.0.0.1:11434\"\nDEFAULT_OPENAI_MODEL = \"gpt-5.4\"\nDEFAULT_OPENAI_BASE_URL = \"https://www.right.codes/codex/v1\"\nDEFAULT_ANTHROPIC_MODEL = \"claude-sonnet-4-6\"\nDEFAULT_ANTHROPIC_BASE_URL = \"https://www.right.codes/claude/v1\"\nDEFAULT_DEEPSEEK_MODEL = \"deepseek-v4-pro\"\nDEFAULT_DEEPSEEK_BASE_URL = \"https://api.deepseek.com/anthropic\"\nOPENAI_REASONING_EFFORTS = {\"none\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"}\nSECRET_ENV_NAMES_VARS = (\"LUMO_SECRET_ENV_NAMES\", \"PICO_SECRET_ENV_NAMES\")\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-9;]*m\")\nANSI_RESET = \"\\x1b[0m\"\nWELCOME_BORDER_COLOR = \"\\x1b[38;2;125;211;252m\"\nWELCOME_GRADIENT_START = (255, 255, 255)\nWELCOME_GRADIENT_END = (125, 211, 252)\n\n\ndef _strip_ansi(text):\n return ANSI_PATTERN.sub(\"\", str(text))\n\n\ndef _ansi_rgb(red, green, blue):\n return f\"\\x1b[38;2;{red};{green};{blue}m\"\n\n\ndef _color(text, ansi_color):\n return f\"{ansi_color}{text}{ANSI_RESET}\"\n\n\ndef _stderr_tool_call_reporter(name, summary):\n print(f\"[tool] {name} {summary}\", file=sys.stderr)\n\n\ndef _stderr_assistant_message_reporter(message):\n print(f\"[assistant] {message}\", file=sys.stderr)\n\n\ndef _stderr_retry_reporter(message):\n print(f\"[retry] {message}\", file=sys.stderr)\n\n\ndef _gradient_text(text, start_rgb=WELCOME_GRADIENT_START, end_rgb=WELCOME_GRADIENT_END):\n text = str(text)\n span = max(1, len(text) - 1)\n parts = []\n for index, char in enumerate(text):\n if char == \" \":\n parts.append(char)\n continue\n ratio = index / span\n rgb = tuple(round(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb))\n parts.append(_color(char, _ansi_rgb(*rgb)))\n return \"\".join(parts)\n\n\ndef _terminal_char_width(char):\n return 2 if unicodedata.east_asian_width(char) in (\"F\", \"W\") else 1\n\n\ndef _terminal_width(text):\n return sum(_terminal_char_width(char) for char in _strip_ansi(text))\n\n\ndef _terminal_ljust(text, width):\n text = str(text)\n return text + \" \" * max(0, width - _terminal_width(text))\n\n\ndef _terminal_center(text, width):\n text = str(text)\n padding = max(0, width - _terminal_width(text))\n left = padding // 2\n right = padding - left\n return \" \" * left + text + \" \" * right\n\n\ndef _take_terminal_width(text, width, from_end=False):\n chars = reversed(str(text)) if from_end else str(text)\n selected = []\n used = 0\n for char in chars:\n char_width = _terminal_char_width(char)\n if used + char_width > width:\n break\n selected.append(char)\n used += char_width\n if from_end:\n selected.reverse()\n return \"\".join(selected)\n\n\ndef _terminal_middle(text, limit):\n text = str(text).replace(\"\\n\", \" \")\n if _terminal_width(text) <= limit:\n return text\n if limit <= 3:\n return _take_terminal_width(text, limit)\n left = (limit - 3) // 2\n right = limit - 3 - left\n return _take_terminal_width(text, left) + \"...\" + _take_terminal_width(text, right, from_end=True)\n\n\ndef _effective_model(args, provider):\n\n\n\n\n explicit_model = getattr(args, \"model\", None)\n if explicit_model:\n return explicit_model\n if provider == \"openai\":\n model = provider_env(\"LUMO_OPENAI_MODEL\", (\"PICO_OPENAI_MODEL\", \"OPENAI_MODEL\"))\n if model:\n return model\n return DEFAULT_OPENAI_MODEL\n if provider == \"anthropic\":\n model = provider_env(\"LUMO_ANTHROPIC_MODEL\", (\"PICO_ANTHROPIC_MODEL\", \"ANTHROPIC_MODEL\"))\n if model:\n return model\n return DEFAULT_ANTHROPIC_MODEL\n if provider == \"deepseek\":\n model = provider_env(\"LUMO_DEEPSEEK_MODEL\", (\"PICO_DEEPSEEK_MODEL\", \"DEEPSEEK_MODEL\"))\n if model:\n return model\n return DEFAULT_DEEPSEEK_MODEL\n return DEFAULT_OLLAMA_MODEL\n\n\ndef _openai_reasoning_effort():\n effort = provider_env(\"LUMO_OPENAI_MODEL_EFFORT\").strip().lower()\n if effort and effort not in OPENAI_REASONING_EFFORTS:\n allowed = \", \".join(sorted(OPENAI_REASONING_EFFORTS))\n raise ValueError(f\"LUMO_OPENAI_MODEL_EFFORT must be one of: {allowed}\")\n return effort or None\n\n\ndef _openai_temperature():\n value = provider_env(\"LUMO_OPENAI_TEMPERATURE\").strip()\n if not value:\n return None\n try:\n temperature = float(value)\n except ValueError as exc:\n raise ValueError(\"LUMO_OPENAI_TEMPERATURE must be a number between 0 and 2\") from exc\n if not 0 <= temperature <= 2:\n raise ValueError(\"LUMO_OPENAI_TEMPERATURE must be a number between 0 and 2\")\n return temperature\n\n\ndef _configured_secret_names(args):\n configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)\n configured_secret_names.update(str(name).upper() for name in args.secret_env_names)\n for env_var in SECRET_ENV_NAMES_VARS:\n extra_names = os.environ.get(env_var, \"\")\n if extra_names.strip():\n configured_secret_names.update(\n item.strip().upper()\n for item in extra_names.split(\",\")\n if item.strip()\n )\n return sorted(configured_secret_names)\n\n\ndef _build_model_client(args, retry_reporter=None):\n provider = getattr(args, \"provider\", \"deepseek\")\n\n\n if provider == \"openai\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\n \"LUMO_OPENAI_API_BASE\",\n (\"PICO_OPENAI_API_BASE\", \"OPENAI_API_BASE\"),\n DEFAULT_OPENAI_BASE_URL,\n )\n api_key = provider_env(\n \"LUMO_OPENAI_API_KEY\",\n (\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"LUMO_RIGHT_CODES_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"LUMO_ANTHROPIC_API_KEY\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n ),\n )\n return OpenAICompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=_openai_temperature(),\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n reasoning_effort=_openai_reasoning_effort(),\n retry_reporter=retry_reporter,\n )\n if provider == \"anthropic\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\n \"LUMO_ANTHROPIC_API_BASE\",\n (\"PICO_ANTHROPIC_API_BASE\", \"ANTHROPIC_API_BASE\"),\n DEFAULT_ANTHROPIC_BASE_URL,\n )\n api_key = provider_env(\n \"LUMO_ANTHROPIC_API_KEY\",\n (\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"LUMO_RIGHT_CODES_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"LUMO_OPENAI_API_KEY\",\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n ),\n )\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n retry_reporter=retry_reporter,\n )\n if provider == \"deepseek\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\n \"LUMO_DEEPSEEK_API_BASE\",\n (\"PICO_DEEPSEEK_API_BASE\", \"DEEPSEEK_API_BASE\"),\n DEFAULT_DEEPSEEK_BASE_URL,\n )\n api_key = provider_env(\"LUMO_DEEPSEEK_API_KEY\", (\"PICO_DEEPSEEK_API_KEY\", \"DEEPSEEK_API_KEY\"))\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n retry_reporter=retry_reporter,\n )\n\n model = _effective_model(args, provider)\n host = getattr(args, \"host\", DEFAULT_OLLAMA_HOST)\n return OllamaModelClient(\n model=model,\n host=host,\n temperature=args.temperature,\n top_p=args.top_p,\n timeout=args.ollama_timeout,\n retry_reporter=retry_reporter,\n )\n\n\ndef build_welcome(agent, model, host):\n width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))\n inner = width - 4\n gap = 3\n\n def row(text):\n body = _terminal_middle(text, width - 4)\n return _border_row(_terminal_ljust(body, width - 4))\n\n def divider(style=\"solid\"):\n char = \"═\" if style == \"strong\" else \"─\"\n left = \"╔\" if style == \"strong\" else \"├\"\n right = \"╗\" if style == \"strong\" else \"┤\"\n return _color(left + char * (width - 2) + right, WELCOME_BORDER_COLOR)\n\n def bottom_divider():\n return _color(\"╚\" + \"═\" * (width - 2) + \"╝\", WELCOME_BORDER_COLOR)\n\n def _border_row(body):\n return f\"{_color('║', WELCOME_BORDER_COLOR)} {body} {_color('║', WELCOME_BORDER_COLOR)}\"\n\n def center(text):\n body = _terminal_middle(text, inner)\n return _border_row(_terminal_center(body, inner))\n\n def split_row(left_text, right_text, left_size, right_size):\n left_body = _terminal_ljust(str(left_text), left_size)\n right_body = _terminal_ljust(str(right_text), right_size)\n return _border_row(f\"{left_body}{' ' * gap}{right_body}\")\n\n top_gap = gap\n art_width = max(_terminal_width(_gradient_text(text)) for text in WELCOME_ART)\n top_left_width = min(max(art_width, 24), max(24, inner - 34))\n top_right_width = max(28, inner - top_gap - top_left_width)\n label_width = 11\n value_width = max(12, top_right_width - label_width)\n right_block = [\n f\"{'WORKSPACE':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.cwd, value_width), value_width).lstrip()}\",\n f\"{'MODEL':<{label_width}}{_terminal_ljust(_terminal_middle(model, value_width), value_width).lstrip()}\",\n f\"{'APPROVAL':<{label_width}}{_terminal_ljust(_terminal_middle(agent.approval_policy, value_width), value_width).lstrip()}\",\n f\"{'BRANCH':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.branch, value_width), value_width).lstrip()}\",\n f\"{'SESSION':<{label_width}}{_terminal_ljust(_terminal_middle(agent.session['id'], value_width), value_width).lstrip()}\",\n ]\n top_height = max(len(WELCOME_ART), len(right_block))\n left_block = list(WELCOME_ART) + [\"\"] * max(0, top_height - len(WELCOME_ART))\n right_padding_top = max(0, (top_height - len(right_block)) // 2)\n right_block = [\"\"] * right_padding_top + right_block\n right_block.extend([\"\"] * max(0, top_height - len(right_block)))\n\n line = divider(\"strong\")\n rows = [\n split_row(\n _terminal_ljust(_gradient_text(left_block[index]) if left_block[index] else \"\", top_left_width),\n _terminal_ljust(right_block[index], top_right_width) if right_block[index] else \" \" * top_right_width,\n top_left_width,\n top_right_width,\n )\n for index in range(top_height)\n ]\n rows.extend(\n [\n divider(),\n center(WELCOME_NAME),\n center(WELCOME_SUBTITLE),\n ]\n )\n return \"\\n\".join([line, *rows, bottom_divider()])\n\n\ndef build_agent(args):\n\n\n\n workspace = WorkspaceContext.build(args.cwd)\n\n load_project_env(workspace.repo_root)\n\n\n configured_secret_names = _configured_secret_names(args)\n store = SessionStore(Path(workspace.repo_root) / AGENT_STATE_DIR / \"sessions\")\n\n model = _build_model_client(args, retry_reporter=_stderr_retry_reporter)\n session_id = args.resume\n if session_id == \"latest\":\n session_id = store.latest()\n if session_id:\n return Pico.from_session(\n model_client=model,\n workspace=workspace,\n session_store=store,\n session_id=session_id,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n tool_call_reporter=_stderr_tool_call_reporter,\n assistant_message_reporter=_stderr_assistant_message_reporter,\n )\n return Pico(\n model_client=model,\n workspace=workspace,\n session_store=store,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n tool_call_reporter=_stderr_tool_call_reporter,\n assistant_message_reporter=_stderr_assistant_message_reporter,\n )\n\n\ndef _maybe_evolve_durable_memory(agent, reason):\n result = agent.evolve_durable_memory(reason=reason)\n if result.get(\"status\") == \"failed\":\n print(f\"durable memory evolution failed: {result.get('error', '')}\", file=sys.stderr)\n return result\n\n\ndef build_arg_parser():\n parser = argparse.ArgumentParser(\n formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n description=\"Minimal coding agent for DeepSeek, OpenAI-compatible, Anthropic-compatible, or Ollama models.\",\n )\n parser.add_argument(\"prompt\", nargs=\"*\", help=\"Optional one-shot prompt.\")\n parser.add_argument(\"--cwd\", default=\".\", help=\"Workspace directory.\")\n parser.add_argument(\"--provider\", choices=(\"ollama\", \"openai\", \"anthropic\", \"deepseek\"), default=\"openai\", help=\"Model backend to use.\")\n parser.add_argument(\n \"--model\",\n default=None,\n help=\"Model name override. Defaults to qwen3.5:4b for Ollama, LUMO_OPENAI_MODEL for openai, LUMO_ANTHROPIC_MODEL for anthropic, and LUMO_DEEPSEEK_MODEL for deepseek when set.\",\n )\n parser.add_argument(\"--host\", default=DEFAULT_OLLAMA_HOST, help=\"Ollama server URL.\")\n parser.add_argument(\"--base-url\", default=None, help=\"Provider API base URL for deepseek, openai, or anthropic.\")\n parser.add_argument(\"--ollama-timeout\", type=int, default=300, help=\"Ollama request timeout in seconds.\")\n parser.add_argument(\"--openai-timeout\", type=int, default=300, help=\"OpenAI-compatible request timeout in seconds.\")\n parser.add_argument(\"--resume\", default=None, help=\"Session id to resume or 'latest'.\")\n parser.add_argument(\"--approval\", choices=(\"ask\", \"auto\", \"never\"), default=\"ask\", help=\"Approval policy for risky tools.\")\n parser.add_argument(\n \"--secret-env-name\",\n dest=\"secret_env_names\",\n action=\"append\",\n default=[],\n help=\"Extra environment variable names to treat as secrets for trace/report redaction.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=12, help=\"Maximum tool/model iterations per request.\")\n parser.add_argument(\"--max-new-tokens\", type=int, default=8192, help=\"Maximum model output tokens per step.\")\n parser.add_argument(\"--no-memory-evolution\", action=\"store_true\", help=\"Disable session-end durable memory evolution.\")\n parser.add_argument(\"--temperature\", type=float, default=0.2, help=\"Sampling temperature sent to Ollama.\")\n parser.add_argument(\"--top-p\", type=float, default=0.9, help=\"Top-p sampling value sent to Ollama.\")\n return parser\n\n\ndef main(argv=None):\n args = build_arg_parser().parse_args(argv)\n agent = build_agent(args)\n\n model = getattr(agent.model_client, \"model\", getattr(args, \"model\", DEFAULT_OLLAMA_MODEL))\n\n host = getattr(agent.model_client, \"host\", getattr(agent.model_client, \"base_url\", getattr(args, \"host\", DEFAULT_OLLAMA_HOST)))\n print(build_welcome(agent, model=model, host=host))\n\n if args.prompt:\n\n prompt = \" \".join(args.prompt).strip()\n if prompt:\n print()\n try:\n print(agent.ask(prompt))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n return 1\n finally:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n while True:\n\n\n try:\n user_input = input(\"\\nLUMO> \").strip()\n except (EOFError, KeyboardInterrupt):\n print(\"\")\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n if not user_input:\n continue\n if user_input in {\"/exit\", \"/quit\"}:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n if user_input == \"/help\":\n print(HELP_DETAILS)\n continue\n if user_input == \"/memory\":\n print(agent.memory_text())\n continue\n if user_input == \"/skills\":\n print(skilllib.render_skill_catalog(agent.refresh_skill_catalog()))\n continue\n if user_input == \"/session\":\n print(agent.session_path)\n continue\n if user_input == \"/reset\":\n agent.reset()\n print(\"session reset\")\n continue\n\n print()\n try:\n print(agent.ask(user_input))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n"}, "80539834b512486faae99c513edeeb328769600a": {"t": 1785767240.9646413, "n": 19713, "h": "c8c26a43f367a58f", "c": "\"\"\"命令行入口。\n\n这个模块负责把“用户怎么启动 Lumo”翻译成 runtime 能理解的对象:\n解析参数、挑模型后端、构建工作区快照、恢复或新建 session,\n最后进入 one-shot 或交互式循环。\n\"\"\"\n\nimport argparse\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nimport unicodedata\nfrom pathlib import Path\n\nfrom .config import load_project_env, provider_env\nfrom .providers.clients import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient\nfrom .runtime import Pico, SessionStore\nfrom . import skills as skilllib\nfrom .workspace import AGENT_STATE_DIR, WorkspaceContext\n\nDEFAULT_SECRET_ENV_NAMES = (\n \"LUMO_OPENAI_API_KEY\",\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENAI_API_TOKEN\",\n \"LUMO_ANTHROPIC_API_KEY\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"LUMO_DEEPSEEK_API_KEY\",\n \"PICO_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"LUMO_RIGHT_CODES_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"GITHUB_PAT\",\n \"GH_PAT\",\n)\n\nWELCOME_ART = tuple(\n \" \".join(parts)\n for parts in (\n (\"██ \", \"██ ██\", \"██ ██\", \"███████\"),\n (\"██ \", \"██ ██\", \"███ ███\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"███████\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ █ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"███████\", \"███████\", \"██ ██\", \"███████\"),\n )\n)\nWELCOME_ART = (\n \"██ ██ ██ ███ ███ ███████\",\n \"██ ██ ██ ████ ████ ██ ██\",\n \"██ ██ ██ ██ ███ ██ ██ ██\",\n \"██ ██ ██ ██ █ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"████████ ███████ ██ ██ ███████\",\n)\nWELCOME_NAME = \"Lumo\"\nWELCOME_SUBTITLE = \"Code is cheap, Show me your talk\"\n\nHELP_DETAILS = textwrap.dedent(\n \"\"\"\\\n Commands:\n /help Show this help message.\n /memory Show durable memory loaded from .lumo/memory.\n /skills List workspace skills grouped by category.\n /session Show the path to the saved session file.\n /reset Clear the current session history and memory.\n /exit Exit the agent.\n \"\"\"\n).strip()\n\n\nDEFAULT_OLLAMA_MODEL = \"qwen3.5:4b\"\nDEFAULT_OLLAMA_HOST = \"http://127.0.0.1:11434\"\nDEFAULT_OPENAI_MODEL = \"gpt-5.4\"\nDEFAULT_OPENAI_BASE_URL = \"https://www.right.codes/codex/v1\"\nDEFAULT_ANTHROPIC_MODEL = \"claude-sonnet-4-6\"\nDEFAULT_ANTHROPIC_BASE_URL = \"https://www.right.codes/claude/v1\"\nDEFAULT_DEEPSEEK_MODEL = \"deepseek-v4-pro\"\nDEFAULT_DEEPSEEK_BASE_URL = \"https://api.deepseek.com/anthropic\"\nOPENAI_REASONING_EFFORTS = {\"none\", \"minimal\", \"low\", \"medium\", \"high\", \"xhigh\"}\nSECRET_ENV_NAMES_VARS = (\"LUMO_SECRET_ENV_NAMES\", \"PICO_SECRET_ENV_NAMES\")\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-9;]*m\")\nANSI_RESET = \"\\x1b[0m\"\nWELCOME_BORDER_COLOR = \"\\x1b[38;2;125;211;252m\"\nWELCOME_GRADIENT_START = (255, 255, 255)\nWELCOME_GRADIENT_END = (125, 211, 252)\n\n\ndef _strip_ansi(text):\n return ANSI_PATTERN.sub(\"\", str(text))\n\n\ndef _ansi_rgb(red, green, blue):\n return f\"\\x1b[38;2;{red};{green};{blue}m\"\n\n\ndef _color(text, ansi_color):\n return f\"{ansi_color}{text}{ANSI_RESET}\"\n\n\ndef _stderr_tool_call_reporter(name, summary):\n print(f\"[tool] {name} {summary}\", file=sys.stderr)\n\n\ndef _stderr_assistant_message_reporter(message):\n print(f\"[assistant] {message}\", file=sys.stderr)\n\n\ndef _gradient_text(text, start_rgb=WELCOME_GRADIENT_START, end_rgb=WELCOME_GRADIENT_END):\n text = str(text)\n span = max(1, len(text) - 1)\n parts = []\n for index, char in enumerate(text):\n if char == \" \":\n parts.append(char)\n continue\n ratio = index / span\n rgb = tuple(round(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb))\n parts.append(_color(char, _ansi_rgb(*rgb)))\n return \"\".join(parts)\n\n\ndef _terminal_char_width(char):\n return 2 if unicodedata.east_asian_width(char) in (\"F\", \"W\") else 1\n\n\ndef _terminal_width(text):\n return sum(_terminal_char_width(char) for char in _strip_ansi(text))\n\n\ndef _terminal_ljust(text, width):\n text = str(text)\n return text + \" \" * max(0, width - _terminal_width(text))\n\n\ndef _terminal_center(text, width):\n text = str(text)\n padding = max(0, width - _terminal_width(text))\n left = padding // 2\n right = padding - left\n return \" \" * left + text + \" \" * right\n\n\ndef _take_terminal_width(text, width, from_end=False):\n chars = reversed(str(text)) if from_end else str(text)\n selected = []\n used = 0\n for char in chars:\n char_width = _terminal_char_width(char)\n if used + char_width > width:\n break\n selected.append(char)\n used += char_width\n if from_end:\n selected.reverse()\n return \"\".join(selected)\n\n\ndef _terminal_middle(text, limit):\n text = str(text).replace(\"\\n\", \" \")\n if _terminal_width(text) <= limit:\n return text\n if limit <= 3:\n return _take_terminal_width(text, limit)\n left = (limit - 3) // 2\n right = limit - 3 - left\n return _take_terminal_width(text, left) + \"...\" + _take_terminal_width(text, right, from_end=True)\n\n\ndef _effective_model(args, provider):\n\n\n\n\n explicit_model = getattr(args, \"model\", None)\n if explicit_model:\n return explicit_model\n if provider == \"openai\":\n model = provider_env(\"LUMO_OPENAI_MODEL\", (\"PICO_OPENAI_MODEL\", \"OPENAI_MODEL\"))\n if model:\n return model\n return DEFAULT_OPENAI_MODEL\n if provider == \"anthropic\":\n model = provider_env(\"LUMO_ANTHROPIC_MODEL\", (\"PICO_ANTHROPIC_MODEL\", \"ANTHROPIC_MODEL\"))\n if model:\n return model\n return DEFAULT_ANTHROPIC_MODEL\n if provider == \"deepseek\":\n model = provider_env(\"LUMO_DEEPSEEK_MODEL\", (\"PICO_DEEPSEEK_MODEL\", \"DEEPSEEK_MODEL\"))\n if model:\n return model\n return DEFAULT_DEEPSEEK_MODEL\n return DEFAULT_OLLAMA_MODEL\n\n\ndef _openai_reasoning_effort():\n effort = provider_env(\"LUMO_OPENAI_MODEL_EFFORT\").strip().lower()\n if effort and effort not in OPENAI_REASONING_EFFORTS:\n allowed = \", \".join(sorted(OPENAI_REASONING_EFFORTS))\n raise ValueError(f\"LUMO_OPENAI_MODEL_EFFORT must be one of: {allowed}\")\n return effort or None\n\n\ndef _openai_temperature():\n value = provider_env(\"LUMO_OPENAI_TEMPERATURE\").strip()\n if not value:\n return None\n try:\n temperature = float(value)\n except ValueError as exc:\n raise ValueError(\"LUMO_OPENAI_TEMPERATURE must be a number between 0 and 2\") from exc\n if not 0 <= temperature <= 2:\n raise ValueError(\"LUMO_OPENAI_TEMPERATURE must be a number between 0 and 2\")\n return temperature\n\n\ndef _configured_secret_names(args):\n configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)\n configured_secret_names.update(str(name).upper() for name in args.secret_env_names)\n for env_var in SECRET_ENV_NAMES_VARS:\n extra_names = os.environ.get(env_var, \"\")\n if extra_names.strip():\n configured_secret_names.update(\n item.strip().upper()\n for item in extra_names.split(\",\")\n if item.strip()\n )\n return sorted(configured_secret_names)\n\n\ndef _build_model_client(args):\n provider = getattr(args, \"provider\", \"deepseek\")\n\n\n if provider == \"openai\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\n \"LUMO_OPENAI_API_BASE\",\n (\"PICO_OPENAI_API_BASE\", \"OPENAI_API_BASE\"),\n DEFAULT_OPENAI_BASE_URL,\n )\n api_key = provider_env(\n \"LUMO_OPENAI_API_KEY\",\n (\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"LUMO_RIGHT_CODES_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"LUMO_ANTHROPIC_API_KEY\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n ),\n )\n return OpenAICompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=_openai_temperature(),\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n reasoning_effort=_openai_reasoning_effort(),\n )\n if provider == \"anthropic\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\n \"LUMO_ANTHROPIC_API_BASE\",\n (\"PICO_ANTHROPIC_API_BASE\", \"ANTHROPIC_API_BASE\"),\n DEFAULT_ANTHROPIC_BASE_URL,\n )\n api_key = provider_env(\n \"LUMO_ANTHROPIC_API_KEY\",\n (\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"LUMO_RIGHT_CODES_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"LUMO_OPENAI_API_KEY\",\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n ),\n )\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"deepseek\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\n \"LUMO_DEEPSEEK_API_BASE\",\n (\"PICO_DEEPSEEK_API_BASE\", \"DEEPSEEK_API_BASE\"),\n DEFAULT_DEEPSEEK_BASE_URL,\n )\n api_key = provider_env(\"LUMO_DEEPSEEK_API_KEY\", (\"PICO_DEEPSEEK_API_KEY\", \"DEEPSEEK_API_KEY\"))\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n\n model = _effective_model(args, provider)\n host = getattr(args, \"host\", DEFAULT_OLLAMA_HOST)\n return OllamaModelClient(\n model=model,\n host=host,\n temperature=args.temperature,\n top_p=args.top_p,\n timeout=args.ollama_timeout,\n )\n\n\ndef build_welcome(agent, model, host):\n width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))\n inner = width - 4\n gap = 3\n\n def row(text):\n body = _terminal_middle(text, width - 4)\n return _border_row(_terminal_ljust(body, width - 4))\n\n def divider(style=\"solid\"):\n char = \"═\" if style == \"strong\" else \"─\"\n left = \"╔\" if style == \"strong\" else \"├\"\n right = \"╗\" if style == \"strong\" else \"┤\"\n return _color(left + char * (width - 2) + right, WELCOME_BORDER_COLOR)\n\n def bottom_divider():\n return _color(\"╚\" + \"═\" * (width - 2) + \"╝\", WELCOME_BORDER_COLOR)\n\n def _border_row(body):\n return f\"{_color('║', WELCOME_BORDER_COLOR)} {body} {_color('║', WELCOME_BORDER_COLOR)}\"\n\n def center(text):\n body = _terminal_middle(text, inner)\n return _border_row(_terminal_center(body, inner))\n\n def split_row(left_text, right_text, left_size, right_size):\n left_body = _terminal_ljust(str(left_text), left_size)\n right_body = _terminal_ljust(str(right_text), right_size)\n return _border_row(f\"{left_body}{' ' * gap}{right_body}\")\n\n top_gap = gap\n art_width = max(_terminal_width(_gradient_text(text)) for text in WELCOME_ART)\n top_left_width = min(max(art_width, 24), max(24, inner - 34))\n top_right_width = max(28, inner - top_gap - top_left_width)\n label_width = 11\n value_width = max(12, top_right_width - label_width)\n right_block = [\n f\"{'WORKSPACE':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.cwd, value_width), value_width).lstrip()}\",\n f\"{'MODEL':<{label_width}}{_terminal_ljust(_terminal_middle(model, value_width), value_width).lstrip()}\",\n f\"{'APPROVAL':<{label_width}}{_terminal_ljust(_terminal_middle(agent.approval_policy, value_width), value_width).lstrip()}\",\n f\"{'BRANCH':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.branch, value_width), value_width).lstrip()}\",\n f\"{'SESSION':<{label_width}}{_terminal_ljust(_terminal_middle(agent.session['id'], value_width), value_width).lstrip()}\",\n ]\n top_height = max(len(WELCOME_ART), len(right_block))\n left_block = list(WELCOME_ART) + [\"\"] * max(0, top_height - len(WELCOME_ART))\n right_padding_top = max(0, (top_height - len(right_block)) // 2)\n right_block = [\"\"] * right_padding_top + right_block\n right_block.extend([\"\"] * max(0, top_height - len(right_block)))\n\n line = divider(\"strong\")\n rows = [\n split_row(\n _terminal_ljust(_gradient_text(left_block[index]) if left_block[index] else \"\", top_left_width),\n _terminal_ljust(right_block[index], top_right_width) if right_block[index] else \" \" * top_right_width,\n top_left_width,\n top_right_width,\n )\n for index in range(top_height)\n ]\n rows.extend(\n [\n divider(),\n center(WELCOME_NAME),\n center(WELCOME_SUBTITLE),\n ]\n )\n return \"\\n\".join([line, *rows, bottom_divider()])\n\n\ndef build_agent(args):\n \"\"\"根据 CLI 参数装配出一个可运行的 Pico 实例。\n\n 为什么存在:\n 命令行参数只是字符串和开关,runtime 需要的是已经装配好的对象图:\n model client、workspace snapshot、session store、secret 配置等。\n 这个函数负责把“启动参数”翻译成“agent 运行现场”。\n\n 输入 / 输出:\n - 输入:`argparse` 解析后的 `args`\n - 输出:一个新的 `Pico`,或一个从旧 session 恢复出来的 `Pico`\n\n 在 agent 链路里的位置:\n 它是整个程序启动链路里最靠近 runtime 的装配点。`main()` 先调它,\n 得到 agent 后,后面无论是 one-shot 还是 REPL 模式,都会落到 `ask()`。\n \"\"\"\n\n\n workspace = WorkspaceContext.build(args.cwd)\n\n load_project_env(workspace.repo_root)\n\n\n configured_secret_names = _configured_secret_names(args)\n store = SessionStore(Path(workspace.repo_root) / AGENT_STATE_DIR / \"sessions\")\n\n model = _build_model_client(args)\n session_id = args.resume\n if session_id == \"latest\":\n session_id = store.latest()\n if session_id:\n return Pico.from_session(\n model_client=model,\n workspace=workspace,\n session_store=store,\n session_id=session_id,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n tool_call_reporter=_stderr_tool_call_reporter,\n assistant_message_reporter=_stderr_assistant_message_reporter,\n )\n return Pico(\n model_client=model,\n workspace=workspace,\n session_store=store,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n tool_call_reporter=_stderr_tool_call_reporter,\n assistant_message_reporter=_stderr_assistant_message_reporter,\n )\n\n\ndef _maybe_evolve_durable_memory(agent, reason):\n result = agent.evolve_durable_memory(reason=reason)\n if result.get(\"status\") == \"failed\":\n print(f\"durable memory evolution failed: {result.get('error', '')}\", file=sys.stderr)\n return result\n\n\ndef build_arg_parser():\n parser = argparse.ArgumentParser(\n formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n description=\"Minimal coding agent for DeepSeek, OpenAI-compatible, Anthropic-compatible, or Ollama models.\",\n )\n parser.add_argument(\"prompt\", nargs=\"*\", help=\"Optional one-shot prompt.\")\n parser.add_argument(\"--cwd\", default=\".\", help=\"Workspace directory.\")\n parser.add_argument(\"--provider\", choices=(\"ollama\", \"openai\", \"anthropic\", \"deepseek\"), default=\"openai\", help=\"Model backend to use.\")\n parser.add_argument(\n \"--model\",\n default=None,\n help=\"Model name override. Defaults to qwen3.5:4b for Ollama, LUMO_OPENAI_MODEL for openai, LUMO_ANTHROPIC_MODEL for anthropic, and LUMO_DEEPSEEK_MODEL for deepseek when set.\",\n )\n parser.add_argument(\"--host\", default=DEFAULT_OLLAMA_HOST, help=\"Ollama server URL.\")\n parser.add_argument(\"--base-url\", default=None, help=\"Provider API base URL for deepseek, openai, or anthropic.\")\n parser.add_argument(\"--ollama-timeout\", type=int, default=300, help=\"Ollama request timeout in seconds.\")\n parser.add_argument(\"--openai-timeout\", type=int, default=300, help=\"OpenAI-compatible request timeout in seconds.\")\n parser.add_argument(\"--resume\", default=None, help=\"Session id to resume or 'latest'.\")\n parser.add_argument(\"--approval\", choices=(\"ask\", \"auto\", \"never\"), default=\"ask\", help=\"Approval policy for risky tools.\")\n parser.add_argument(\n \"--secret-env-name\",\n dest=\"secret_env_names\",\n action=\"append\",\n default=[],\n help=\"Extra environment variable names to treat as secrets for trace/report redaction.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=12, help=\"Maximum tool/model iterations per request.\")\n parser.add_argument(\"--max-new-tokens\", type=int, default=8192, help=\"Maximum model output tokens per step.\")\n parser.add_argument(\"--no-memory-evolution\", action=\"store_true\", help=\"Disable session-end durable memory evolution.\")\n parser.add_argument(\"--temperature\", type=float, default=0.2, help=\"Sampling temperature sent to Ollama.\")\n parser.add_argument(\"--top-p\", type=float, default=0.9, help=\"Top-p sampling value sent to Ollama.\")\n return parser\n\n\ndef main(argv=None):\n args = build_arg_parser().parse_args(argv)\n agent = build_agent(args)\n\n model = getattr(agent.model_client, \"model\", getattr(args, \"model\", DEFAULT_OLLAMA_MODEL))\n\n host = getattr(agent.model_client, \"host\", getattr(agent.model_client, \"base_url\", getattr(args, \"host\", DEFAULT_OLLAMA_HOST)))\n print(build_welcome(agent, model=model, host=host))\n\n if args.prompt:\n\n prompt = \" \".join(args.prompt).strip()\n if prompt:\n print()\n try:\n print(agent.ask(prompt))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n return 1\n finally:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n while True:\n\n\n try:\n user_input = input(\"\\nLUMO> \").strip()\n except (EOFError, KeyboardInterrupt):\n print(\"\")\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n if not user_input:\n continue\n if user_input in {\"/exit\", \"/quit\"}:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n if user_input == \"/help\":\n print(HELP_DETAILS)\n continue\n if user_input == \"/memory\":\n print(agent.memory_text())\n continue\n if user_input == \"/skills\":\n print(skilllib.render_skill_catalog(agent.refresh_skill_catalog()))\n continue\n if user_input == \"/session\":\n print(agent.session_path)\n continue\n if user_input == \"/reset\":\n agent.reset()\n print(\"session reset\")\n continue\n\n print()\n try:\n print(agent.ask(user_input))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n"}, "6fa1e6ecdf14701f4d99ca8546a680189dc1a71f": {"t": 1785767243.414883, "n": 18679, "h": "3894a5d2a79bc6f1", "c": "\"\"\"命令行入口。\n\n这个模块负责把“用户怎么启动 Lumo”翻译成 runtime 能理解的对象:\n解析参数、挑模型后端、构建工作区快照、恢复或新建 session,\n最后进入 one-shot 或交互式循环。\n\"\"\"\n\nimport argparse\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nimport unicodedata\nfrom pathlib import Path\n\nfrom .config import load_project_env, provider_env\nfrom .providers.clients import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient\nfrom .runtime import Pico, SessionStore\nfrom .workspace import AGENT_STATE_DIR, WorkspaceContext\n\nDEFAULT_SECRET_ENV_NAMES = (\n \"LUMO_OPENAI_API_KEY\",\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENAI_API_TOKEN\",\n \"LUMO_ANTHROPIC_API_KEY\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"LUMO_DEEPSEEK_API_KEY\",\n \"PICO_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"LUMO_RIGHT_CODES_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"GITHUB_PAT\",\n \"GH_PAT\",\n)\n\nWELCOME_ART = tuple(\n \" \".join(parts)\n for parts in (\n (\"██ \", \"██ ██\", \"██ ██\", \"███████\"),\n (\"██ \", \"██ ██\", \"███ ███\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"███████\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ █ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"███████\", \"███████\", \"██ ██\", \"███████\"),\n )\n)\nWELCOME_ART = (\n \"██ ██ ██ ███ ███ ███████\",\n \"██ ██ ██ ████ ████ ██ ██\",\n \"██ ██ ██ ██ ███ ██ ██ ██\",\n \"██ ██ ██ ██ █ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"████████ ███████ ██ ██ ███████\",\n)\nWELCOME_NAME = \"Lumo\"\nWELCOME_SUBTITLE = \"Code is cheap, Show me your talk\"\n\nHELP_DETAILS = textwrap.dedent(\n \"\"\"\\\n Commands:\n /help Show this help message.\n /memory Show durable memory loaded from .lumo/memory.\n /session Show the path to the saved session file.\n /reset Clear the current session history and memory.\n /exit Exit the agent.\n \"\"\"\n).strip()\n\n\nDEFAULT_OLLAMA_MODEL = \"qwen3.5:4b\"\nDEFAULT_OLLAMA_HOST = \"http://127.0.0.1:11434\"\nDEFAULT_OPENAI_MODEL = \"gpt-5.4\"\nDEFAULT_OPENAI_BASE_URL = \"https://www.right.codes/codex/v1\"\nDEFAULT_ANTHROPIC_MODEL = \"claude-sonnet-4-6\"\nDEFAULT_ANTHROPIC_BASE_URL = \"https://www.right.codes/claude/v1\"\nDEFAULT_DEEPSEEK_MODEL = \"deepseek-v4-pro\"\nDEFAULT_DEEPSEEK_BASE_URL = \"https://api.deepseek.com/anthropic\"\nSECRET_ENV_NAMES_VARS = (\"LUMO_SECRET_ENV_NAMES\", \"PICO_SECRET_ENV_NAMES\")\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-9;]*m\")\nANSI_RESET = \"\\x1b[0m\"\nWELCOME_BORDER_COLOR = \"\\x1b[38;2;125;211;252m\"\nWELCOME_GRADIENT_START = (255, 255, 255)\nWELCOME_GRADIENT_END = (125, 211, 252)\n\n\ndef _strip_ansi(text):\n return ANSI_PATTERN.sub(\"\", str(text))\n\n\ndef _ansi_rgb(red, green, blue):\n return f\"\\x1b[38;2;{red};{green};{blue}m\"\n\n\ndef _color(text, ansi_color):\n return f\"{ansi_color}{text}{ANSI_RESET}\"\n\n\ndef _stderr_tool_call_reporter(name, summary):\n print(f\"[tool] {name} {summary}\", file=sys.stderr)\n\n\ndef _stderr_assistant_message_reporter(message):\n print(f\"[assistant] {message}\", file=sys.stderr)\n\n\ndef _gradient_text(text, start_rgb=WELCOME_GRADIENT_START, end_rgb=WELCOME_GRADIENT_END):\n text = str(text)\n span = max(1, len(text) - 1)\n parts = []\n for index, char in enumerate(text):\n if char == \" \":\n parts.append(char)\n continue\n ratio = index / span\n rgb = tuple(round(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb))\n parts.append(_color(char, _ansi_rgb(*rgb)))\n return \"\".join(parts)\n\n\ndef _terminal_char_width(char):\n return 2 if unicodedata.east_asian_width(char) in (\"F\", \"W\") else 1\n\n\ndef _terminal_width(text):\n return sum(_terminal_char_width(char) for char in _strip_ansi(text))\n\n\ndef _terminal_ljust(text, width):\n text = str(text)\n return text + \" \" * max(0, width - _terminal_width(text))\n\n\ndef _terminal_center(text, width):\n text = str(text)\n padding = max(0, width - _terminal_width(text))\n left = padding // 2\n right = padding - left\n return \" \" * left + text + \" \" * right\n\n\ndef _take_terminal_width(text, width, from_end=False):\n chars = reversed(str(text)) if from_end else str(text)\n selected = []\n used = 0\n for char in chars:\n char_width = _terminal_char_width(char)\n if used + char_width > width:\n break\n selected.append(char)\n used += char_width\n if from_end:\n selected.reverse()\n return \"\".join(selected)\n\n\ndef _terminal_middle(text, limit):\n text = str(text).replace(\"\\n\", \" \")\n if _terminal_width(text) <= limit:\n return text\n if limit <= 3:\n return _take_terminal_width(text, limit)\n left = (limit - 3) // 2\n right = limit - 3 - left\n return _take_terminal_width(text, left) + \"...\" + _take_terminal_width(text, right, from_end=True)\n\n\ndef _effective_model(args, provider):\n\n\n\n\n explicit_model = getattr(args, \"model\", None)\n if explicit_model:\n return explicit_model\n if provider == \"openai\":\n model = provider_env(\"LUMO_OPENAI_MODEL\", (\"PICO_OPENAI_MODEL\", \"OPENAI_MODEL\"))\n if model:\n return model\n return DEFAULT_OPENAI_MODEL\n if provider == \"anthropic\":\n model = provider_env(\"LUMO_ANTHROPIC_MODEL\", (\"PICO_ANTHROPIC_MODEL\", \"ANTHROPIC_MODEL\"))\n if model:\n return model\n return DEFAULT_ANTHROPIC_MODEL\n if provider == \"deepseek\":\n model = provider_env(\"LUMO_DEEPSEEK_MODEL\", (\"PICO_DEEPSEEK_MODEL\", \"DEEPSEEK_MODEL\"))\n if model:\n return model\n return DEFAULT_DEEPSEEK_MODEL\n return DEFAULT_OLLAMA_MODEL\n\n\ndef _configured_secret_names(args):\n configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)\n configured_secret_names.update(str(name).upper() for name in args.secret_env_names)\n for env_var in SECRET_ENV_NAMES_VARS:\n extra_names = os.environ.get(env_var, \"\")\n if extra_names.strip():\n configured_secret_names.update(\n item.strip().upper()\n for item in extra_names.split(\",\")\n if item.strip()\n )\n return sorted(configured_secret_names)\n\n\ndef _build_model_client(args):\n provider = getattr(args, \"provider\", \"deepseek\")\n\n\n if provider == \"openai\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\n \"LUMO_OPENAI_API_BASE\",\n (\"PICO_OPENAI_API_BASE\", \"OPENAI_API_BASE\"),\n DEFAULT_OPENAI_BASE_URL,\n )\n api_key = provider_env(\n \"LUMO_OPENAI_API_KEY\",\n (\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"LUMO_RIGHT_CODES_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"LUMO_ANTHROPIC_API_KEY\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n ),\n )\n return OpenAICompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"anthropic\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\n \"LUMO_ANTHROPIC_API_BASE\",\n (\"PICO_ANTHROPIC_API_BASE\", \"ANTHROPIC_API_BASE\"),\n DEFAULT_ANTHROPIC_BASE_URL,\n )\n api_key = provider_env(\n \"LUMO_ANTHROPIC_API_KEY\",\n (\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"LUMO_RIGHT_CODES_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"LUMO_OPENAI_API_KEY\",\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n ),\n )\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"deepseek\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\n \"LUMO_DEEPSEEK_API_BASE\",\n (\"PICO_DEEPSEEK_API_BASE\", \"DEEPSEEK_API_BASE\"),\n DEFAULT_DEEPSEEK_BASE_URL,\n )\n api_key = provider_env(\"LUMO_DEEPSEEK_API_KEY\", (\"PICO_DEEPSEEK_API_KEY\", \"DEEPSEEK_API_KEY\"))\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n\n model = _effective_model(args, provider)\n host = getattr(args, \"host\", DEFAULT_OLLAMA_HOST)\n return OllamaModelClient(\n model=model,\n host=host,\n temperature=args.temperature,\n top_p=args.top_p,\n timeout=args.ollama_timeout,\n )\n\n\ndef build_welcome(agent, model, host):\n width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))\n inner = width - 4\n gap = 3\n\n def row(text):\n body = _terminal_middle(text, width - 4)\n return _border_row(_terminal_ljust(body, width - 4))\n\n def divider(style=\"solid\"):\n char = \"═\" if style == \"strong\" else \"─\"\n left = \"╔\" if style == \"strong\" else \"├\"\n right = \"╗\" if style == \"strong\" else \"┤\"\n return _color(left + char * (width - 2) + right, WELCOME_BORDER_COLOR)\n\n def bottom_divider():\n return _color(\"╚\" + \"═\" * (width - 2) + \"╝\", WELCOME_BORDER_COLOR)\n\n def _border_row(body):\n return f\"{_color('║', WELCOME_BORDER_COLOR)} {body} {_color('║', WELCOME_BORDER_COLOR)}\"\n\n def center(text):\n body = _terminal_middle(text, inner)\n return _border_row(_terminal_center(body, inner))\n\n def split_row(left_text, right_text, left_size, right_size):\n left_body = _terminal_ljust(str(left_text), left_size)\n right_body = _terminal_ljust(str(right_text), right_size)\n return _border_row(f\"{left_body}{' ' * gap}{right_body}\")\n\n top_gap = gap\n art_width = max(_terminal_width(_gradient_text(text)) for text in WELCOME_ART)\n top_left_width = min(max(art_width, 24), max(24, inner - 34))\n top_right_width = max(28, inner - top_gap - top_left_width)\n label_width = 11\n value_width = max(12, top_right_width - label_width)\n right_block = [\n f\"{'WORKSPACE':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.cwd, value_width), value_width).lstrip()}\",\n f\"{'MODEL':<{label_width}}{_terminal_ljust(_terminal_middle(model, value_width), value_width).lstrip()}\",\n f\"{'APPROVAL':<{label_width}}{_terminal_ljust(_terminal_middle(agent.approval_policy, value_width), value_width).lstrip()}\",\n f\"{'BRANCH':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.branch, value_width), value_width).lstrip()}\",\n f\"{'SESSION':<{label_width}}{_terminal_ljust(_terminal_middle(agent.session['id'], value_width), value_width).lstrip()}\",\n ]\n top_height = max(len(WELCOME_ART), len(right_block))\n left_block = list(WELCOME_ART) + [\"\"] * max(0, top_height - len(WELCOME_ART))\n right_padding_top = max(0, (top_height - len(right_block)) // 2)\n right_block = [\"\"] * right_padding_top + right_block\n right_block.extend([\"\"] * max(0, top_height - len(right_block)))\n\n line = divider(\"strong\")\n rows = [\n split_row(\n _terminal_ljust(_gradient_text(left_block[index]) if left_block[index] else \"\", top_left_width),\n _terminal_ljust(right_block[index], top_right_width) if right_block[index] else \" \" * top_right_width,\n top_left_width,\n top_right_width,\n )\n for index in range(top_height)\n ]\n rows.extend(\n [\n divider(),\n center(WELCOME_NAME),\n center(WELCOME_SUBTITLE),\n ]\n )\n return \"\\n\".join([line, *rows, bottom_divider()])\n\n\ndef build_agent(args):\n \"\"\"根据 CLI 参数装配出一个可运行的 Pico 实例。\n\n 为什么存在:\n 命令行参数只是字符串和开关,runtime 需要的是已经装配好的对象图:\n model client、workspace snapshot、session store、secret 配置等。\n 这个函数负责把“启动参数”翻译成“agent 运行现场”。\n\n 输入 / 输出:\n - 输入:`argparse` 解析后的 `args`\n - 输出:一个新的 `Pico`,或一个从旧 session 恢复出来的 `Pico`\n\n 在 agent 链路里的位置:\n 它是整个程序启动链路里最靠近 runtime 的装配点。`main()` 先调它,\n 得到 agent 后,后面无论是 one-shot 还是 REPL 模式,都会落到 `ask()`。\n \"\"\"\n\n\n workspace = WorkspaceContext.build(args.cwd)\n\n load_project_env(workspace.repo_root)\n\n\n configured_secret_names = _configured_secret_names(args)\n store = SessionStore(Path(workspace.repo_root) / AGENT_STATE_DIR / \"sessions\")\n\n model = _build_model_client(args)\n session_id = args.resume\n if session_id == \"latest\":\n session_id = store.latest()\n if session_id:\n return Pico.from_session(\n model_client=model,\n workspace=workspace,\n session_store=store,\n session_id=session_id,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n feature_flags={\"request_rewrite\": True},\n tool_call_reporter=_stderr_tool_call_reporter,\n assistant_message_reporter=_stderr_assistant_message_reporter,\n )\n return Pico(\n model_client=model,\n workspace=workspace,\n session_store=store,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n feature_flags={\"request_rewrite\": True},\n tool_call_reporter=_stderr_tool_call_reporter,\n assistant_message_reporter=_stderr_assistant_message_reporter,\n )\n\n\ndef _maybe_evolve_durable_memory(agent, reason):\n result = agent.evolve_durable_memory(reason=reason)\n if result.get(\"status\") == \"failed\":\n print(f\"durable memory evolution failed: {result.get('error', '')}\", file=sys.stderr)\n return result\n\n\ndef build_arg_parser():\n parser = argparse.ArgumentParser(\n formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n description=\"Minimal coding agent for DeepSeek, OpenAI-compatible, Anthropic-compatible, or Ollama models.\",\n )\n parser.add_argument(\"prompt\", nargs=\"*\", help=\"Optional one-shot prompt.\")\n parser.add_argument(\"--cwd\", default=\".\", help=\"Workspace directory.\")\n parser.add_argument(\"--provider\", choices=(\"ollama\", \"openai\", \"anthropic\", \"deepseek\"), default=\"openai\", help=\"Model backend to use.\")\n parser.add_argument(\n \"--model\",\n default=None,\n help=\"Model name override. Defaults to qwen3.5:4b for Ollama, LUMO_OPENAI_MODEL for openai, LUMO_ANTHROPIC_MODEL for anthropic, and LUMO_DEEPSEEK_MODEL for deepseek when set.\",\n )\n parser.add_argument(\"--host\", default=DEFAULT_OLLAMA_HOST, help=\"Ollama server URL.\")\n parser.add_argument(\"--base-url\", default=None, help=\"Provider API base URL for deepseek, openai, or anthropic.\")\n parser.add_argument(\"--ollama-timeout\", type=int, default=300, help=\"Ollama request timeout in seconds.\")\n parser.add_argument(\"--openai-timeout\", type=int, default=300, help=\"OpenAI-compatible request timeout in seconds.\")\n parser.add_argument(\"--resume\", default=None, help=\"Session id to resume or 'latest'.\")\n parser.add_argument(\"--approval\", choices=(\"ask\", \"auto\", \"never\"), default=\"ask\", help=\"Approval policy for risky tools.\")\n parser.add_argument(\n \"--secret-env-name\",\n dest=\"secret_env_names\",\n action=\"append\",\n default=[],\n help=\"Extra environment variable names to treat as secrets for trace/report redaction.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=12, help=\"Maximum tool/model iterations per request.\")\n parser.add_argument(\"--max-new-tokens\", type=int, default=8192, help=\"Maximum model output tokens per step.\")\n parser.add_argument(\"--no-memory-evolution\", action=\"store_true\", help=\"Disable session-end durable memory evolution.\")\n parser.add_argument(\"--temperature\", type=float, default=0.2, help=\"Sampling temperature sent to Ollama.\")\n parser.add_argument(\"--top-p\", type=float, default=0.9, help=\"Top-p sampling value sent to Ollama.\")\n return parser\n\n\ndef main(argv=None):\n args = build_arg_parser().parse_args(argv)\n agent = build_agent(args)\n\n model = getattr(agent.model_client, \"model\", getattr(args, \"model\", DEFAULT_OLLAMA_MODEL))\n\n host = getattr(agent.model_client, \"host\", getattr(agent.model_client, \"base_url\", getattr(args, \"host\", DEFAULT_OLLAMA_HOST)))\n print(build_welcome(agent, model=model, host=host))\n\n if args.prompt:\n\n prompt = \" \".join(args.prompt).strip()\n if prompt:\n print()\n try:\n print(agent.ask(prompt))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n return 1\n finally:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n while True:\n\n\n try:\n user_input = input(\"\\nLUMO> \").strip()\n except (EOFError, KeyboardInterrupt):\n print(\"\")\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n if not user_input:\n continue\n if user_input in {\"/exit\", \"/quit\"}:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n if user_input == \"/help\":\n print(HELP_DETAILS)\n continue\n if user_input == \"/memory\":\n print(agent.memory_text())\n continue\n if user_input == \"/session\":\n print(agent.session_path)\n continue\n if user_input == \"/reset\":\n agent.reset()\n print(\"session reset\")\n continue\n\n print()\n try:\n print(agent.ask(user_input))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n"}, "c66541fd2caa1722b3294ad36e61fa6b98478ced": {"t": 1785767245.5112247, "n": 17850, "h": "5fcda383e8256343", "c": "\"\"\"命令行入口。\n\n这个模块负责把“用户怎么启动 Lumo”翻译成 runtime 能理解的对象:\n解析参数、挑模型后端、构建工作区快照、恢复或新建 session,\n最后进入 one-shot 或交互式循环。\n\"\"\"\n\nimport argparse\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nimport unicodedata\nfrom pathlib import Path\n\nfrom .config import load_project_env, provider_env\nfrom .providers.clients import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient\nfrom .runtime import Pico, SessionStore\nfrom .workspace import AGENT_STATE_DIR, WorkspaceContext\n\nDEFAULT_SECRET_ENV_NAMES = (\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENAI_API_TOKEN\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"PICO_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"GITHUB_PAT\",\n \"GH_PAT\",\n)\n\nWELCOME_ART = tuple(\n \" \".join(parts)\n for parts in (\n (\"██ \", \"██ ██\", \"██ ██\", \"███████\"),\n (\"██ \", \"██ ██\", \"███ ███\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"███████\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ █ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"███████\", \"███████\", \"██ ██\", \"███████\"),\n )\n)\nWELCOME_ART = (\n \"██ ██ ██ ███ ███ ███████\",\n \"██ ██ ██ ████ ████ ██ ██\",\n \"██ ██ ██ ██ ███ ██ ██ ██\",\n \"██ ██ ██ ██ █ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"████████ ███████ ██ ██ ███████\",\n)\nWELCOME_NAME = \"Lumo\"\nWELCOME_SUBTITLE = \"Code is cheap, Show me your talk\"\n\nHELP_DETAILS = textwrap.dedent(\n \"\"\"\\\n Commands:\n /help Show this help message.\n /memory Show durable memory loaded from .lumo/memory.\n /session Show the path to the saved session file.\n /reset Clear the current session history and memory.\n /exit Exit the agent.\n \"\"\"\n).strip()\n\n\nDEFAULT_OLLAMA_MODEL = \"qwen3.5:4b\"\nDEFAULT_OLLAMA_HOST = \"http://127.0.0.1:11434\"\nDEFAULT_OPENAI_MODEL = \"gpt-5.4\"\nDEFAULT_OPENAI_BASE_URL = \"https://www.right.codes/codex/v1\"\nDEFAULT_ANTHROPIC_MODEL = \"claude-sonnet-4-6\"\nDEFAULT_ANTHROPIC_BASE_URL = \"https://www.right.codes/claude/v1\"\nDEFAULT_DEEPSEEK_MODEL = \"deepseek-v4-pro\"\nDEFAULT_DEEPSEEK_BASE_URL = \"https://api.deepseek.com/anthropic\"\nSECRET_ENV_NAMES_VAR = \"PICO_SECRET_ENV_NAMES\"\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-9;]*m\")\nANSI_RESET = \"\\x1b[0m\"\nWELCOME_BORDER_COLOR = \"\\x1b[38;2;125;211;252m\"\nWELCOME_GRADIENT_START = (255, 255, 255)\nWELCOME_GRADIENT_END = (125, 211, 252)\n\n\ndef _strip_ansi(text):\n return ANSI_PATTERN.sub(\"\", str(text))\n\n\ndef _ansi_rgb(red, green, blue):\n return f\"\\x1b[38;2;{red};{green};{blue}m\"\n\n\ndef _color(text, ansi_color):\n return f\"{ansi_color}{text}{ANSI_RESET}\"\n\n\ndef _stderr_tool_call_reporter(name, summary):\n print(f\"[tool] {name} {summary}\", file=sys.stderr)\n\n\ndef _stderr_assistant_message_reporter(message):\n print(f\"[assistant] {message}\", file=sys.stderr)\n\n\ndef _gradient_text(text, start_rgb=WELCOME_GRADIENT_START, end_rgb=WELCOME_GRADIENT_END):\n text = str(text)\n span = max(1, len(text) - 1)\n parts = []\n for index, char in enumerate(text):\n if char == \" \":\n parts.append(char)\n continue\n ratio = index / span\n rgb = tuple(round(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb))\n parts.append(_color(char, _ansi_rgb(*rgb)))\n return \"\".join(parts)\n\n\ndef _terminal_char_width(char):\n return 2 if unicodedata.east_asian_width(char) in (\"F\", \"W\") else 1\n\n\ndef _terminal_width(text):\n return sum(_terminal_char_width(char) for char in _strip_ansi(text))\n\n\ndef _terminal_ljust(text, width):\n text = str(text)\n return text + \" \" * max(0, width - _terminal_width(text))\n\n\ndef _terminal_center(text, width):\n text = str(text)\n padding = max(0, width - _terminal_width(text))\n left = padding // 2\n right = padding - left\n return \" \" * left + text + \" \" * right\n\n\ndef _take_terminal_width(text, width, from_end=False):\n chars = reversed(str(text)) if from_end else str(text)\n selected = []\n used = 0\n for char in chars:\n char_width = _terminal_char_width(char)\n if used + char_width > width:\n break\n selected.append(char)\n used += char_width\n if from_end:\n selected.reverse()\n return \"\".join(selected)\n\n\ndef _terminal_middle(text, limit):\n text = str(text).replace(\"\\n\", \" \")\n if _terminal_width(text) <= limit:\n return text\n if limit <= 3:\n return _take_terminal_width(text, limit)\n left = (limit - 3) // 2\n right = limit - 3 - left\n return _take_terminal_width(text, left) + \"...\" + _take_terminal_width(text, right, from_end=True)\n\n\ndef _effective_model(args, provider):\n\n\n\n\n explicit_model = getattr(args, \"model\", None)\n if explicit_model:\n return explicit_model\n if provider == \"openai\":\n model = provider_env(\"PICO_OPENAI_MODEL\", (\"OPENAI_MODEL\",))\n if model:\n return model\n return DEFAULT_OPENAI_MODEL\n if provider == \"anthropic\":\n model = provider_env(\"PICO_ANTHROPIC_MODEL\", (\"ANTHROPIC_MODEL\",))\n if model:\n return model\n return DEFAULT_ANTHROPIC_MODEL\n if provider == \"deepseek\":\n model = provider_env(\"PICO_DEEPSEEK_MODEL\", (\"DEEPSEEK_MODEL\",))\n if model:\n return model\n return DEFAULT_DEEPSEEK_MODEL\n return DEFAULT_OLLAMA_MODEL\n\n\ndef _configured_secret_names(args):\n configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)\n configured_secret_names.update(str(name).upper() for name in args.secret_env_names)\n extra_names = os.environ.get(SECRET_ENV_NAMES_VAR, \"\")\n if extra_names.strip():\n configured_secret_names.update(\n item.strip().upper()\n for item in extra_names.split(\",\")\n if item.strip()\n )\n return sorted(configured_secret_names)\n\n\ndef _build_model_client(args):\n provider = getattr(args, \"provider\", \"deepseek\")\n\n\n if provider == \"openai\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_OPENAI_API_BASE\", (\"OPENAI_API_BASE\",), DEFAULT_OPENAI_BASE_URL)\n api_key = provider_env(\n \"PICO_OPENAI_API_KEY\",\n (\"OPENAI_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_ANTHROPIC_API_KEY\", \"ANTHROPIC_API_KEY\"),\n )\n return OpenAICompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"anthropic\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_ANTHROPIC_API_BASE\", (\"ANTHROPIC_API_BASE\",), DEFAULT_ANTHROPIC_BASE_URL)\n api_key = provider_env(\n \"PICO_ANTHROPIC_API_KEY\",\n (\"ANTHROPIC_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_OPENAI_API_KEY\", \"OPENAI_API_KEY\"),\n )\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"deepseek\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_DEEPSEEK_API_BASE\", (\"DEEPSEEK_API_BASE\",), DEFAULT_DEEPSEEK_BASE_URL)\n api_key = provider_env(\"PICO_DEEPSEEK_API_KEY\", (\"DEEPSEEK_API_KEY\",))\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n\n model = _effective_model(args, provider)\n host = getattr(args, \"host\", DEFAULT_OLLAMA_HOST)\n return OllamaModelClient(\n model=model,\n host=host,\n temperature=args.temperature,\n top_p=args.top_p,\n timeout=args.ollama_timeout,\n )\n\n\ndef build_welcome(agent, model, host):\n width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))\n inner = width - 4\n gap = 3\n\n def row(text):\n body = _terminal_middle(text, width - 4)\n return _border_row(_terminal_ljust(body, width - 4))\n\n def divider(style=\"solid\"):\n char = \"═\" if style == \"strong\" else \"─\"\n left = \"╔\" if style == \"strong\" else \"├\"\n right = \"╗\" if style == \"strong\" else \"┤\"\n return _color(left + char * (width - 2) + right, WELCOME_BORDER_COLOR)\n\n def bottom_divider():\n return _color(\"╚\" + \"═\" * (width - 2) + \"╝\", WELCOME_BORDER_COLOR)\n\n def _border_row(body):\n return f\"{_color('║', WELCOME_BORDER_COLOR)} {body} {_color('║', WELCOME_BORDER_COLOR)}\"\n\n def center(text):\n body = _terminal_middle(text, inner)\n return _border_row(_terminal_center(body, inner))\n\n def split_row(left_text, right_text, left_size, right_size):\n left_body = _terminal_ljust(str(left_text), left_size)\n right_body = _terminal_ljust(str(right_text), right_size)\n return _border_row(f\"{left_body}{' ' * gap}{right_body}\")\n\n top_gap = gap\n art_width = max(_terminal_width(_gradient_text(text)) for text in WELCOME_ART)\n top_left_width = min(max(art_width, 24), max(24, inner - 34))\n top_right_width = max(28, inner - top_gap - top_left_width)\n label_width = 11\n value_width = max(12, top_right_width - label_width)\n right_block = [\n f\"{'WORKSPACE':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.cwd, value_width), value_width).lstrip()}\",\n f\"{'MODEL':<{label_width}}{_terminal_ljust(_terminal_middle(model, value_width), value_width).lstrip()}\",\n f\"{'APPROVAL':<{label_width}}{_terminal_ljust(_terminal_middle(agent.approval_policy, value_width), value_width).lstrip()}\",\n f\"{'BRANCH':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.branch, value_width), value_width).lstrip()}\",\n f\"{'SESSION':<{label_width}}{_terminal_ljust(_terminal_middle(agent.session['id'], value_width), value_width).lstrip()}\",\n ]\n top_height = max(len(WELCOME_ART), len(right_block))\n left_block = list(WELCOME_ART) + [\"\"] * max(0, top_height - len(WELCOME_ART))\n right_padding_top = max(0, (top_height - len(right_block)) // 2)\n right_block = [\"\"] * right_padding_top + right_block\n right_block.extend([\"\"] * max(0, top_height - len(right_block)))\n\n line = divider(\"strong\")\n rows = [\n split_row(\n _terminal_ljust(_gradient_text(left_block[index]) if left_block[index] else \"\", top_left_width),\n _terminal_ljust(right_block[index], top_right_width) if right_block[index] else \" \" * top_right_width,\n top_left_width,\n top_right_width,\n )\n for index in range(top_height)\n ]\n rows.extend(\n [\n divider(),\n center(WELCOME_NAME),\n center(WELCOME_SUBTITLE),\n ]\n )\n return \"\\n\".join([line, *rows, bottom_divider()])\n\n\ndef build_agent(args):\n \"\"\"根据 CLI 参数装配出一个可运行的 Pico 实例。\n\n 为什么存在:\n 命令行参数只是字符串和开关,runtime 需要的是已经装配好的对象图:\n model client、workspace snapshot、session store、secret 配置等。\n 这个函数负责把“启动参数”翻译成“agent 运行现场”。\n\n 输入 / 输出:\n - 输入:`argparse` 解析后的 `args`\n - 输出:一个新的 `Pico`,或一个从旧 session 恢复出来的 `Pico`\n\n 在 agent 链路里的位置:\n 它是整个程序启动链路里最靠近 runtime 的装配点。`main()` 先调它,\n 得到 agent 后,后面无论是 one-shot 还是 REPL 模式,都会落到 `ask()`。\n \"\"\"\n\n\n workspace = WorkspaceContext.build(args.cwd)\n\n load_project_env(workspace.repo_root)\n\n\n configured_secret_names = _configured_secret_names(args)\n store = SessionStore(Path(workspace.repo_root) / AGENT_STATE_DIR / \"sessions\")\n\n model = _build_model_client(args)\n session_id = args.resume\n if session_id == \"latest\":\n session_id = store.latest()\n if session_id:\n return Pico.from_session(\n model_client=model,\n workspace=workspace,\n session_store=store,\n session_id=session_id,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n feature_flags={\"request_rewrite\": True},\n tool_call_reporter=_stderr_tool_call_reporter,\n assistant_message_reporter=_stderr_assistant_message_reporter,\n )\n return Pico(\n model_client=model,\n workspace=workspace,\n session_store=store,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n feature_flags={\"request_rewrite\": True},\n tool_call_reporter=_stderr_tool_call_reporter,\n assistant_message_reporter=_stderr_assistant_message_reporter,\n )\n\n\ndef _maybe_evolve_durable_memory(agent, reason):\n result = agent.evolve_durable_memory(reason=reason)\n if result.get(\"status\") == \"failed\":\n print(f\"durable memory evolution failed: {result.get('error', '')}\", file=sys.stderr)\n return result\n\n\ndef build_arg_parser():\n parser = argparse.ArgumentParser(\n formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n description=\"Minimal coding agent for DeepSeek, OpenAI-compatible, Anthropic-compatible, or Ollama models.\",\n )\n parser.add_argument(\"prompt\", nargs=\"*\", help=\"Optional one-shot prompt.\")\n parser.add_argument(\"--cwd\", default=\".\", help=\"Workspace directory.\")\n parser.add_argument(\"--provider\", choices=(\"ollama\", \"openai\", \"anthropic\", \"deepseek\"), default=\"openai\", help=\"Model backend to use.\")\n parser.add_argument(\n \"--model\",\n default=None,\n help=\"Model name override. Defaults to qwen3.5:4b for Ollama, PICO_OPENAI_MODEL for openai, PICO_ANTHROPIC_MODEL for anthropic, and PICO_DEEPSEEK_MODEL for deepseek when set.\",\n )\n parser.add_argument(\"--host\", default=DEFAULT_OLLAMA_HOST, help=\"Ollama server URL.\")\n parser.add_argument(\"--base-url\", default=None, help=\"Provider API base URL for deepseek, openai, or anthropic.\")\n parser.add_argument(\"--ollama-timeout\", type=int, default=300, help=\"Ollama request timeout in seconds.\")\n parser.add_argument(\"--openai-timeout\", type=int, default=300, help=\"OpenAI-compatible request timeout in seconds.\")\n parser.add_argument(\"--resume\", default=None, help=\"Session id to resume or 'latest'.\")\n parser.add_argument(\"--approval\", choices=(\"ask\", \"auto\", \"never\"), default=\"ask\", help=\"Approval policy for risky tools.\")\n parser.add_argument(\n \"--secret-env-name\",\n dest=\"secret_env_names\",\n action=\"append\",\n default=[],\n help=\"Extra environment variable names to treat as secrets for trace/report redaction.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=12, help=\"Maximum tool/model iterations per request.\")\n parser.add_argument(\"--max-new-tokens\", type=int, default=8192, help=\"Maximum model output tokens per step.\")\n parser.add_argument(\"--no-memory-evolution\", action=\"store_true\", help=\"Disable session-end durable memory evolution.\")\n parser.add_argument(\"--temperature\", type=float, default=0.2, help=\"Sampling temperature sent to Ollama.\")\n parser.add_argument(\"--top-p\", type=float, default=0.9, help=\"Top-p sampling value sent to Ollama.\")\n return parser\n\n\ndef main(argv=None):\n args = build_arg_parser().parse_args(argv)\n agent = build_agent(args)\n if args.resume and not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"resume_activation\")\n\n model = getattr(agent.model_client, \"model\", getattr(args, \"model\", DEFAULT_OLLAMA_MODEL))\n\n host = getattr(agent.model_client, \"host\", getattr(agent.model_client, \"base_url\", getattr(args, \"host\", DEFAULT_OLLAMA_HOST)))\n print(build_welcome(agent, model=model, host=host))\n\n if args.prompt:\n\n prompt = \" \".join(args.prompt).strip()\n if prompt:\n print()\n try:\n print(agent.ask(prompt))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n return 1\n finally:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n while True:\n\n\n try:\n user_input = input(\"\\nLUMO> \").strip()\n except (EOFError, KeyboardInterrupt):\n print(\"\")\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n if not user_input:\n continue\n if user_input in {\"/exit\", \"/quit\"}:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n if user_input == \"/help\":\n print(HELP_DETAILS)\n continue\n if user_input == \"/memory\":\n print(agent.memory_text())\n continue\n if user_input == \"/session\":\n print(agent.session_path)\n continue\n if user_input == \"/reset\":\n agent.reset()\n print(\"session reset\")\n continue\n\n print()\n try:\n print(agent.ask(user_input))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n"}, "813a5e216126d253170e657cccfe1afdadfe4832": {"t": 1785767247.7592192, "n": 17599, "h": "0e539ec9e2248436", "c": "\"\"\"命令行入口。\n\n这个模块负责把“用户怎么启动 Lumo”翻译成 runtime 能理解的对象:\n解析参数、挑模型后端、构建工作区快照、恢复或新建 session,\n最后进入 one-shot 或交互式循环。\n\"\"\"\n\nimport argparse\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nimport unicodedata\nfrom pathlib import Path\n\nfrom .config import load_project_env, provider_env\nfrom .providers.clients import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient\nfrom .runtime import Pico, SessionStore\nfrom .workspace import AGENT_STATE_DIR, WorkspaceContext\n\nDEFAULT_SECRET_ENV_NAMES = (\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENAI_API_TOKEN\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"PICO_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"GITHUB_PAT\",\n \"GH_PAT\",\n)\n\nWELCOME_ART = tuple(\n \" \".join(parts)\n for parts in (\n (\"██ \", \"██ ██\", \"██ ██\", \"███████\"),\n (\"██ \", \"██ ██\", \"███ ███\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"███████\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ █ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"███████\", \"███████\", \"██ ██\", \"███████\"),\n )\n)\nWELCOME_ART = (\n \"██ ██ ██ ███ ███ ███████\",\n \"██ ██ ██ ████ ████ ██ ██\",\n \"██ ██ ██ ██ ███ ██ ██ ██\",\n \"██ ██ ██ ██ █ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"████████ ███████ ██ ██ ███████\",\n)\nWELCOME_NAME = \"Lumo\"\nWELCOME_SUBTITLE = \"Code is cheap, Show me your talk\"\n\nHELP_DETAILS = textwrap.dedent(\n \"\"\"\\\n Commands:\n /help Show this help message.\n /memory Show durable memory loaded from .lumo/memory.\n /session Show the path to the saved session file.\n /reset Clear the current session history and memory.\n /exit Exit the agent.\n \"\"\"\n).strip()\n\n\nDEFAULT_OLLAMA_MODEL = \"qwen3.5:4b\"\nDEFAULT_OLLAMA_HOST = \"http://127.0.0.1:11434\"\nDEFAULT_OPENAI_MODEL = \"gpt-5.4\"\nDEFAULT_OPENAI_BASE_URL = \"https://www.right.codes/codex/v1\"\nDEFAULT_ANTHROPIC_MODEL = \"claude-sonnet-4-6\"\nDEFAULT_ANTHROPIC_BASE_URL = \"https://www.right.codes/claude/v1\"\nDEFAULT_DEEPSEEK_MODEL = \"deepseek-v4-pro\"\nDEFAULT_DEEPSEEK_BASE_URL = \"https://api.deepseek.com/anthropic\"\nSECRET_ENV_NAMES_VAR = \"PICO_SECRET_ENV_NAMES\"\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-9;]*m\")\nANSI_RESET = \"\\x1b[0m\"\nWELCOME_BORDER_COLOR = \"\\x1b[38;2;125;211;252m\"\nWELCOME_GRADIENT_START = (255, 255, 255)\nWELCOME_GRADIENT_END = (125, 211, 252)\n\n\ndef _strip_ansi(text):\n return ANSI_PATTERN.sub(\"\", str(text))\n\n\ndef _ansi_rgb(red, green, blue):\n return f\"\\x1b[38;2;{red};{green};{blue}m\"\n\n\ndef _color(text, ansi_color):\n return f\"{ansi_color}{text}{ANSI_RESET}\"\n\n\ndef _stderr_tool_call_reporter(name, summary):\n print(f\"[tool] {name} {summary}\", file=sys.stderr)\n\n\ndef _gradient_text(text, start_rgb=WELCOME_GRADIENT_START, end_rgb=WELCOME_GRADIENT_END):\n text = str(text)\n span = max(1, len(text) - 1)\n parts = []\n for index, char in enumerate(text):\n if char == \" \":\n parts.append(char)\n continue\n ratio = index / span\n rgb = tuple(round(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb))\n parts.append(_color(char, _ansi_rgb(*rgb)))\n return \"\".join(parts)\n\n\ndef _terminal_char_width(char):\n return 2 if unicodedata.east_asian_width(char) in (\"F\", \"W\") else 1\n\n\ndef _terminal_width(text):\n return sum(_terminal_char_width(char) for char in _strip_ansi(text))\n\n\ndef _terminal_ljust(text, width):\n text = str(text)\n return text + \" \" * max(0, width - _terminal_width(text))\n\n\ndef _terminal_center(text, width):\n text = str(text)\n padding = max(0, width - _terminal_width(text))\n left = padding // 2\n right = padding - left\n return \" \" * left + text + \" \" * right\n\n\ndef _take_terminal_width(text, width, from_end=False):\n chars = reversed(str(text)) if from_end else str(text)\n selected = []\n used = 0\n for char in chars:\n char_width = _terminal_char_width(char)\n if used + char_width > width:\n break\n selected.append(char)\n used += char_width\n if from_end:\n selected.reverse()\n return \"\".join(selected)\n\n\ndef _terminal_middle(text, limit):\n text = str(text).replace(\"\\n\", \" \")\n if _terminal_width(text) <= limit:\n return text\n if limit <= 3:\n return _take_terminal_width(text, limit)\n left = (limit - 3) // 2\n right = limit - 3 - left\n return _take_terminal_width(text, left) + \"...\" + _take_terminal_width(text, right, from_end=True)\n\n\ndef _effective_model(args, provider):\n\n\n\n\n explicit_model = getattr(args, \"model\", None)\n if explicit_model:\n return explicit_model\n if provider == \"openai\":\n model = provider_env(\"PICO_OPENAI_MODEL\", (\"OPENAI_MODEL\",))\n if model:\n return model\n return DEFAULT_OPENAI_MODEL\n if provider == \"anthropic\":\n model = provider_env(\"PICO_ANTHROPIC_MODEL\", (\"ANTHROPIC_MODEL\",))\n if model:\n return model\n return DEFAULT_ANTHROPIC_MODEL\n if provider == \"deepseek\":\n model = provider_env(\"PICO_DEEPSEEK_MODEL\", (\"DEEPSEEK_MODEL\",))\n if model:\n return model\n return DEFAULT_DEEPSEEK_MODEL\n return DEFAULT_OLLAMA_MODEL\n\n\ndef _configured_secret_names(args):\n configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)\n configured_secret_names.update(str(name).upper() for name in args.secret_env_names)\n extra_names = os.environ.get(SECRET_ENV_NAMES_VAR, \"\")\n if extra_names.strip():\n configured_secret_names.update(\n item.strip().upper()\n for item in extra_names.split(\",\")\n if item.strip()\n )\n return sorted(configured_secret_names)\n\n\ndef _build_model_client(args):\n provider = getattr(args, \"provider\", \"deepseek\")\n\n\n if provider == \"openai\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_OPENAI_API_BASE\", (\"OPENAI_API_BASE\",), DEFAULT_OPENAI_BASE_URL)\n api_key = provider_env(\n \"PICO_OPENAI_API_KEY\",\n (\"OPENAI_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_ANTHROPIC_API_KEY\", \"ANTHROPIC_API_KEY\"),\n )\n return OpenAICompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"anthropic\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_ANTHROPIC_API_BASE\", (\"ANTHROPIC_API_BASE\",), DEFAULT_ANTHROPIC_BASE_URL)\n api_key = provider_env(\n \"PICO_ANTHROPIC_API_KEY\",\n (\"ANTHROPIC_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_OPENAI_API_KEY\", \"OPENAI_API_KEY\"),\n )\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"deepseek\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_DEEPSEEK_API_BASE\", (\"DEEPSEEK_API_BASE\",), DEFAULT_DEEPSEEK_BASE_URL)\n api_key = provider_env(\"PICO_DEEPSEEK_API_KEY\", (\"DEEPSEEK_API_KEY\",))\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n\n model = _effective_model(args, provider)\n host = getattr(args, \"host\", DEFAULT_OLLAMA_HOST)\n return OllamaModelClient(\n model=model,\n host=host,\n temperature=args.temperature,\n top_p=args.top_p,\n timeout=args.ollama_timeout,\n )\n\n\ndef build_welcome(agent, model, host):\n width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))\n inner = width - 4\n gap = 3\n\n def row(text):\n body = _terminal_middle(text, width - 4)\n return _border_row(_terminal_ljust(body, width - 4))\n\n def divider(style=\"solid\"):\n char = \"═\" if style == \"strong\" else \"─\"\n left = \"╔\" if style == \"strong\" else \"├\"\n right = \"╗\" if style == \"strong\" else \"┤\"\n return _color(left + char * (width - 2) + right, WELCOME_BORDER_COLOR)\n\n def bottom_divider():\n return _color(\"╚\" + \"═\" * (width - 2) + \"╝\", WELCOME_BORDER_COLOR)\n\n def _border_row(body):\n return f\"{_color('║', WELCOME_BORDER_COLOR)} {body} {_color('║', WELCOME_BORDER_COLOR)}\"\n\n def center(text):\n body = _terminal_middle(text, inner)\n return _border_row(_terminal_center(body, inner))\n\n def split_row(left_text, right_text, left_size, right_size):\n left_body = _terminal_ljust(str(left_text), left_size)\n right_body = _terminal_ljust(str(right_text), right_size)\n return _border_row(f\"{left_body}{' ' * gap}{right_body}\")\n\n top_gap = gap\n art_width = max(_terminal_width(_gradient_text(text)) for text in WELCOME_ART)\n top_left_width = min(max(art_width, 24), max(24, inner - 34))\n top_right_width = max(28, inner - top_gap - top_left_width)\n label_width = 11\n value_width = max(12, top_right_width - label_width)\n right_block = [\n f\"{'WORKSPACE':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.cwd, value_width), value_width).lstrip()}\",\n f\"{'MODEL':<{label_width}}{_terminal_ljust(_terminal_middle(model, value_width), value_width).lstrip()}\",\n f\"{'APPROVAL':<{label_width}}{_terminal_ljust(_terminal_middle(agent.approval_policy, value_width), value_width).lstrip()}\",\n f\"{'BRANCH':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.branch, value_width), value_width).lstrip()}\",\n f\"{'SESSION':<{label_width}}{_terminal_ljust(_terminal_middle(agent.session['id'], value_width), value_width).lstrip()}\",\n ]\n top_height = max(len(WELCOME_ART), len(right_block))\n left_block = list(WELCOME_ART) + [\"\"] * max(0, top_height - len(WELCOME_ART))\n right_padding_top = max(0, (top_height - len(right_block)) // 2)\n right_block = [\"\"] * right_padding_top + right_block\n right_block.extend([\"\"] * max(0, top_height - len(right_block)))\n\n line = divider(\"strong\")\n rows = [\n split_row(\n _terminal_ljust(_gradient_text(left_block[index]) if left_block[index] else \"\", top_left_width),\n _terminal_ljust(right_block[index], top_right_width) if right_block[index] else \" \" * top_right_width,\n top_left_width,\n top_right_width,\n )\n for index in range(top_height)\n ]\n rows.extend(\n [\n divider(),\n center(WELCOME_NAME),\n center(WELCOME_SUBTITLE),\n ]\n )\n return \"\\n\".join([line, *rows, bottom_divider()])\n\n\ndef build_agent(args):\n \"\"\"根据 CLI 参数装配出一个可运行的 Pico 实例。\n\n 为什么存在:\n 命令行参数只是字符串和开关,runtime 需要的是已经装配好的对象图:\n model client、workspace snapshot、session store、secret 配置等。\n 这个函数负责把“启动参数”翻译成“agent 运行现场”。\n\n 输入 / 输出:\n - 输入:`argparse` 解析后的 `args`\n - 输出:一个新的 `Pico`,或一个从旧 session 恢复出来的 `Pico`\n\n 在 agent 链路里的位置:\n 它是整个程序启动链路里最靠近 runtime 的装配点。`main()` 先调它,\n 得到 agent 后,后面无论是 one-shot 还是 REPL 模式,都会落到 `ask()`。\n \"\"\"\n\n\n workspace = WorkspaceContext.build(args.cwd)\n\n load_project_env(workspace.repo_root)\n\n\n configured_secret_names = _configured_secret_names(args)\n store = SessionStore(Path(workspace.repo_root) / AGENT_STATE_DIR / \"sessions\")\n\n model = _build_model_client(args)\n session_id = args.resume\n if session_id == \"latest\":\n session_id = store.latest()\n if session_id:\n return Pico.from_session(\n model_client=model,\n workspace=workspace,\n session_store=store,\n session_id=session_id,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n feature_flags={\"request_rewrite\": True},\n tool_call_reporter=_stderr_tool_call_reporter,\n )\n return Pico(\n model_client=model,\n workspace=workspace,\n session_store=store,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n feature_flags={\"request_rewrite\": True},\n tool_call_reporter=_stderr_tool_call_reporter,\n )\n\n\ndef _maybe_evolve_durable_memory(agent, reason):\n result = agent.evolve_durable_memory(reason=reason)\n if result.get(\"status\") == \"failed\":\n print(f\"durable memory evolution failed: {result.get('error', '')}\", file=sys.stderr)\n return result\n\n\ndef build_arg_parser():\n parser = argparse.ArgumentParser(\n formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n description=\"Minimal coding agent for DeepSeek, OpenAI-compatible, Anthropic-compatible, or Ollama models.\",\n )\n parser.add_argument(\"prompt\", nargs=\"*\", help=\"Optional one-shot prompt.\")\n parser.add_argument(\"--cwd\", default=\".\", help=\"Workspace directory.\")\n parser.add_argument(\"--provider\", choices=(\"ollama\", \"openai\", \"anthropic\", \"deepseek\"), default=\"openai\", help=\"Model backend to use.\")\n parser.add_argument(\n \"--model\",\n default=None,\n help=\"Model name override. Defaults to qwen3.5:4b for Ollama, PICO_OPENAI_MODEL for openai, PICO_ANTHROPIC_MODEL for anthropic, and PICO_DEEPSEEK_MODEL for deepseek when set.\",\n )\n parser.add_argument(\"--host\", default=DEFAULT_OLLAMA_HOST, help=\"Ollama server URL.\")\n parser.add_argument(\"--base-url\", default=None, help=\"Provider API base URL for deepseek, openai, or anthropic.\")\n parser.add_argument(\"--ollama-timeout\", type=int, default=300, help=\"Ollama request timeout in seconds.\")\n parser.add_argument(\"--openai-timeout\", type=int, default=300, help=\"OpenAI-compatible request timeout in seconds.\")\n parser.add_argument(\"--resume\", default=None, help=\"Session id to resume or 'latest'.\")\n parser.add_argument(\"--approval\", choices=(\"ask\", \"auto\", \"never\"), default=\"ask\", help=\"Approval policy for risky tools.\")\n parser.add_argument(\n \"--secret-env-name\",\n dest=\"secret_env_names\",\n action=\"append\",\n default=[],\n help=\"Extra environment variable names to treat as secrets for trace/report redaction.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=6, help=\"Maximum tool/model iterations per request.\")\n parser.add_argument(\"--max-new-tokens\", type=int, default=8192, help=\"Maximum model output tokens per step.\")\n parser.add_argument(\"--no-memory-evolution\", action=\"store_true\", help=\"Disable session-end durable memory evolution.\")\n parser.add_argument(\"--temperature\", type=float, default=0.2, help=\"Sampling temperature sent to Ollama.\")\n parser.add_argument(\"--top-p\", type=float, default=0.9, help=\"Top-p sampling value sent to Ollama.\")\n return parser\n\n\ndef main(argv=None):\n args = build_arg_parser().parse_args(argv)\n agent = build_agent(args)\n if args.resume and not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"resume_activation\")\n\n model = getattr(agent.model_client, \"model\", getattr(args, \"model\", DEFAULT_OLLAMA_MODEL))\n\n host = getattr(agent.model_client, \"host\", getattr(agent.model_client, \"base_url\", getattr(args, \"host\", DEFAULT_OLLAMA_HOST)))\n print(build_welcome(agent, model=model, host=host))\n\n if args.prompt:\n\n prompt = \" \".join(args.prompt).strip()\n if prompt:\n print()\n try:\n print(agent.ask(prompt))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n return 1\n finally:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n while True:\n\n\n try:\n user_input = input(\"\\nLUMO> \").strip()\n except (EOFError, KeyboardInterrupt):\n print(\"\")\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n if not user_input:\n continue\n if user_input in {\"/exit\", \"/quit\"}:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n if user_input == \"/help\":\n print(HELP_DETAILS)\n continue\n if user_input == \"/memory\":\n print(agent.memory_text())\n continue\n if user_input == \"/session\":\n print(agent.session_path)\n continue\n if user_input == \"/reset\":\n agent.reset()\n print(\"session reset\")\n continue\n\n print()\n try:\n print(agent.ask(user_input))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n"}, "b2c7cb68b73ef085296cc39ab33c0d3abf02ba8e": {"t": 1785767249.7225251, "n": 17497, "h": "5b1e789607c4a190", "c": "\"\"\"命令行入口。\n\n这个模块负责把“用户怎么启动 Lumo”翻译成 runtime 能理解的对象:\n解析参数、挑模型后端、构建工作区快照、恢复或新建 session,\n最后进入 one-shot 或交互式循环。\n\"\"\"\n\nimport argparse\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nimport unicodedata\nfrom pathlib import Path\n\nfrom .config import load_project_env, provider_env\nfrom .providers.clients import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient\nfrom .runtime import Pico, SessionStore\nfrom .workspace import AGENT_STATE_DIR, WorkspaceContext\n\nDEFAULT_SECRET_ENV_NAMES = (\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENAI_API_TOKEN\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"PICO_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"GITHUB_PAT\",\n \"GH_PAT\",\n)\n\nWELCOME_ART = tuple(\n \" \".join(parts)\n for parts in (\n (\"██ \", \"██ ██\", \"██ ██\", \"███████\"),\n (\"██ \", \"██ ██\", \"███ ███\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"███████\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ █ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"███████\", \"███████\", \"██ ██\", \"███████\"),\n )\n)\nWELCOME_ART = (\n \"██ ██ ██ ███ ███ ███████\",\n \"██ ██ ██ ████ ████ ██ ██\",\n \"██ ██ ██ ██ ███ ██ ██ ██\",\n \"██ ██ ██ ██ █ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"██ ██ ██ ██ ██ ██ ██\",\n \"████████ ███████ ██ ██ ███████\",\n)\nWELCOME_NAME = \"Lumo\"\nWELCOME_SUBTITLE = \"Code is cheap, Show me your talk\"\n\nHELP_DETAILS = textwrap.dedent(\n \"\"\"\\\n Commands:\n /help Show this help message.\n /memory Show durable memory loaded from .lumo/memory.\n /session Show the path to the saved session file.\n /reset Clear the current session history and memory.\n /exit Exit the agent.\n \"\"\"\n).strip()\n\n\nDEFAULT_OLLAMA_MODEL = \"qwen3.5:4b\"\nDEFAULT_OLLAMA_HOST = \"http://127.0.0.1:11434\"\nDEFAULT_OPENAI_MODEL = \"gpt-5.4\"\nDEFAULT_OPENAI_BASE_URL = \"https://www.right.codes/codex/v1\"\nDEFAULT_ANTHROPIC_MODEL = \"claude-sonnet-4-6\"\nDEFAULT_ANTHROPIC_BASE_URL = \"https://www.right.codes/claude/v1\"\nDEFAULT_DEEPSEEK_MODEL = \"deepseek-v4-pro\"\nDEFAULT_DEEPSEEK_BASE_URL = \"https://api.deepseek.com/anthropic\"\nSECRET_ENV_NAMES_VAR = \"PICO_SECRET_ENV_NAMES\"\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-9;]*m\")\nANSI_RESET = \"\\x1b[0m\"\nWELCOME_BORDER_COLOR = \"\\x1b[38;2;125;211;252m\"\nWELCOME_GRADIENT_START = (255, 255, 255)\nWELCOME_GRADIENT_END = (125, 211, 252)\n\n\ndef _strip_ansi(text):\n return ANSI_PATTERN.sub(\"\", str(text))\n\n\ndef _ansi_rgb(red, green, blue):\n return f\"\\x1b[38;2;{red};{green};{blue}m\"\n\n\ndef _color(text, ansi_color):\n return f\"{ansi_color}{text}{ANSI_RESET}\"\n\n\ndef _stderr_tool_call_reporter(name, summary):\n print(f\"[tool] {name} {summary}\", file=sys.stderr)\n\n\ndef _gradient_text(text, start_rgb=WELCOME_GRADIENT_START, end_rgb=WELCOME_GRADIENT_END):\n text = str(text)\n span = max(1, len(text) - 1)\n parts = []\n for index, char in enumerate(text):\n if char == \" \":\n parts.append(char)\n continue\n ratio = index / span\n rgb = tuple(round(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb))\n parts.append(_color(char, _ansi_rgb(*rgb)))\n return \"\".join(parts)\n\n\ndef _terminal_char_width(char):\n return 2 if unicodedata.east_asian_width(char) in (\"F\", \"W\") else 1\n\n\ndef _terminal_width(text):\n return sum(_terminal_char_width(char) for char in _strip_ansi(text))\n\n\ndef _terminal_ljust(text, width):\n text = str(text)\n return text + \" \" * max(0, width - _terminal_width(text))\n\n\ndef _terminal_center(text, width):\n text = str(text)\n padding = max(0, width - _terminal_width(text))\n left = padding // 2\n right = padding - left\n return \" \" * left + text + \" \" * right\n\n\ndef _take_terminal_width(text, width, from_end=False):\n chars = reversed(str(text)) if from_end else str(text)\n selected = []\n used = 0\n for char in chars:\n char_width = _terminal_char_width(char)\n if used + char_width > width:\n break\n selected.append(char)\n used += char_width\n if from_end:\n selected.reverse()\n return \"\".join(selected)\n\n\ndef _terminal_middle(text, limit):\n text = str(text).replace(\"\\n\", \" \")\n if _terminal_width(text) <= limit:\n return text\n if limit <= 3:\n return _take_terminal_width(text, limit)\n left = (limit - 3) // 2\n right = limit - 3 - left\n return _take_terminal_width(text, left) + \"...\" + _take_terminal_width(text, right, from_end=True)\n\n\ndef _effective_model(args, provider):\n\n\n\n\n explicit_model = getattr(args, \"model\", None)\n if explicit_model:\n return explicit_model\n if provider == \"openai\":\n model = provider_env(\"PICO_OPENAI_MODEL\", (\"OPENAI_MODEL\",))\n if model:\n return model\n return DEFAULT_OPENAI_MODEL\n if provider == \"anthropic\":\n model = provider_env(\"PICO_ANTHROPIC_MODEL\", (\"ANTHROPIC_MODEL\",))\n if model:\n return model\n return DEFAULT_ANTHROPIC_MODEL\n if provider == \"deepseek\":\n model = provider_env(\"PICO_DEEPSEEK_MODEL\", (\"DEEPSEEK_MODEL\",))\n if model:\n return model\n return DEFAULT_DEEPSEEK_MODEL\n return DEFAULT_OLLAMA_MODEL\n\n\ndef _configured_secret_names(args):\n configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)\n configured_secret_names.update(str(name).upper() for name in args.secret_env_names)\n extra_names = os.environ.get(SECRET_ENV_NAMES_VAR, \"\")\n if extra_names.strip():\n configured_secret_names.update(\n item.strip().upper()\n for item in extra_names.split(\",\")\n if item.strip()\n )\n return sorted(configured_secret_names)\n\n\ndef _build_model_client(args):\n provider = getattr(args, \"provider\", \"deepseek\")\n\n\n if provider == \"openai\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_OPENAI_API_BASE\", (\"OPENAI_API_BASE\",), DEFAULT_OPENAI_BASE_URL)\n api_key = provider_env(\n \"PICO_OPENAI_API_KEY\",\n (\"OPENAI_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_ANTHROPIC_API_KEY\", \"ANTHROPIC_API_KEY\"),\n )\n return OpenAICompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"anthropic\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_ANTHROPIC_API_BASE\", (\"ANTHROPIC_API_BASE\",), DEFAULT_ANTHROPIC_BASE_URL)\n api_key = provider_env(\n \"PICO_ANTHROPIC_API_KEY\",\n (\"ANTHROPIC_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_OPENAI_API_KEY\", \"OPENAI_API_KEY\"),\n )\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"deepseek\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_DEEPSEEK_API_BASE\", (\"DEEPSEEK_API_BASE\",), DEFAULT_DEEPSEEK_BASE_URL)\n api_key = provider_env(\"PICO_DEEPSEEK_API_KEY\", (\"DEEPSEEK_API_KEY\",))\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n\n model = _effective_model(args, provider)\n host = getattr(args, \"host\", DEFAULT_OLLAMA_HOST)\n return OllamaModelClient(\n model=model,\n host=host,\n temperature=args.temperature,\n top_p=args.top_p,\n timeout=args.ollama_timeout,\n )\n\n\ndef build_welcome(agent, model, host):\n width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))\n inner = width - 4\n gap = 3\n\n def row(text):\n body = _terminal_middle(text, width - 4)\n return _border_row(_terminal_ljust(body, width - 4))\n\n def divider(style=\"solid\"):\n char = \"═\" if style == \"strong\" else \"─\"\n left = \"╔\" if style == \"strong\" else \"├\"\n right = \"╗\" if style == \"strong\" else \"┤\"\n return _color(left + char * (width - 2) + right, WELCOME_BORDER_COLOR)\n\n def bottom_divider():\n return _color(\"╚\" + \"═\" * (width - 2) + \"╝\", WELCOME_BORDER_COLOR)\n\n def _border_row(body):\n return f\"{_color('║', WELCOME_BORDER_COLOR)} {body} {_color('║', WELCOME_BORDER_COLOR)}\"\n\n def center(text):\n body = _terminal_middle(text, inner)\n return _border_row(_terminal_center(body, inner))\n\n def split_row(left_text, right_text, left_size, right_size):\n left_body = _terminal_ljust(str(left_text), left_size)\n right_body = _terminal_ljust(str(right_text), right_size)\n return _border_row(f\"{left_body}{' ' * gap}{right_body}\")\n\n top_gap = gap\n art_width = max(_terminal_width(_gradient_text(text)) for text in WELCOME_ART)\n top_left_width = min(max(art_width, 24), max(24, inner - 34))\n top_right_width = max(28, inner - top_gap - top_left_width)\n label_width = 11\n value_width = max(12, top_right_width - label_width)\n right_block = [\n f\"{'WORKSPACE':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.cwd, value_width), value_width).lstrip()}\",\n f\"{'MODEL':<{label_width}}{_terminal_ljust(_terminal_middle(model, value_width), value_width).lstrip()}\",\n f\"{'APPROVAL':<{label_width}}{_terminal_ljust(_terminal_middle(agent.approval_policy, value_width), value_width).lstrip()}\",\n f\"{'BRANCH':<{label_width}}{_terminal_ljust(_terminal_middle(agent.workspace.branch, value_width), value_width).lstrip()}\",\n f\"{'SESSION':<{label_width}}{_terminal_ljust(_terminal_middle(agent.session['id'], value_width), value_width).lstrip()}\",\n ]\n top_height = max(len(WELCOME_ART), len(right_block))\n left_block = list(WELCOME_ART) + [\"\"] * max(0, top_height - len(WELCOME_ART))\n right_padding_top = max(0, (top_height - len(right_block)) // 2)\n right_block = [\"\"] * right_padding_top + right_block\n right_block.extend([\"\"] * max(0, top_height - len(right_block)))\n\n line = divider(\"strong\")\n rows = [\n split_row(\n _terminal_ljust(_gradient_text(left_block[index]) if left_block[index] else \"\", top_left_width),\n _terminal_ljust(right_block[index], top_right_width) if right_block[index] else \" \" * top_right_width,\n top_left_width,\n top_right_width,\n )\n for index in range(top_height)\n ]\n rows.extend(\n [\n divider(),\n center(WELCOME_NAME),\n center(WELCOME_SUBTITLE),\n ]\n )\n return \"\\n\".join([line, *rows, bottom_divider()])\n\n\ndef build_agent(args):\n \"\"\"根据 CLI 参数装配出一个可运行的 Pico 实例。\n\n 为什么存在:\n 命令行参数只是字符串和开关,runtime 需要的是已经装配好的对象图:\n model client、workspace snapshot、session store、secret 配置等。\n 这个函数负责把“启动参数”翻译成“agent 运行现场”。\n\n 输入 / 输出:\n - 输入:`argparse` 解析后的 `args`\n - 输出:一个新的 `Pico`,或一个从旧 session 恢复出来的 `Pico`\n\n 在 agent 链路里的位置:\n 它是整个程序启动链路里最靠近 runtime 的装配点。`main()` 先调它,\n 得到 agent 后,后面无论是 one-shot 还是 REPL 模式,都会落到 `ask()`。\n \"\"\"\n\n\n workspace = WorkspaceContext.build(args.cwd)\n\n load_project_env(workspace.repo_root)\n\n\n configured_secret_names = _configured_secret_names(args)\n store = SessionStore(Path(workspace.repo_root) / AGENT_STATE_DIR / \"sessions\")\n\n model = _build_model_client(args)\n session_id = args.resume\n if session_id == \"latest\":\n session_id = store.latest()\n if session_id:\n return Pico.from_session(\n model_client=model,\n workspace=workspace,\n session_store=store,\n session_id=session_id,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n tool_call_reporter=_stderr_tool_call_reporter,\n )\n return Pico(\n model_client=model,\n workspace=workspace,\n session_store=store,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n tool_call_reporter=_stderr_tool_call_reporter,\n )\n\n\ndef _maybe_evolve_durable_memory(agent, reason):\n result = agent.evolve_durable_memory(reason=reason)\n if result.get(\"status\") == \"failed\":\n print(f\"durable memory evolution failed: {result.get('error', '')}\", file=sys.stderr)\n return result\n\n\ndef build_arg_parser():\n parser = argparse.ArgumentParser(\n formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n description=\"Minimal coding agent for DeepSeek, OpenAI-compatible, Anthropic-compatible, or Ollama models.\",\n )\n parser.add_argument(\"prompt\", nargs=\"*\", help=\"Optional one-shot prompt.\")\n parser.add_argument(\"--cwd\", default=\".\", help=\"Workspace directory.\")\n parser.add_argument(\"--provider\", choices=(\"ollama\", \"openai\", \"anthropic\", \"deepseek\"), default=\"openai\", help=\"Model backend to use.\")\n parser.add_argument(\n \"--model\",\n default=None,\n help=\"Model name override. Defaults to qwen3.5:4b for Ollama, PICO_OPENAI_MODEL for openai, PICO_ANTHROPIC_MODEL for anthropic, and PICO_DEEPSEEK_MODEL for deepseek when set.\",\n )\n parser.add_argument(\"--host\", default=DEFAULT_OLLAMA_HOST, help=\"Ollama server URL.\")\n parser.add_argument(\"--base-url\", default=None, help=\"Provider API base URL for deepseek, openai, or anthropic.\")\n parser.add_argument(\"--ollama-timeout\", type=int, default=300, help=\"Ollama request timeout in seconds.\")\n parser.add_argument(\"--openai-timeout\", type=int, default=300, help=\"OpenAI-compatible request timeout in seconds.\")\n parser.add_argument(\"--resume\", default=None, help=\"Session id to resume or 'latest'.\")\n parser.add_argument(\"--approval\", choices=(\"ask\", \"auto\", \"never\"), default=\"ask\", help=\"Approval policy for risky tools.\")\n parser.add_argument(\n \"--secret-env-name\",\n dest=\"secret_env_names\",\n action=\"append\",\n default=[],\n help=\"Extra environment variable names to treat as secrets for trace/report redaction.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=6, help=\"Maximum tool/model iterations per request.\")\n parser.add_argument(\"--max-new-tokens\", type=int, default=8192, help=\"Maximum model output tokens per step.\")\n parser.add_argument(\"--no-memory-evolution\", action=\"store_true\", help=\"Disable session-end durable memory evolution.\")\n parser.add_argument(\"--temperature\", type=float, default=0.2, help=\"Sampling temperature sent to Ollama.\")\n parser.add_argument(\"--top-p\", type=float, default=0.9, help=\"Top-p sampling value sent to Ollama.\")\n return parser\n\n\ndef main(argv=None):\n args = build_arg_parser().parse_args(argv)\n agent = build_agent(args)\n if args.resume and not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"resume_activation\")\n\n model = getattr(agent.model_client, \"model\", getattr(args, \"model\", DEFAULT_OLLAMA_MODEL))\n\n host = getattr(agent.model_client, \"host\", getattr(agent.model_client, \"base_url\", getattr(args, \"host\", DEFAULT_OLLAMA_HOST)))\n print(build_welcome(agent, model=model, host=host))\n\n if args.prompt:\n\n prompt = \" \".join(args.prompt).strip()\n if prompt:\n print()\n try:\n print(agent.ask(prompt))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n return 1\n finally:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n while True:\n\n\n try:\n user_input = input(\"\\nLUMO> \").strip()\n except (EOFError, KeyboardInterrupt):\n print(\"\")\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n if not user_input:\n continue\n if user_input in {\"/exit\", \"/quit\"}:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n if user_input == \"/help\":\n print(HELP_DETAILS)\n continue\n if user_input == \"/memory\":\n print(agent.memory_text())\n continue\n if user_input == \"/session\":\n print(agent.session_path)\n continue\n if user_input == \"/reset\":\n agent.reset()\n print(\"session reset\")\n continue\n\n print()\n try:\n print(agent.ask(user_input))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n"}, "3456114ebdfb905de7c11ef6d18bcadd1a59a3c7": {"t": 1785767251.909396, "n": 15779, "h": "b919424ade32fcac", "c": "\"\"\"命令行入口。\n\n这个模块负责把“用户怎么启动 Lumo”翻译成 runtime 能理解的对象:\n解析参数、挑模型后端、构建工作区快照、恢复或新建 session,\n最后进入 one-shot 或交互式循环。\n\"\"\"\n\nimport argparse\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nimport unicodedata\nfrom pathlib import Path\n\nfrom .config import load_project_env, provider_env\nfrom .providers.clients import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient\nfrom .runtime import Pico, SessionStore\nfrom .workspace import AGENT_STATE_DIR, WorkspaceContext\n\nDEFAULT_SECRET_ENV_NAMES = (\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENAI_API_TOKEN\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"PICO_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"GITHUB_PAT\",\n \"GH_PAT\",\n)\n\nWELCOME_ART = tuple(\n \" \".join(parts)\n for parts in (\n (\"██ \", \"██ ██\", \"██ ██\", \"███████\"),\n (\"██ \", \"██ ██\", \"███ ███\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"███████\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ █ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"███████\", \"███████\", \"██ ██\", \"███████\"),\n )\n)\nWELCOME_NAME = \"Lumo\"\nWELCOME_SUBTITLE = \"Code is cheap, Show me your talk\"\n\nHELP_DETAILS = textwrap.dedent(\n \"\"\"\\\n Commands:\n /help Show this help message.\n /memory Show durable memory loaded from .lumo/memory.\n /session Show the path to the saved session file.\n /reset Clear the current session history and memory.\n /exit Exit the agent.\n \"\"\"\n).strip()\n\n\nDEFAULT_OLLAMA_MODEL = \"qwen3.5:4b\"\nDEFAULT_OLLAMA_HOST = \"http://127.0.0.1:11434\"\nDEFAULT_OPENAI_MODEL = \"gpt-5.4\"\nDEFAULT_OPENAI_BASE_URL = \"https://www.right.codes/codex/v1\"\nDEFAULT_ANTHROPIC_MODEL = \"claude-sonnet-4-6\"\nDEFAULT_ANTHROPIC_BASE_URL = \"https://www.right.codes/claude/v1\"\nDEFAULT_DEEPSEEK_MODEL = \"deepseek-v4-pro\"\nDEFAULT_DEEPSEEK_BASE_URL = \"https://api.deepseek.com/anthropic\"\nSECRET_ENV_NAMES_VAR = \"PICO_SECRET_ENV_NAMES\"\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-9;]*m\")\nANSI_RESET = \"\\x1b[0m\"\nWELCOME_BORDER_COLOR = \"\\x1b[38;2;125;211;252m\"\nWELCOME_GRADIENT_START = (255, 255, 255)\nWELCOME_GRADIENT_END = (125, 211, 252)\n\n\ndef _strip_ansi(text):\n return ANSI_PATTERN.sub(\"\", str(text))\n\n\ndef _ansi_rgb(red, green, blue):\n return f\"\\x1b[38;2;{red};{green};{blue}m\"\n\n\ndef _color(text, ansi_color):\n return f\"{ansi_color}{text}{ANSI_RESET}\"\n\n\ndef _gradient_text(text, start_rgb=WELCOME_GRADIENT_START, end_rgb=WELCOME_GRADIENT_END):\n text = str(text)\n span = max(1, len(text) - 1)\n parts = []\n for index, char in enumerate(text):\n if char == \" \":\n parts.append(char)\n continue\n ratio = index / span\n rgb = tuple(round(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb))\n parts.append(_color(char, _ansi_rgb(*rgb)))\n return \"\".join(parts)\n\n\ndef _terminal_char_width(char):\n return 2 if unicodedata.east_asian_width(char) in (\"F\", \"W\") else 1\n\n\ndef _terminal_width(text):\n return sum(_terminal_char_width(char) for char in _strip_ansi(text))\n\n\ndef _terminal_ljust(text, width):\n text = str(text)\n return text + \" \" * max(0, width - _terminal_width(text))\n\n\ndef _terminal_center(text, width):\n text = str(text)\n padding = max(0, width - _terminal_width(text))\n left = padding // 2\n right = padding - left\n return \" \" * left + text + \" \" * right\n\n\ndef _take_terminal_width(text, width, from_end=False):\n chars = reversed(str(text)) if from_end else str(text)\n selected = []\n used = 0\n for char in chars:\n char_width = _terminal_char_width(char)\n if used + char_width > width:\n break\n selected.append(char)\n used += char_width\n if from_end:\n selected.reverse()\n return \"\".join(selected)\n\n\ndef _terminal_middle(text, limit):\n text = str(text).replace(\"\\n\", \" \")\n if _terminal_width(text) <= limit:\n return text\n if limit <= 3:\n return _take_terminal_width(text, limit)\n left = (limit - 3) // 2\n right = limit - 3 - left\n return _take_terminal_width(text, left) + \"...\" + _take_terminal_width(text, right, from_end=True)\n\n\ndef _effective_model(args, provider):\n\n\n\n\n explicit_model = getattr(args, \"model\", None)\n if explicit_model:\n return explicit_model\n if provider == \"openai\":\n model = provider_env(\"PICO_OPENAI_MODEL\", (\"OPENAI_MODEL\",))\n if model:\n return model\n return DEFAULT_OPENAI_MODEL\n if provider == \"anthropic\":\n model = provider_env(\"PICO_ANTHROPIC_MODEL\", (\"ANTHROPIC_MODEL\",))\n if model:\n return model\n return DEFAULT_ANTHROPIC_MODEL\n if provider == \"deepseek\":\n model = provider_env(\"PICO_DEEPSEEK_MODEL\", (\"DEEPSEEK_MODEL\",))\n if model:\n return model\n return DEFAULT_DEEPSEEK_MODEL\n return DEFAULT_OLLAMA_MODEL\n\n\ndef _configured_secret_names(args):\n configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)\n configured_secret_names.update(str(name).upper() for name in args.secret_env_names)\n extra_names = os.environ.get(SECRET_ENV_NAMES_VAR, \"\")\n if extra_names.strip():\n configured_secret_names.update(\n item.strip().upper()\n for item in extra_names.split(\",\")\n if item.strip()\n )\n return sorted(configured_secret_names)\n\n\ndef _build_model_client(args):\n provider = getattr(args, \"provider\", \"deepseek\")\n\n\n if provider == \"openai\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_OPENAI_API_BASE\", (\"OPENAI_API_BASE\",), DEFAULT_OPENAI_BASE_URL)\n api_key = provider_env(\n \"PICO_OPENAI_API_KEY\",\n (\"OPENAI_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_ANTHROPIC_API_KEY\", \"ANTHROPIC_API_KEY\"),\n )\n return OpenAICompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"anthropic\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_ANTHROPIC_API_BASE\", (\"ANTHROPIC_API_BASE\",), DEFAULT_ANTHROPIC_BASE_URL)\n api_key = provider_env(\n \"PICO_ANTHROPIC_API_KEY\",\n (\"ANTHROPIC_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_OPENAI_API_KEY\", \"OPENAI_API_KEY\"),\n )\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"deepseek\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_DEEPSEEK_API_BASE\", (\"DEEPSEEK_API_BASE\",), DEFAULT_DEEPSEEK_BASE_URL)\n api_key = provider_env(\"PICO_DEEPSEEK_API_KEY\", (\"DEEPSEEK_API_KEY\",))\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n\n model = _effective_model(args, provider)\n host = getattr(args, \"host\", DEFAULT_OLLAMA_HOST)\n return OllamaModelClient(\n model=model,\n host=host,\n temperature=args.temperature,\n top_p=args.top_p,\n timeout=args.ollama_timeout,\n )\n\n\ndef build_welcome(agent, model, host):\n width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))\n inner = width - 4\n gap = 3\n left_width = (inner - gap) // 2\n right_width = inner - gap - left_width\n\n def row(text):\n body = _terminal_middle(text, width - 4)\n return _border_row(_terminal_ljust(body, width - 4))\n\n def divider(style=\"solid\"):\n char = \"═\" if style == \"strong\" else \"─\"\n left = \"╔\" if style == \"strong\" else \"├\"\n right = \"╗\" if style == \"strong\" else \"┤\"\n return _color(left + char * (width - 2) + right, WELCOME_BORDER_COLOR)\n\n def bottom_divider():\n return _color(\"╚\" + \"═\" * (width - 2) + \"╝\", WELCOME_BORDER_COLOR)\n\n def _border_row(body):\n return f\"{_color('║', WELCOME_BORDER_COLOR)} {body} {_color('║', WELCOME_BORDER_COLOR)}\"\n\n def center(text):\n body = _terminal_middle(text, inner)\n return _border_row(_terminal_center(body, inner))\n\n def cell(label, value, size):\n body = _terminal_middle(f\"{label:<9} {value}\", size)\n return _terminal_ljust(body, size)\n\n def pair(left_label, left_value, right_label, right_value):\n left = cell(left_label, left_value, left_width)\n right = cell(right_label, right_value, right_width)\n return _border_row(f\"{left}{' ' * gap}{right}\")\n\n line = divider(\"strong\")\n rows = [center(_gradient_text(text)) for text in WELCOME_ART]\n rows.extend(\n [\n center(WELCOME_NAME),\n center(WELCOME_SUBTITLE),\n\n divider(),\n row(\"\"),\n row(\"WORKSPACE \" + _terminal_middle(agent.workspace.cwd, inner - 11)),\n pair(\"MODEL\", model, \"BRANCH\", agent.workspace.branch),\n pair(\"APPROVAL\", agent.approval_policy, \"SESSION\", agent.session[\"id\"]),\n row(\"\"),\n ]\n )\n return \"\\n\".join([line, *rows, bottom_divider()])\n\n\ndef build_agent(args):\n \"\"\"根据 CLI 参数装配出一个可运行的 Pico 实例。\n\n 为什么存在:\n 命令行参数只是字符串和开关,runtime 需要的是已经装配好的对象图:\n model client、workspace snapshot、session store、secret 配置等。\n 这个函数负责把“启动参数”翻译成“agent 运行现场”。\n\n 输入 / 输出:\n - 输入:`argparse` 解析后的 `args`\n - 输出:一个新的 `Pico`,或一个从旧 session 恢复出来的 `Pico`\n\n 在 agent 链路里的位置:\n 它是整个程序启动链路里最靠近 runtime 的装配点。`main()` 先调它,\n 得到 agent 后,后面无论是 one-shot 还是 REPL 模式,都会落到 `ask()`。\n \"\"\"\n\n\n workspace = WorkspaceContext.build(args.cwd)\n\n load_project_env(workspace.repo_root)\n\n\n configured_secret_names = _configured_secret_names(args)\n store = SessionStore(Path(workspace.repo_root) / AGENT_STATE_DIR / \"sessions\")\n\n model = _build_model_client(args)\n session_id = args.resume\n if session_id == \"latest\":\n session_id = store.latest()\n if session_id:\n return Pico.from_session(\n model_client=model,\n workspace=workspace,\n session_store=store,\n session_id=session_id,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n )\n return Pico(\n model_client=model,\n workspace=workspace,\n session_store=store,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n )\n\n\ndef _maybe_evolve_durable_memory(agent, reason):\n result = agent.evolve_durable_memory(reason=reason)\n if result.get(\"status\") == \"failed\":\n print(f\"durable memory evolution failed: {result.get('error', '')}\", file=sys.stderr)\n return result\n\n\ndef build_arg_parser():\n parser = argparse.ArgumentParser(\n formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n description=\"Minimal coding agent for DeepSeek, OpenAI-compatible, Anthropic-compatible, or Ollama models.\",\n )\n parser.add_argument(\"prompt\", nargs=\"*\", help=\"Optional one-shot prompt.\")\n parser.add_argument(\"--cwd\", default=\".\", help=\"Workspace directory.\")\n parser.add_argument(\"--provider\", choices=(\"ollama\", \"openai\", \"anthropic\", \"deepseek\"), default=\"openai\", help=\"Model backend to use.\")\n parser.add_argument(\n \"--model\",\n default=None,\n help=\"Model name override. Defaults to qwen3.5:4b for Ollama, PICO_OPENAI_MODEL for openai, PICO_ANTHROPIC_MODEL for anthropic, and PICO_DEEPSEEK_MODEL for deepseek when set.\",\n )\n parser.add_argument(\"--host\", default=DEFAULT_OLLAMA_HOST, help=\"Ollama server URL.\")\n parser.add_argument(\"--base-url\", default=None, help=\"Provider API base URL for deepseek, openai, or anthropic.\")\n parser.add_argument(\"--ollama-timeout\", type=int, default=300, help=\"Ollama request timeout in seconds.\")\n parser.add_argument(\"--openai-timeout\", type=int, default=300, help=\"OpenAI-compatible request timeout in seconds.\")\n parser.add_argument(\"--resume\", default=None, help=\"Session id to resume or 'latest'.\")\n parser.add_argument(\"--approval\", choices=(\"ask\", \"auto\", \"never\"), default=\"ask\", help=\"Approval policy for risky tools.\")\n parser.add_argument(\n \"--secret-env-name\",\n dest=\"secret_env_names\",\n action=\"append\",\n default=[],\n help=\"Extra environment variable names to treat as secrets for trace/report redaction.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=6, help=\"Maximum tool/model iterations per request.\")\n parser.add_argument(\"--max-new-tokens\", type=int, default=8192, help=\"Maximum model output tokens per step.\")\n parser.add_argument(\"--no-memory-evolution\", action=\"store_true\", help=\"Disable session-end durable memory evolution.\")\n parser.add_argument(\"--temperature\", type=float, default=0.2, help=\"Sampling temperature sent to Ollama.\")\n parser.add_argument(\"--top-p\", type=float, default=0.9, help=\"Top-p sampling value sent to Ollama.\")\n return parser\n\n\ndef main(argv=None):\n args = build_arg_parser().parse_args(argv)\n agent = build_agent(args)\n if args.resume and not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"resume_activation\")\n\n model = getattr(agent.model_client, \"model\", getattr(args, \"model\", DEFAULT_OLLAMA_MODEL))\n\n host = getattr(agent.model_client, \"host\", getattr(agent.model_client, \"base_url\", getattr(args, \"host\", DEFAULT_OLLAMA_HOST)))\n print(build_welcome(agent, model=model, host=host))\n\n if args.prompt:\n\n prompt = \" \".join(args.prompt).strip()\n if prompt:\n print()\n try:\n print(agent.ask(prompt))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n return 1\n finally:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n while True:\n\n\n try:\n user_input = input(\"\\nLUMO> \").strip()\n except (EOFError, KeyboardInterrupt):\n print(\"\")\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n if not user_input:\n continue\n if user_input in {\"/exit\", \"/quit\"}:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n if user_input == \"/help\":\n print(HELP_DETAILS)\n continue\n if user_input == \"/memory\":\n print(agent.memory_text())\n continue\n if user_input == \"/session\":\n print(agent.session_path)\n continue\n if user_input == \"/reset\":\n agent.reset()\n print(\"session reset\")\n continue\n\n print()\n try:\n print(agent.ask(user_input))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n"}, "2be6e49a00ddb72383b29561fc980bf572975f48": {"t": 1785767253.324939, "n": 16524, "h": "87836cad28d98435", "c": "\"\"\"命令行入口。\n\n这个模块负责把“用户怎么启动 Lumo”翻译成 runtime 能理解的对象:\n解析参数、挑模型后端、构建工作区快照、恢复或新建 session,\n最后进入 one-shot 或交互式循环。\n\"\"\"\n\nimport argparse\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nimport unicodedata\nfrom pathlib import Path\n\nfrom .config import load_project_env, provider_env\nfrom .providers.clients import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient\nfrom .runtime import Pico, SessionStore\nfrom .workspace import AGENT_STATE_DIR, WorkspaceContext\n\nDEFAULT_SECRET_ENV_NAMES = (\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENAI_API_TOKEN\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"PICO_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"GITHUB_PAT\",\n \"GH_PAT\",\n)\n\nWELCOME_ART = tuple(\n \" \".join(parts)\n for parts in (\n (\"██ \", \"██ ██\", \"██ ██\", \"███████\"),\n (\"██ \", \"██ ██\", \"███ ███\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"███████\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ █ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"███████\", \"███████\", \"██ ██\", \"███████\"),\n )\n)\nWELCOME_NAME = \"Lumo\"\nWELCOME_SUBTITLE = \"Code is cheap, Show me your talk\"\n# WELCOME_STATUS = \"calm shell, ready for work\"\nHELP_DETAILS = textwrap.dedent(\n \"\"\"\\\n Commands:\n /help Show this help message.\n /memory Show durable memory loaded from .lumo/memory.\n /session Show the path to the saved session file.\n /reset Clear the current session history and memory.\n /exit Exit the agent.\n \"\"\"\n).strip()\n\n\nDEFAULT_OLLAMA_MODEL = \"qwen3.5:4b\"\nDEFAULT_OLLAMA_HOST = \"http://127.0.0.1:11434\"\nDEFAULT_OPENAI_MODEL = \"gpt-5.4\"\nDEFAULT_OPENAI_BASE_URL = \"https://www.right.codes/codex/v1\"\nDEFAULT_ANTHROPIC_MODEL = \"claude-sonnet-4-6\"\nDEFAULT_ANTHROPIC_BASE_URL = \"https://www.right.codes/claude/v1\"\nDEFAULT_DEEPSEEK_MODEL = \"deepseek-v4-pro\"\nDEFAULT_DEEPSEEK_BASE_URL = \"https://api.deepseek.com/anthropic\"\nSECRET_ENV_NAMES_VAR = \"PICO_SECRET_ENV_NAMES\"\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-9;]*m\")\nANSI_RESET = \"\\x1b[0m\"\nWELCOME_BORDER_COLOR = \"\\x1b[38;2;125;211;252m\"\nWELCOME_GRADIENT_START = (255, 255, 255)\nWELCOME_GRADIENT_END = (125, 211, 252)\n\n\ndef _strip_ansi(text):\n return ANSI_PATTERN.sub(\"\", str(text))\n\n\ndef _ansi_rgb(red, green, blue):\n return f\"\\x1b[38;2;{red};{green};{blue}m\"\n\n\ndef _color(text, ansi_color):\n return f\"{ansi_color}{text}{ANSI_RESET}\"\n\n\ndef _gradient_text(text, start_rgb=WELCOME_GRADIENT_START, end_rgb=WELCOME_GRADIENT_END):\n text = str(text)\n span = max(1, len(text) - 1)\n parts = []\n for index, char in enumerate(text):\n if char == \" \":\n parts.append(char)\n continue\n ratio = index / span\n rgb = tuple(round(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb))\n parts.append(_color(char, _ansi_rgb(*rgb)))\n return \"\".join(parts)\n\n\ndef _terminal_char_width(char):\n return 2 if unicodedata.east_asian_width(char) in (\"F\", \"W\") else 1\n\n\ndef _terminal_width(text):\n return sum(_terminal_char_width(char) for char in _strip_ansi(text))\n\n\ndef _terminal_ljust(text, width):\n text = str(text)\n return text + \" \" * max(0, width - _terminal_width(text))\n\n\ndef _terminal_center(text, width):\n text = str(text)\n padding = max(0, width - _terminal_width(text))\n left = padding // 2\n right = padding - left\n return \" \" * left + text + \" \" * right\n\n\ndef _take_terminal_width(text, width, from_end=False):\n chars = reversed(str(text)) if from_end else str(text)\n selected = []\n used = 0\n for char in chars:\n char_width = _terminal_char_width(char)\n if used + char_width > width:\n break\n selected.append(char)\n used += char_width\n if from_end:\n selected.reverse()\n return \"\".join(selected)\n\n\ndef _terminal_middle(text, limit):\n text = str(text).replace(\"\\n\", \" \")\n if _terminal_width(text) <= limit:\n return text\n if limit <= 3:\n return _take_terminal_width(text, limit)\n left = (limit - 3) // 2\n right = limit - 3 - left\n return _take_terminal_width(text, left) + \"...\" + _take_terminal_width(text, right, from_end=True)\n\n\ndef _effective_model(args, provider):\n # 模型选择优先级:\n # 1. 用户显式传入 --model\n # 2. provider 对应的环境变量\n # 3. 代码里的默认值\n explicit_model = getattr(args, \"model\", None)\n if explicit_model:\n return explicit_model\n if provider == \"openai\":\n model = provider_env(\"PICO_OPENAI_MODEL\", (\"OPENAI_MODEL\",))\n if model:\n return model\n return DEFAULT_OPENAI_MODEL\n if provider == \"anthropic\":\n model = provider_env(\"PICO_ANTHROPIC_MODEL\", (\"ANTHROPIC_MODEL\",))\n if model:\n return model\n return DEFAULT_ANTHROPIC_MODEL\n if provider == \"deepseek\":\n model = provider_env(\"PICO_DEEPSEEK_MODEL\", (\"DEEPSEEK_MODEL\",))\n if model:\n return model\n return DEFAULT_DEEPSEEK_MODEL\n return DEFAULT_OLLAMA_MODEL\n\n\ndef _configured_secret_names(args):\n configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)\n configured_secret_names.update(str(name).upper() for name in args.secret_env_names)\n extra_names = os.environ.get(SECRET_ENV_NAMES_VAR, \"\")\n if extra_names.strip():\n configured_secret_names.update(\n item.strip().upper()\n for item in extra_names.split(\",\")\n if item.strip()\n )\n return sorted(configured_secret_names)\n\n\ndef _build_model_client(args):\n provider = getattr(args, \"provider\", \"deepseek\")\n # CLI 只负责把 provider 选择翻译成具体 client。\n # 真正的提示词格式、缓存支持、HTTP 协议差异,都封装在 models.py 里。\n if provider == \"openai\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_OPENAI_API_BASE\", (\"OPENAI_API_BASE\",), DEFAULT_OPENAI_BASE_URL)\n api_key = provider_env(\n \"PICO_OPENAI_API_KEY\",\n (\"OPENAI_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_ANTHROPIC_API_KEY\", \"ANTHROPIC_API_KEY\"),\n )\n return OpenAICompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"anthropic\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_ANTHROPIC_API_BASE\", (\"ANTHROPIC_API_BASE\",), DEFAULT_ANTHROPIC_BASE_URL)\n api_key = provider_env(\n \"PICO_ANTHROPIC_API_KEY\",\n (\"ANTHROPIC_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_OPENAI_API_KEY\", \"OPENAI_API_KEY\"),\n )\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"deepseek\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_DEEPSEEK_API_BASE\", (\"DEEPSEEK_API_BASE\",), DEFAULT_DEEPSEEK_BASE_URL)\n api_key = provider_env(\"PICO_DEEPSEEK_API_KEY\", (\"DEEPSEEK_API_KEY\",))\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n\n model = _effective_model(args, provider)\n host = getattr(args, \"host\", DEFAULT_OLLAMA_HOST)\n return OllamaModelClient(\n model=model,\n host=host,\n temperature=args.temperature,\n top_p=args.top_p,\n timeout=args.ollama_timeout,\n )\n\n\ndef build_welcome(agent, model, host):\n width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))\n inner = width - 4\n gap = 3\n left_width = (inner - gap) // 2\n right_width = inner - gap - left_width\n\n def row(text):\n body = _terminal_middle(text, width - 4)\n return _border_row(_terminal_ljust(body, width - 4))\n\n def divider(style=\"solid\"):\n char = \"═\" if style == \"strong\" else \"─\"\n left = \"╔\" if style == \"strong\" else \"├\"\n right = \"╗\" if style == \"strong\" else \"┤\"\n return _color(left + char * (width - 2) + right, WELCOME_BORDER_COLOR)\n\n def bottom_divider():\n return _color(\"╚\" + \"═\" * (width - 2) + \"╝\", WELCOME_BORDER_COLOR)\n\n def _border_row(body):\n return f\"{_color('║', WELCOME_BORDER_COLOR)} {body} {_color('║', WELCOME_BORDER_COLOR)}\"\n\n def center(text):\n body = _terminal_middle(text, inner)\n return _border_row(_terminal_center(body, inner))\n\n def cell(label, value, size):\n body = _terminal_middle(f\"{label:<9} {value}\", size)\n return _terminal_ljust(body, size)\n\n def pair(left_label, left_value, right_label, right_value):\n left = cell(left_label, left_value, left_width)\n right = cell(right_label, right_value, right_width)\n return _border_row(f\"{left}{' ' * gap}{right}\")\n\n line = divider(\"strong\")\n rows = [center(_gradient_text(text)) for text in WELCOME_ART]\n rows.extend(\n [\n center(WELCOME_NAME),\n center(WELCOME_SUBTITLE),\n # center(WELCOME_STATUS),\n divider(),\n row(\"\"),\n row(\"WORKSPACE \" + _terminal_middle(agent.workspace.cwd, inner - 11)),\n pair(\"MODEL\", model, \"BRANCH\", agent.workspace.branch),\n pair(\"APPROVAL\", agent.approval_policy, \"SESSION\", agent.session[\"id\"]),\n row(\"\"),\n ]\n )\n return \"\\n\".join([line, *rows, bottom_divider()])\n\n\ndef build_agent(args):\n \"\"\"根据 CLI 参数装配出一个可运行的 Pico 实例。\n\n 为什么存在:\n 命令行参数只是字符串和开关,runtime 需要的是已经装配好的对象图:\n model client、workspace snapshot、session store、secret 配置等。\n 这个函数负责把“启动参数”翻译成“agent 运行现场”。\n\n 输入 / 输出:\n - 输入:`argparse` 解析后的 `args`\n - 输出:一个新的 `Pico`,或一个从旧 session 恢复出来的 `Pico`\n\n 在 agent 链路里的位置:\n 它是整个程序启动链路里最靠近 runtime 的装配点。`main()` 先调它,\n 得到 agent 后,后面无论是 one-shot 还是 REPL 模式,都会落到 `ask()`。\n \"\"\"\n # 这里是 CLI 到 runtime 的装配点:\n # 先采集工作区快照和加载项目级环境,再整理 secret 名单、模型后端和 session。\n workspace = WorkspaceContext.build(args.cwd)\n # 加载项目级环境变量,覆盖系统环境。这些环境变量可能会被后续的模型调用用到,\n load_project_env(workspace.repo_root)\n# “整理出一份最终要被当成敏感信息处理的环境变量名列表。”\n# 也就是说,后面程序在写 trace、report、session 之类内容时,看到这些环境变量名,就会把它们脱敏,不直接暴露真实值。\n configured_secret_names = _configured_secret_names(args)\n store = SessionStore(Path(workspace.repo_root) / AGENT_STATE_DIR / \"sessions\")\n # return OpenAICompatibleModelClient\n model = _build_model_client(args)\n session_id = args.resume\n if session_id == \"latest\":\n session_id = store.latest()\n if session_id:\n return Pico.from_session(\n model_client=model,\n workspace=workspace,\n session_store=store,\n session_id=session_id,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n )\n return Pico(\n model_client=model,\n workspace=workspace,\n session_store=store,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n )\n\n\ndef _maybe_evolve_durable_memory(agent, reason):\n result = agent.evolve_durable_memory(reason=reason)\n if result.get(\"status\") == \"failed\":\n print(f\"durable memory evolution failed: {result.get('error', '')}\", file=sys.stderr)\n return result\n\n\ndef build_arg_parser():\n parser = argparse.ArgumentParser(\n formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n description=\"Minimal coding agent for DeepSeek, OpenAI-compatible, Anthropic-compatible, or Ollama models.\",\n )\n parser.add_argument(\"prompt\", nargs=\"*\", help=\"Optional one-shot prompt.\")\n parser.add_argument(\"--cwd\", default=\".\", help=\"Workspace directory.\")\n parser.add_argument(\"--provider\", choices=(\"ollama\", \"openai\", \"anthropic\", \"deepseek\"), default=\"openai\", help=\"Model backend to use.\")\n parser.add_argument(\n \"--model\",\n default=None,\n help=\"Model name override. Defaults to qwen3.5:4b for Ollama, PICO_OPENAI_MODEL for openai, PICO_ANTHROPIC_MODEL for anthropic, and PICO_DEEPSEEK_MODEL for deepseek when set.\",\n )\n parser.add_argument(\"--host\", default=DEFAULT_OLLAMA_HOST, help=\"Ollama server URL.\")\n parser.add_argument(\"--base-url\", default=None, help=\"Provider API base URL for deepseek, openai, or anthropic.\")\n parser.add_argument(\"--ollama-timeout\", type=int, default=300, help=\"Ollama request timeout in seconds.\")\n parser.add_argument(\"--openai-timeout\", type=int, default=300, help=\"OpenAI-compatible request timeout in seconds.\")\n parser.add_argument(\"--resume\", default=None, help=\"Session id to resume or 'latest'.\")\n parser.add_argument(\"--approval\", choices=(\"ask\", \"auto\", \"never\"), default=\"ask\", help=\"Approval policy for risky tools.\")\n parser.add_argument(\n \"--secret-env-name\",\n dest=\"secret_env_names\",\n action=\"append\",\n default=[],\n help=\"Extra environment variable names to treat as secrets for trace/report redaction.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=6, help=\"Maximum tool/model iterations per request.\")\n parser.add_argument(\"--max-new-tokens\", type=int, default=8192, help=\"Maximum model output tokens per step.\")\n parser.add_argument(\"--no-memory-evolution\", action=\"store_true\", help=\"Disable session-end durable memory evolution.\")\n parser.add_argument(\"--temperature\", type=float, default=0.2, help=\"Sampling temperature sent to Ollama.\")\n parser.add_argument(\"--top-p\", type=float, default=0.9, help=\"Top-p sampling value sent to Ollama.\")\n return parser\n\n\ndef main(argv=None):\n args = build_arg_parser().parse_args(argv)\n agent = build_agent(args)\n if args.resume and not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"resume_activation\")\n # 欢迎页里显示的 MODEL gpt-5.4\n model = getattr(agent.model_client, \"model\", getattr(args, \"model\", DEFAULT_OLLAMA_MODEL))\n # base_url 或 host 都行,优先级:model_client 里如果有就用它的,否则用 CLI 参数里的,再没有就用默认值。\n host = getattr(agent.model_client, \"host\", getattr(agent.model_client, \"base_url\", getattr(args, \"host\", DEFAULT_OLLAMA_HOST)))\n print(build_welcome(agent, model=model, host=host))\n\n if args.prompt:\n # one-shot 模式:只跑一次 ask,不进入 REPL 循环。\n prompt = \" \".join(args.prompt).strip()\n if prompt:\n print()\n try:\n print(agent.ask(prompt))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n return 1\n finally:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n while True:\n # 交互模式:每次读取一条用户输入,交给同一个 agent,\n # 因此 session history 和 working memory 会跨轮延续。\n try:\n user_input = input(\"\\nLUMO> \").strip()\n except (EOFError, KeyboardInterrupt):\n print(\"\")\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n if not user_input:\n continue\n if user_input in {\"/exit\", \"/quit\"}:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n if user_input == \"/help\":\n print(HELP_DETAILS)\n continue\n if user_input == \"/memory\":\n print(agent.memory_text())\n continue\n if user_input == \"/session\":\n print(agent.session_path)\n continue\n if user_input == \"/reset\":\n agent.reset()\n print(\"session reset\")\n continue\n\n print()\n try:\n print(agent.ask(user_input))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n"}, "2123b6de1f7f9b4ecc1b95cf7a37a26da61c1cd2": {"t": 1785767254.9359956, "n": 16521, "h": "c1115fa4f467b4a2", "c": "\"\"\"命令行入口。\n\n这个模块负责把“用户怎么启动 Lumo”翻译成 runtime 能理解的对象:\n解析参数、挑模型后端、构建工作区快照、恢复或新建 session,\n最后进入 one-shot 或交互式循环。\n\"\"\"\n\nimport argparse\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nimport unicodedata\nfrom pathlib import Path\n\nfrom .config import load_project_env, provider_env\nfrom .providers.clients import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient\nfrom .runtime import Pico, SessionStore\nfrom .workspace import AGENT_STATE_DIR, WorkspaceContext\n\nDEFAULT_SECRET_ENV_NAMES = (\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENAI_API_TOKEN\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"PICO_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"GITHUB_PAT\",\n \"GH_PAT\",\n)\n\nWELCOME_ART = tuple(\n \" \".join(parts)\n for parts in (\n (\"██ \", \"██ ██\", \"██ ██\", \"███████\"),\n (\"██ \", \"██ ██\", \"███ ███\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"███████\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ █ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"███████\", \"███████\", \"██ ██\", \"███████\"),\n )\n)\nWELCOME_NAME = \"Lumo\"\nWELCOME_SUBTITLE = \"Code is cheap, Show me your talk\"\n# WELCOME_STATUS = \"calm shell, ready for work\"\nHELP_DETAILS = textwrap.dedent(\n \"\"\"\\\n Commands:\n /help Show this help message.\n /memory Show the agent's distilled working memory.\n /session Show the path to the saved session file.\n /reset Clear the current session history and memory.\n /exit Exit the agent.\n \"\"\"\n).strip()\n\n\nDEFAULT_OLLAMA_MODEL = \"qwen3.5:4b\"\nDEFAULT_OLLAMA_HOST = \"http://127.0.0.1:11434\"\nDEFAULT_OPENAI_MODEL = \"gpt-5.4\"\nDEFAULT_OPENAI_BASE_URL = \"https://www.right.codes/codex/v1\"\nDEFAULT_ANTHROPIC_MODEL = \"claude-sonnet-4-6\"\nDEFAULT_ANTHROPIC_BASE_URL = \"https://www.right.codes/claude/v1\"\nDEFAULT_DEEPSEEK_MODEL = \"deepseek-v4-pro\"\nDEFAULT_DEEPSEEK_BASE_URL = \"https://api.deepseek.com/anthropic\"\nSECRET_ENV_NAMES_VAR = \"PICO_SECRET_ENV_NAMES\"\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-9;]*m\")\nANSI_RESET = \"\\x1b[0m\"\nWELCOME_BORDER_COLOR = \"\\x1b[38;2;125;211;252m\"\nWELCOME_GRADIENT_START = (255, 255, 255)\nWELCOME_GRADIENT_END = (125, 211, 252)\n\n\ndef _strip_ansi(text):\n return ANSI_PATTERN.sub(\"\", str(text))\n\n\ndef _ansi_rgb(red, green, blue):\n return f\"\\x1b[38;2;{red};{green};{blue}m\"\n\n\ndef _color(text, ansi_color):\n return f\"{ansi_color}{text}{ANSI_RESET}\"\n\n\ndef _gradient_text(text, start_rgb=WELCOME_GRADIENT_START, end_rgb=WELCOME_GRADIENT_END):\n text = str(text)\n span = max(1, len(text) - 1)\n parts = []\n for index, char in enumerate(text):\n if char == \" \":\n parts.append(char)\n continue\n ratio = index / span\n rgb = tuple(round(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb))\n parts.append(_color(char, _ansi_rgb(*rgb)))\n return \"\".join(parts)\n\n\ndef _terminal_char_width(char):\n return 2 if unicodedata.east_asian_width(char) in (\"F\", \"W\") else 1\n\n\ndef _terminal_width(text):\n return sum(_terminal_char_width(char) for char in _strip_ansi(text))\n\n\ndef _terminal_ljust(text, width):\n text = str(text)\n return text + \" \" * max(0, width - _terminal_width(text))\n\n\ndef _terminal_center(text, width):\n text = str(text)\n padding = max(0, width - _terminal_width(text))\n left = padding // 2\n right = padding - left\n return \" \" * left + text + \" \" * right\n\n\ndef _take_terminal_width(text, width, from_end=False):\n chars = reversed(str(text)) if from_end else str(text)\n selected = []\n used = 0\n for char in chars:\n char_width = _terminal_char_width(char)\n if used + char_width > width:\n break\n selected.append(char)\n used += char_width\n if from_end:\n selected.reverse()\n return \"\".join(selected)\n\n\ndef _terminal_middle(text, limit):\n text = str(text).replace(\"\\n\", \" \")\n if _terminal_width(text) <= limit:\n return text\n if limit <= 3:\n return _take_terminal_width(text, limit)\n left = (limit - 3) // 2\n right = limit - 3 - left\n return _take_terminal_width(text, left) + \"...\" + _take_terminal_width(text, right, from_end=True)\n\n\ndef _effective_model(args, provider):\n # 模型选择优先级:\n # 1. 用户显式传入 --model\n # 2. provider 对应的环境变量\n # 3. 代码里的默认值\n explicit_model = getattr(args, \"model\", None)\n if explicit_model:\n return explicit_model\n if provider == \"openai\":\n model = provider_env(\"PICO_OPENAI_MODEL\", (\"OPENAI_MODEL\",))\n if model:\n return model\n return DEFAULT_OPENAI_MODEL\n if provider == \"anthropic\":\n model = provider_env(\"PICO_ANTHROPIC_MODEL\", (\"ANTHROPIC_MODEL\",))\n if model:\n return model\n return DEFAULT_ANTHROPIC_MODEL\n if provider == \"deepseek\":\n model = provider_env(\"PICO_DEEPSEEK_MODEL\", (\"DEEPSEEK_MODEL\",))\n if model:\n return model\n return DEFAULT_DEEPSEEK_MODEL\n return DEFAULT_OLLAMA_MODEL\n\n\ndef _configured_secret_names(args):\n configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)\n configured_secret_names.update(str(name).upper() for name in args.secret_env_names)\n extra_names = os.environ.get(SECRET_ENV_NAMES_VAR, \"\")\n if extra_names.strip():\n configured_secret_names.update(\n item.strip().upper()\n for item in extra_names.split(\",\")\n if item.strip()\n )\n return sorted(configured_secret_names)\n\n\ndef _build_model_client(args):\n provider = getattr(args, \"provider\", \"deepseek\")\n # CLI 只负责把 provider 选择翻译成具体 client。\n # 真正的提示词格式、缓存支持、HTTP 协议差异,都封装在 models.py 里。\n if provider == \"openai\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_OPENAI_API_BASE\", (\"OPENAI_API_BASE\",), DEFAULT_OPENAI_BASE_URL)\n api_key = provider_env(\n \"PICO_OPENAI_API_KEY\",\n (\"OPENAI_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_ANTHROPIC_API_KEY\", \"ANTHROPIC_API_KEY\"),\n )\n return OpenAICompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"anthropic\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_ANTHROPIC_API_BASE\", (\"ANTHROPIC_API_BASE\",), DEFAULT_ANTHROPIC_BASE_URL)\n api_key = provider_env(\n \"PICO_ANTHROPIC_API_KEY\",\n (\"ANTHROPIC_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_OPENAI_API_KEY\", \"OPENAI_API_KEY\"),\n )\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"deepseek\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_DEEPSEEK_API_BASE\", (\"DEEPSEEK_API_BASE\",), DEFAULT_DEEPSEEK_BASE_URL)\n api_key = provider_env(\"PICO_DEEPSEEK_API_KEY\", (\"DEEPSEEK_API_KEY\",))\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n\n model = _effective_model(args, provider)\n host = getattr(args, \"host\", DEFAULT_OLLAMA_HOST)\n return OllamaModelClient(\n model=model,\n host=host,\n temperature=args.temperature,\n top_p=args.top_p,\n timeout=args.ollama_timeout,\n )\n\n\ndef build_welcome(agent, model, host):\n width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))\n inner = width - 4\n gap = 3\n left_width = (inner - gap) // 2\n right_width = inner - gap - left_width\n\n def row(text):\n body = _terminal_middle(text, width - 4)\n return _border_row(_terminal_ljust(body, width - 4))\n\n def divider(style=\"solid\"):\n char = \"═\" if style == \"strong\" else \"─\"\n left = \"╔\" if style == \"strong\" else \"├\"\n right = \"╗\" if style == \"strong\" else \"┤\"\n return _color(left + char * (width - 2) + right, WELCOME_BORDER_COLOR)\n\n def bottom_divider():\n return _color(\"╚\" + \"═\" * (width - 2) + \"╝\", WELCOME_BORDER_COLOR)\n\n def _border_row(body):\n return f\"{_color('║', WELCOME_BORDER_COLOR)} {body} {_color('║', WELCOME_BORDER_COLOR)}\"\n\n def center(text):\n body = _terminal_middle(text, inner)\n return _border_row(_terminal_center(body, inner))\n\n def cell(label, value, size):\n body = _terminal_middle(f\"{label:<9} {value}\", size)\n return _terminal_ljust(body, size)\n\n def pair(left_label, left_value, right_label, right_value):\n left = cell(left_label, left_value, left_width)\n right = cell(right_label, right_value, right_width)\n return _border_row(f\"{left}{' ' * gap}{right}\")\n\n line = divider(\"strong\")\n rows = [center(_gradient_text(text)) for text in WELCOME_ART]\n rows.extend(\n [\n center(WELCOME_NAME),\n center(WELCOME_SUBTITLE),\n # center(WELCOME_STATUS),\n divider(),\n row(\"\"),\n row(\"WORKSPACE \" + _terminal_middle(agent.workspace.cwd, inner - 11)),\n pair(\"MODEL\", model, \"BRANCH\", agent.workspace.branch),\n pair(\"APPROVAL\", agent.approval_policy, \"SESSION\", agent.session[\"id\"]),\n row(\"\"),\n ]\n )\n return \"\\n\".join([line, *rows, bottom_divider()])\n\n\ndef build_agent(args):\n \"\"\"根据 CLI 参数装配出一个可运行的 Pico 实例。\n\n 为什么存在:\n 命令行参数只是字符串和开关,runtime 需要的是已经装配好的对象图:\n model client、workspace snapshot、session store、secret 配置等。\n 这个函数负责把“启动参数”翻译成“agent 运行现场”。\n\n 输入 / 输出:\n - 输入:`argparse` 解析后的 `args`\n - 输出:一个新的 `Pico`,或一个从旧 session 恢复出来的 `Pico`\n\n 在 agent 链路里的位置:\n 它是整个程序启动链路里最靠近 runtime 的装配点。`main()` 先调它,\n 得到 agent 后,后面无论是 one-shot 还是 REPL 模式,都会落到 `ask()`。\n \"\"\"\n # 这里是 CLI 到 runtime 的装配点:\n # 先采集工作区快照和加载项目级环境,再整理 secret 名单、模型后端和 session。\n workspace = WorkspaceContext.build(args.cwd)\n # 加载项目级环境变量,覆盖系统环境。这些环境变量可能会被后续的模型调用用到,\n load_project_env(workspace.repo_root)\n# “整理出一份最终要被当成敏感信息处理的环境变量名列表。”\n# 也就是说,后面程序在写 trace、report、session 之类内容时,看到这些环境变量名,就会把它们脱敏,不直接暴露真实值。\n configured_secret_names = _configured_secret_names(args)\n store = SessionStore(Path(workspace.repo_root) / AGENT_STATE_DIR / \"sessions\")\n # return OpenAICompatibleModelClient\n model = _build_model_client(args)\n session_id = args.resume\n if session_id == \"latest\":\n session_id = store.latest()\n if session_id:\n return Pico.from_session(\n model_client=model,\n workspace=workspace,\n session_store=store,\n session_id=session_id,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n )\n return Pico(\n model_client=model,\n workspace=workspace,\n session_store=store,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n )\n\n\ndef _maybe_evolve_durable_memory(agent, reason):\n result = agent.evolve_durable_memory(reason=reason)\n if result.get(\"status\") == \"failed\":\n print(f\"durable memory evolution failed: {result.get('error', '')}\", file=sys.stderr)\n return result\n\n\ndef build_arg_parser():\n parser = argparse.ArgumentParser(\n formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n description=\"Minimal coding agent for DeepSeek, OpenAI-compatible, Anthropic-compatible, or Ollama models.\",\n )\n parser.add_argument(\"prompt\", nargs=\"*\", help=\"Optional one-shot prompt.\")\n parser.add_argument(\"--cwd\", default=\".\", help=\"Workspace directory.\")\n parser.add_argument(\"--provider\", choices=(\"ollama\", \"openai\", \"anthropic\", \"deepseek\"), default=\"openai\", help=\"Model backend to use.\")\n parser.add_argument(\n \"--model\",\n default=None,\n help=\"Model name override. Defaults to qwen3.5:4b for Ollama, PICO_OPENAI_MODEL for openai, PICO_ANTHROPIC_MODEL for anthropic, and PICO_DEEPSEEK_MODEL for deepseek when set.\",\n )\n parser.add_argument(\"--host\", default=DEFAULT_OLLAMA_HOST, help=\"Ollama server URL.\")\n parser.add_argument(\"--base-url\", default=None, help=\"Provider API base URL for deepseek, openai, or anthropic.\")\n parser.add_argument(\"--ollama-timeout\", type=int, default=300, help=\"Ollama request timeout in seconds.\")\n parser.add_argument(\"--openai-timeout\", type=int, default=300, help=\"OpenAI-compatible request timeout in seconds.\")\n parser.add_argument(\"--resume\", default=None, help=\"Session id to resume or 'latest'.\")\n parser.add_argument(\"--approval\", choices=(\"ask\", \"auto\", \"never\"), default=\"ask\", help=\"Approval policy for risky tools.\")\n parser.add_argument(\n \"--secret-env-name\",\n dest=\"secret_env_names\",\n action=\"append\",\n default=[],\n help=\"Extra environment variable names to treat as secrets for trace/report redaction.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=6, help=\"Maximum tool/model iterations per request.\")\n parser.add_argument(\"--max-new-tokens\", type=int, default=8192, help=\"Maximum model output tokens per step.\")\n parser.add_argument(\"--no-memory-evolution\", action=\"store_true\", help=\"Disable session-end durable memory evolution.\")\n parser.add_argument(\"--temperature\", type=float, default=0.2, help=\"Sampling temperature sent to Ollama.\")\n parser.add_argument(\"--top-p\", type=float, default=0.9, help=\"Top-p sampling value sent to Ollama.\")\n return parser\n\n\ndef main(argv=None):\n args = build_arg_parser().parse_args(argv)\n agent = build_agent(args)\n if args.resume and not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"resume_activation\")\n # 欢迎页里显示的 MODEL gpt-5.4\n model = getattr(agent.model_client, \"model\", getattr(args, \"model\", DEFAULT_OLLAMA_MODEL))\n # base_url 或 host 都行,优先级:model_client 里如果有就用它的,否则用 CLI 参数里的,再没有就用默认值。\n host = getattr(agent.model_client, \"host\", getattr(agent.model_client, \"base_url\", getattr(args, \"host\", DEFAULT_OLLAMA_HOST)))\n print(build_welcome(agent, model=model, host=host))\n\n if args.prompt:\n # one-shot 模式:只跑一次 ask,不进入 REPL 循环。\n prompt = \" \".join(args.prompt).strip()\n if prompt:\n print()\n try:\n print(agent.ask(prompt))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n return 1\n finally:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n while True:\n # 交互模式:每次读取一条用户输入,交给同一个 agent,\n # 因此 session history 和 working memory 会跨轮延续。\n try:\n user_input = input(\"\\nLUMO> \").strip()\n except (EOFError, KeyboardInterrupt):\n print(\"\")\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n\n if not user_input:\n continue\n if user_input in {\"/exit\", \"/quit\"}:\n if not args.no_memory_evolution:\n _maybe_evolve_durable_memory(agent, \"session_end\")\n return 0\n if user_input == \"/help\":\n print(HELP_DETAILS)\n continue\n if user_input == \"/memory\":\n print(agent.memory_text())\n continue\n if user_input == \"/session\":\n print(agent.session_path)\n continue\n if user_input == \"/reset\":\n agent.reset()\n print(\"session reset\")\n continue\n\n print()\n try:\n print(agent.ask(user_input))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n"}, "99379c0df1cf102c08b3c9f69967411bfdd99e47": {"t": 1785767256.3968346, "n": 15654, "h": "91330d82b673f90b", "c": "\"\"\"命令行入口。\n\n这个模块负责把“用户怎么启动 Lumo”翻译成 runtime 能理解的对象:\n解析参数、挑模型后端、构建工作区快照、恢复或新建 session,\n最后进入 one-shot 或交互式循环。\n\"\"\"\n\nimport argparse\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nimport unicodedata\nfrom pathlib import Path\n\nfrom .config import load_project_env, provider_env\nfrom .providers.clients import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient\nfrom .runtime import Pico, SessionStore\nfrom .workspace import AGENT_STATE_DIR, WorkspaceContext\n\nDEFAULT_SECRET_ENV_NAMES = (\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENAI_API_TOKEN\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"PICO_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"GITHUB_PAT\",\n \"GH_PAT\",\n)\n\nWELCOME_ART = tuple(\n \" \".join(parts)\n for parts in (\n (\"██ \", \"██ ██\", \"██ ██\", \"███████\"),\n (\"██ \", \"██ ██\", \"███ ███\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"███████\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ █ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"███████\", \"███████\", \"██ ██\", \"███████\"),\n )\n)\nWELCOME_NAME = \"Lumo\"\nWELCOME_SUBTITLE = \"Code is cheap, Show me your talk\"\n# WELCOME_STATUS = \"calm shell, ready for work\"\nHELP_DETAILS = textwrap.dedent(\n \"\"\"\\\n Commands:\n /help Show this help message.\n /memory Show the agent's distilled working memory.\n /session Show the path to the saved session file.\n /reset Clear the current session history and memory.\n /exit Exit the agent.\n \"\"\"\n).strip()\n\n\nDEFAULT_OLLAMA_MODEL = \"qwen3.5:4b\"\nDEFAULT_OLLAMA_HOST = \"http://127.0.0.1:11434\"\nDEFAULT_OPENAI_MODEL = \"gpt-5.4\"\nDEFAULT_OPENAI_BASE_URL = \"https://www.right.codes/codex/v1\"\nDEFAULT_ANTHROPIC_MODEL = \"claude-sonnet-4-6\"\nDEFAULT_ANTHROPIC_BASE_URL = \"https://www.right.codes/claude/v1\"\nDEFAULT_DEEPSEEK_MODEL = \"deepseek-v4-pro\"\nDEFAULT_DEEPSEEK_BASE_URL = \"https://api.deepseek.com/anthropic\"\nSECRET_ENV_NAMES_VAR = \"PICO_SECRET_ENV_NAMES\"\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-9;]*m\")\nANSI_RESET = \"\\x1b[0m\"\nWELCOME_BORDER_COLOR = \"\\x1b[38;2;125;211;252m\"\nWELCOME_GRADIENT_START = (255, 255, 255)\nWELCOME_GRADIENT_END = (125, 211, 252)\n\n\ndef _strip_ansi(text):\n return ANSI_PATTERN.sub(\"\", str(text))\n\n\ndef _ansi_rgb(red, green, blue):\n return f\"\\x1b[38;2;{red};{green};{blue}m\"\n\n\ndef _color(text, ansi_color):\n return f\"{ansi_color}{text}{ANSI_RESET}\"\n\n\ndef _gradient_text(text, start_rgb=WELCOME_GRADIENT_START, end_rgb=WELCOME_GRADIENT_END):\n text = str(text)\n span = max(1, len(text) - 1)\n parts = []\n for index, char in enumerate(text):\n if char == \" \":\n parts.append(char)\n continue\n ratio = index / span\n rgb = tuple(round(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb))\n parts.append(_color(char, _ansi_rgb(*rgb)))\n return \"\".join(parts)\n\n\ndef _terminal_char_width(char):\n return 2 if unicodedata.east_asian_width(char) in (\"F\", \"W\") else 1\n\n\ndef _terminal_width(text):\n return sum(_terminal_char_width(char) for char in _strip_ansi(text))\n\n\ndef _terminal_ljust(text, width):\n text = str(text)\n return text + \" \" * max(0, width - _terminal_width(text))\n\n\ndef _terminal_center(text, width):\n text = str(text)\n padding = max(0, width - _terminal_width(text))\n left = padding // 2\n right = padding - left\n return \" \" * left + text + \" \" * right\n\n\ndef _take_terminal_width(text, width, from_end=False):\n chars = reversed(str(text)) if from_end else str(text)\n selected = []\n used = 0\n for char in chars:\n char_width = _terminal_char_width(char)\n if used + char_width > width:\n break\n selected.append(char)\n used += char_width\n if from_end:\n selected.reverse()\n return \"\".join(selected)\n\n\ndef _terminal_middle(text, limit):\n text = str(text).replace(\"\\n\", \" \")\n if _terminal_width(text) <= limit:\n return text\n if limit <= 3:\n return _take_terminal_width(text, limit)\n left = (limit - 3) // 2\n right = limit - 3 - left\n return _take_terminal_width(text, left) + \"...\" + _take_terminal_width(text, right, from_end=True)\n\n\ndef _effective_model(args, provider):\n # 模型选择优先级:\n # 1. 用户显式传入 --model\n # 2. provider 对应的环境变量\n # 3. 代码里的默认值\n explicit_model = getattr(args, \"model\", None)\n if explicit_model:\n return explicit_model\n if provider == \"openai\":\n model = provider_env(\"PICO_OPENAI_MODEL\", (\"OPENAI_MODEL\",))\n if model:\n return model\n return DEFAULT_OPENAI_MODEL\n if provider == \"anthropic\":\n model = provider_env(\"PICO_ANTHROPIC_MODEL\", (\"ANTHROPIC_MODEL\",))\n if model:\n return model\n return DEFAULT_ANTHROPIC_MODEL\n if provider == \"deepseek\":\n model = provider_env(\"PICO_DEEPSEEK_MODEL\", (\"DEEPSEEK_MODEL\",))\n if model:\n return model\n return DEFAULT_DEEPSEEK_MODEL\n return DEFAULT_OLLAMA_MODEL\n\n\ndef _configured_secret_names(args):\n configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)\n configured_secret_names.update(str(name).upper() for name in args.secret_env_names)\n extra_names = os.environ.get(SECRET_ENV_NAMES_VAR, \"\")\n if extra_names.strip():\n configured_secret_names.update(\n item.strip().upper()\n for item in extra_names.split(\",\")\n if item.strip()\n )\n return sorted(configured_secret_names)\n\n\ndef _build_model_client(args):\n provider = getattr(args, \"provider\", \"deepseek\")\n # CLI 只负责把 provider 选择翻译成具体 client。\n # 真正的提示词格式、缓存支持、HTTP 协议差异,都封装在 models.py 里。\n if provider == \"openai\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_OPENAI_API_BASE\", (\"OPENAI_API_BASE\",), DEFAULT_OPENAI_BASE_URL)\n api_key = provider_env(\n \"PICO_OPENAI_API_KEY\",\n (\"OPENAI_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_ANTHROPIC_API_KEY\", \"ANTHROPIC_API_KEY\"),\n )\n return OpenAICompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"anthropic\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_ANTHROPIC_API_BASE\", (\"ANTHROPIC_API_BASE\",), DEFAULT_ANTHROPIC_BASE_URL)\n api_key = provider_env(\n \"PICO_ANTHROPIC_API_KEY\",\n (\"ANTHROPIC_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_OPENAI_API_KEY\", \"OPENAI_API_KEY\"),\n )\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"deepseek\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_DEEPSEEK_API_BASE\", (\"DEEPSEEK_API_BASE\",), DEFAULT_DEEPSEEK_BASE_URL)\n api_key = provider_env(\"PICO_DEEPSEEK_API_KEY\", (\"DEEPSEEK_API_KEY\",))\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n\n model = _effective_model(args, provider)\n host = getattr(args, \"host\", DEFAULT_OLLAMA_HOST)\n return OllamaModelClient(\n model=model,\n host=host,\n temperature=args.temperature,\n top_p=args.top_p,\n timeout=args.ollama_timeout,\n )\n\n\ndef build_welcome(agent, model, host):\n width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))\n inner = width - 4\n gap = 3\n left_width = (inner - gap) // 2\n right_width = inner - gap - left_width\n\n def row(text):\n body = _terminal_middle(text, width - 4)\n return _border_row(_terminal_ljust(body, width - 4))\n\n def divider(style=\"solid\"):\n char = \"═\" if style == \"strong\" else \"─\"\n left = \"╔\" if style == \"strong\" else \"├\"\n right = \"╗\" if style == \"strong\" else \"┤\"\n return _color(left + char * (width - 2) + right, WELCOME_BORDER_COLOR)\n\n def bottom_divider():\n return _color(\"╚\" + \"═\" * (width - 2) + \"╝\", WELCOME_BORDER_COLOR)\n\n def _border_row(body):\n return f\"{_color('║', WELCOME_BORDER_COLOR)} {body} {_color('║', WELCOME_BORDER_COLOR)}\"\n\n def center(text):\n body = _terminal_middle(text, inner)\n return _border_row(_terminal_center(body, inner))\n\n def cell(label, value, size):\n body = _terminal_middle(f\"{label:<9} {value}\", size)\n return _terminal_ljust(body, size)\n\n def pair(left_label, left_value, right_label, right_value):\n left = cell(left_label, left_value, left_width)\n right = cell(right_label, right_value, right_width)\n return _border_row(f\"{left}{' ' * gap}{right}\")\n\n line = divider(\"strong\")\n rows = [center(_gradient_text(text)) for text in WELCOME_ART]\n rows.extend(\n [\n center(WELCOME_NAME),\n center(WELCOME_SUBTITLE),\n # center(WELCOME_STATUS),\n divider(),\n row(\"\"),\n row(\"WORKSPACE \" + _terminal_middle(agent.workspace.cwd, inner - 11)),\n pair(\"MODEL\", model, \"BRANCH\", agent.workspace.branch),\n pair(\"APPROVAL\", agent.approval_policy, \"SESSION\", agent.session[\"id\"]),\n row(\"\"),\n ]\n )\n return \"\\n\".join([line, *rows, bottom_divider()])\n\n\ndef build_agent(args):\n \"\"\"根据 CLI 参数装配出一个可运行的 Pico 实例。\n\n 为什么存在:\n 命令行参数只是字符串和开关,runtime 需要的是已经装配好的对象图:\n model client、workspace snapshot、session store、secret 配置等。\n 这个函数负责把“启动参数”翻译成“agent 运行现场”。\n\n 输入 / 输出:\n - 输入:`argparse` 解析后的 `args`\n - 输出:一个新的 `Pico`,或一个从旧 session 恢复出来的 `Pico`\n\n 在 agent 链路里的位置:\n 它是整个程序启动链路里最靠近 runtime 的装配点。`main()` 先调它,\n 得到 agent 后,后面无论是 one-shot 还是 REPL 模式,都会落到 `ask()`。\n \"\"\"\n # 这里是 CLI 到 runtime 的装配点:\n # 先采集工作区快照和加载项目级环境,再整理 secret 名单、模型后端和 session。\n workspace = WorkspaceContext.build(args.cwd)\n # 加载项目级环境变量,覆盖系统环境。这些环境变量可能会被后续的模型调用用到,\n load_project_env(workspace.repo_root)\n# “整理出一份最终要被当成敏感信息处理的环境变量名列表。”\n# 也就是说,后面程序在写 trace、report、session 之类内容时,看到这些环境变量名,就会把它们脱敏,不直接暴露真实值。\n configured_secret_names = _configured_secret_names(args)\n store = SessionStore(Path(workspace.repo_root) / AGENT_STATE_DIR / \"sessions\")\n # return OpenAICompatibleModelClient\n model = _build_model_client(args)\n session_id = args.resume\n if session_id == \"latest\":\n session_id = store.latest()\n if session_id:\n return Pico.from_session(\n model_client=model,\n workspace=workspace,\n session_store=store,\n session_id=session_id,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n )\n return Pico(\n model_client=model,\n workspace=workspace,\n session_store=store,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n )\n\n\ndef build_arg_parser():\n parser = argparse.ArgumentParser(\n formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n description=\"Minimal coding agent for DeepSeek, OpenAI-compatible, Anthropic-compatible, or Ollama models.\",\n )\n parser.add_argument(\"prompt\", nargs=\"*\", help=\"Optional one-shot prompt.\")\n parser.add_argument(\"--cwd\", default=\".\", help=\"Workspace directory.\")\n parser.add_argument(\"--provider\", choices=(\"ollama\", \"openai\", \"anthropic\", \"deepseek\"), default=\"openai\", help=\"Model backend to use.\")\n parser.add_argument(\n \"--model\",\n default=None,\n help=\"Model name override. Defaults to qwen3.5:4b for Ollama, PICO_OPENAI_MODEL for openai, PICO_ANTHROPIC_MODEL for anthropic, and PICO_DEEPSEEK_MODEL for deepseek when set.\",\n )\n parser.add_argument(\"--host\", default=DEFAULT_OLLAMA_HOST, help=\"Ollama server URL.\")\n parser.add_argument(\"--base-url\", default=None, help=\"Provider API base URL for deepseek, openai, or anthropic.\")\n parser.add_argument(\"--ollama-timeout\", type=int, default=300, help=\"Ollama request timeout in seconds.\")\n parser.add_argument(\"--openai-timeout\", type=int, default=300, help=\"OpenAI-compatible request timeout in seconds.\")\n parser.add_argument(\"--resume\", default=None, help=\"Session id to resume or 'latest'.\")\n parser.add_argument(\"--approval\", choices=(\"ask\", \"auto\", \"never\"), default=\"ask\", help=\"Approval policy for risky tools.\")\n parser.add_argument(\n \"--secret-env-name\",\n dest=\"secret_env_names\",\n action=\"append\",\n default=[],\n help=\"Extra environment variable names to treat as secrets for trace/report redaction.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=6, help=\"Maximum tool/model iterations per request.\")\n parser.add_argument(\"--max-new-tokens\", type=int, default=8192, help=\"Maximum model output tokens per step.\")\n parser.add_argument(\"--temperature\", type=float, default=0.2, help=\"Sampling temperature sent to Ollama.\")\n parser.add_argument(\"--top-p\", type=float, default=0.9, help=\"Top-p sampling value sent to Ollama.\")\n return parser\n\n\ndef main(argv=None):\n args = build_arg_parser().parse_args(argv)\n agent = build_agent(args)\n # 欢迎页里显示的 MODEL gpt-5.4\n model = getattr(agent.model_client, \"model\", getattr(args, \"model\", DEFAULT_OLLAMA_MODEL))\n # base_url 或 host 都行,优先级:model_client 里如果有就用它的,否则用 CLI 参数里的,再没有就用默认值。\n host = getattr(agent.model_client, \"host\", getattr(agent.model_client, \"base_url\", getattr(args, \"host\", DEFAULT_OLLAMA_HOST)))\n print(build_welcome(agent, model=model, host=host))\n\n if args.prompt:\n # one-shot 模式:只跑一次 ask,不进入 REPL 循环。\n prompt = \" \".join(args.prompt).strip()\n if prompt:\n print()\n try:\n print(agent.ask(prompt))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n return 1\n return 0\n\n while True:\n # 交互模式:每次读取一条用户输入,交给同一个 agent,\n # 因此 session history 和 working memory 会跨轮延续。\n try:\n user_input = input(\"\\nLUMO> \").strip()\n except (EOFError, KeyboardInterrupt):\n print(\"\")\n return 0\n\n if not user_input:\n continue\n if user_input in {\"/exit\", \"/quit\"}:\n return 0\n if user_input == \"/help\":\n print(HELP_DETAILS)\n continue\n if user_input == \"/memory\":\n print(agent.memory_text())\n continue\n if user_input == \"/session\":\n print(agent.session_path)\n continue\n if user_input == \"/reset\":\n agent.reset()\n print(\"session reset\")\n continue\n\n print()\n try:\n print(agent.ask(user_input))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n"}, "0bcc23fd636478ce598f7624573f528ad5f73475": {"t": 1785767257.7474551, "n": 15653, "h": "7c6b77a2e268eb8c", "c": "\"\"\"命令行入口。\n\n这个模块负责把“用户怎么启动 Lumo”翻译成 runtime 能理解的对象:\n解析参数、挑模型后端、构建工作区快照、恢复或新建 session,\n最后进入 one-shot 或交互式循环。\n\"\"\"\n\nimport argparse\nimport os\nimport re\nimport shutil\nimport sys\nimport textwrap\nimport unicodedata\nfrom pathlib import Path\n\nfrom .config import load_project_env, provider_env\nfrom .providers.clients import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient\nfrom .runtime import Pico, SessionStore\nfrom .workspace import AGENT_STATE_DIR, WorkspaceContext\n\nDEFAULT_SECRET_ENV_NAMES = (\n \"PICO_OPENAI_API_KEY\",\n \"OPENAI_API_KEY\",\n \"OPENAI_API_TOKEN\",\n \"PICO_ANTHROPIC_API_KEY\",\n \"ANTHROPIC_API_KEY\",\n \"ANTHROPIC_AUTH_TOKEN\",\n \"PICO_DEEPSEEK_API_KEY\",\n \"DEEPSEEK_API_KEY\",\n \"PICO_RIGHT_CODES_API_KEY\",\n \"RIGHT_CODES_API_KEY\",\n \"GITHUB_PAT\",\n \"GH_PAT\",\n)\n\nWELCOME_ART = tuple(\n \" \".join(parts)\n for parts in (\n (\"██ \", \"██ ██\", \"██ ██\", \"███████\"),\n (\"██ \", \"██ ██\", \"███ ███\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"███████\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ █ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"██ \", \"██ ██\", \"██ ██\", \"██ ██\"),\n (\"███████\", \"███████\", \"██ ██\", \"███████\"),\n )\n)\nWELCOME_NAME = \"Lumo\"\nWELCOME_SUBTITLE = \"Code is cheap, Show me your talk\"\n# WELCOME_STATUS = \"calm shell, ready for work\"\nHELP_DETAILS = textwrap.dedent(\n \"\"\"\\\n Commands:\n /help Show this help message.\n /memory Show the agent's distilled working memory.\n /session Show the path to the saved session file.\n /reset Clear the current session history and memory.\n /exit Exit the agent.\n \"\"\"\n).strip()\n\n\nDEFAULT_OLLAMA_MODEL = \"qwen3.5:4b\"\nDEFAULT_OLLAMA_HOST = \"http://127.0.0.1:11434\"\nDEFAULT_OPENAI_MODEL = \"gpt-5.4\"\nDEFAULT_OPENAI_BASE_URL = \"https://www.right.codes/codex/v1\"\nDEFAULT_ANTHROPIC_MODEL = \"claude-sonnet-4-6\"\nDEFAULT_ANTHROPIC_BASE_URL = \"https://www.right.codes/claude/v1\"\nDEFAULT_DEEPSEEK_MODEL = \"deepseek-v4-pro\"\nDEFAULT_DEEPSEEK_BASE_URL = \"https://api.deepseek.com/anthropic\"\nSECRET_ENV_NAMES_VAR = \"PICO_SECRET_ENV_NAMES\"\nANSI_PATTERN = re.compile(r\"\\x1b\\[[0-9;]*m\")\nANSI_RESET = \"\\x1b[0m\"\nWELCOME_BORDER_COLOR = \"\\x1b[38;2;125;211;252m\"\nWELCOME_GRADIENT_START = (255, 255, 255)\nWELCOME_GRADIENT_END = (125, 211, 252)\n\n\ndef _strip_ansi(text):\n return ANSI_PATTERN.sub(\"\", str(text))\n\n\ndef _ansi_rgb(red, green, blue):\n return f\"\\x1b[38;2;{red};{green};{blue}m\"\n\n\ndef _color(text, ansi_color):\n return f\"{ansi_color}{text}{ANSI_RESET}\"\n\n\ndef _gradient_text(text, start_rgb=WELCOME_GRADIENT_START, end_rgb=WELCOME_GRADIENT_END):\n text = str(text)\n span = max(1, len(text) - 1)\n parts = []\n for index, char in enumerate(text):\n if char == \" \":\n parts.append(char)\n continue\n ratio = index / span\n rgb = tuple(round(start + (end - start) * ratio) for start, end in zip(start_rgb, end_rgb))\n parts.append(_color(char, _ansi_rgb(*rgb)))\n return \"\".join(parts)\n\n\ndef _terminal_char_width(char):\n return 2 if unicodedata.east_asian_width(char) in (\"F\", \"W\") else 1\n\n\ndef _terminal_width(text):\n return sum(_terminal_char_width(char) for char in _strip_ansi(text))\n\n\ndef _terminal_ljust(text, width):\n text = str(text)\n return text + \" \" * max(0, width - _terminal_width(text))\n\n\ndef _terminal_center(text, width):\n text = str(text)\n padding = max(0, width - _terminal_width(text))\n left = padding // 2\n right = padding - left\n return \" \" * left + text + \" \" * right\n\n\ndef _take_terminal_width(text, width, from_end=False):\n chars = reversed(str(text)) if from_end else str(text)\n selected = []\n used = 0\n for char in chars:\n char_width = _terminal_char_width(char)\n if used + char_width > width:\n break\n selected.append(char)\n used += char_width\n if from_end:\n selected.reverse()\n return \"\".join(selected)\n\n\ndef _terminal_middle(text, limit):\n text = str(text).replace(\"\\n\", \" \")\n if _terminal_width(text) <= limit:\n return text\n if limit <= 3:\n return _take_terminal_width(text, limit)\n left = (limit - 3) // 2\n right = limit - 3 - left\n return _take_terminal_width(text, left) + \"...\" + _take_terminal_width(text, right, from_end=True)\n\n\ndef _effective_model(args, provider):\n # 模型选择优先级:\n # 1. 用户显式传入 --model\n # 2. provider 对应的环境变量\n # 3. 代码里的默认值\n explicit_model = getattr(args, \"model\", None)\n if explicit_model:\n return explicit_model\n if provider == \"openai\":\n model = provider_env(\"PICO_OPENAI_MODEL\", (\"OPENAI_MODEL\",))\n if model:\n return model\n return DEFAULT_OPENAI_MODEL\n if provider == \"anthropic\":\n model = provider_env(\"PICO_ANTHROPIC_MODEL\", (\"ANTHROPIC_MODEL\",))\n if model:\n return model\n return DEFAULT_ANTHROPIC_MODEL\n if provider == \"deepseek\":\n model = provider_env(\"PICO_DEEPSEEK_MODEL\", (\"DEEPSEEK_MODEL\",))\n if model:\n return model\n return DEFAULT_DEEPSEEK_MODEL\n return DEFAULT_OLLAMA_MODEL\n\n\ndef _configured_secret_names(args):\n configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)\n configured_secret_names.update(str(name).upper() for name in args.secret_env_names)\n extra_names = os.environ.get(SECRET_ENV_NAMES_VAR, \"\")\n if extra_names.strip():\n configured_secret_names.update(\n item.strip().upper()\n for item in extra_names.split(\",\")\n if item.strip()\n )\n return sorted(configured_secret_names)\n\n\ndef _build_model_client(args):\n provider = getattr(args, \"provider\", \"deepseek\")\n # CLI 只负责把 provider 选择翻译成具体 client。\n # 真正的提示词格式、缓存支持、HTTP 协议差异,都封装在 models.py 里。\n if provider == \"openai\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_OPENAI_API_BASE\", (\"OPENAI_API_BASE\",), DEFAULT_OPENAI_BASE_URL)\n api_key = provider_env(\n \"PICO_OPENAI_API_KEY\",\n (\"OPENAI_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_ANTHROPIC_API_KEY\", \"ANTHROPIC_API_KEY\"),\n )\n return OpenAICompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"anthropic\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_ANTHROPIC_API_BASE\", (\"ANTHROPIC_API_BASE\",), DEFAULT_ANTHROPIC_BASE_URL)\n api_key = provider_env(\n \"PICO_ANTHROPIC_API_KEY\",\n (\"ANTHROPIC_API_KEY\", \"PICO_RIGHT_CODES_API_KEY\", \"RIGHT_CODES_API_KEY\", \"PICO_OPENAI_API_KEY\", \"OPENAI_API_KEY\"),\n )\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n if provider == \"deepseek\":\n model = _effective_model(args, provider)\n base_url = getattr(args, \"base_url\", None) or provider_env(\"PICO_DEEPSEEK_API_BASE\", (\"DEEPSEEK_API_BASE\",), DEFAULT_DEEPSEEK_BASE_URL)\n api_key = provider_env(\"PICO_DEEPSEEK_API_KEY\", (\"DEEPSEEK_API_KEY\",))\n return AnthropicCompatibleModelClient(\n model=model,\n base_url=base_url,\n api_key=api_key,\n temperature=args.temperature,\n timeout=getattr(args, \"openai_timeout\", getattr(args, \"ollama_timeout\", 300)),\n )\n\n model = _effective_model(args, provider)\n host = getattr(args, \"host\", DEFAULT_OLLAMA_HOST)\n return OllamaModelClient(\n model=model,\n host=host,\n temperature=args.temperature,\n top_p=args.top_p,\n timeout=args.ollama_timeout,\n )\n\n\ndef build_welcome(agent, model, host):\n width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))\n inner = width - 4\n gap = 3\n left_width = (inner - gap) // 2\n right_width = inner - gap - left_width\n\n def row(text):\n body = _terminal_middle(text, width - 4)\n return _border_row(_terminal_ljust(body, width - 4))\n\n def divider(style=\"solid\"):\n char = \"═\" if style == \"strong\" else \"─\"\n left = \"╔\" if style == \"strong\" else \"├\"\n right = \"╗\" if style == \"strong\" else \"┤\"\n return _color(left + char * (width - 2) + right, WELCOME_BORDER_COLOR)\n\n def bottom_divider():\n return _color(\"╚\" + \"═\" * (width - 2) + \"╝\", WELCOME_BORDER_COLOR)\n\n def _border_row(body):\n return f\"{_color('║', WELCOME_BORDER_COLOR)} {body} {_color('║', WELCOME_BORDER_COLOR)}\"\n\n def center(text):\n body = _terminal_middle(text, inner)\n return _border_row(_terminal_center(body, inner))\n\n def cell(label, value, size):\n body = _terminal_middle(f\"{label:<9} {value}\", size)\n return _terminal_ljust(body, size)\n\n def pair(left_label, left_value, right_label, right_value):\n left = cell(left_label, left_value, left_width)\n right = cell(right_label, right_value, right_width)\n return _border_row(f\"{left}{' ' * gap}{right}\")\n\n line = divider(\"strong\")\n rows = [center(_gradient_text(text)) for text in WELCOME_ART]\n rows.extend(\n [\n center(WELCOME_NAME),\n center(WELCOME_SUBTITLE),\n # center(WELCOME_STATUS),\n divider(),\n row(\"\"),\n row(\"WORKSPACE \" + _terminal_middle(agent.workspace.cwd, inner - 11)),\n pair(\"MODEL\", model, \"BRANCH\", agent.workspace.branch),\n pair(\"APPROVAL\", agent.approval_policy, \"SESSION\", agent.session[\"id\"]),\n row(\"\"),\n ]\n )\n return \"\\n\".join([line, *rows, bottom_divider()])\n\n\ndef build_agent(args):\n \"\"\"根据 CLI 参数装配出一个可运行的 Pico 实例。\n\n 为什么存在:\n 命令行参数只是字符串和开关,runtime 需要的是已经装配好的对象图:\n model client、workspace snapshot、session store、secret 配置等。\n 这个函数负责把“启动参数”翻译成“agent 运行现场”。\n\n 输入 / 输出:\n - 输入:`argparse` 解析后的 `args`\n - 输出:一个新的 `Pico`,或一个从旧 session 恢复出来的 `Pico`\n\n 在 agent 链路里的位置:\n 它是整个程序启动链路里最靠近 runtime 的装配点。`main()` 先调它,\n 得到 agent 后,后面无论是 one-shot 还是 REPL 模式,都会落到 `ask()`。\n \"\"\"\n # 这里是 CLI 到 runtime 的装配点:\n # 先采集工作区快照和加载项目级环境,再整理 secret 名单、模型后端和 session。\n workspace = WorkspaceContext.build(args.cwd)\n # 加载项目级环境变量,覆盖系统环境。这些环境变量可能会被后续的模型调用用到,\n load_project_env(workspace.repo_root)\n# “整理出一份最终要被当成敏感信息处理的环境变量名列表。”\n# 也就是说,后面程序在写 trace、report、session 之类内容时,看到这些环境变量名,就会把它们脱敏,不直接暴露真实值。\n configured_secret_names = _configured_secret_names(args)\n store = SessionStore(Path(workspace.repo_root) / AGENT_STATE_DIR / \"sessions\")\n # return OpenAICompatibleModelClient\n model = _build_model_client(args)\n session_id = args.resume\n if session_id == \"latest\":\n session_id = store.latest()\n if session_id:\n return Pico.from_session(\n model_client=model,\n workspace=workspace,\n session_store=store,\n session_id=session_id,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n )\n return Pico(\n model_client=model,\n workspace=workspace,\n session_store=store,\n approval_policy=args.approval,\n max_steps=args.max_steps,\n max_new_tokens=args.max_new_tokens,\n secret_env_names=configured_secret_names,\n )\n\n\ndef build_arg_parser():\n parser = argparse.ArgumentParser(\n formatter_class=argparse.ArgumentDefaultsHelpFormatter,\n description=\"Minimal coding agent for DeepSeek, OpenAI-compatible, Anthropic-compatible, or Ollama models.\",\n )\n parser.add_argument(\"prompt\", nargs=\"*\", help=\"Optional one-shot prompt.\")\n parser.add_argument(\"--cwd\", default=\".\", help=\"Workspace directory.\")\n parser.add_argument(\"--provider\", choices=(\"ollama\", \"openai\", \"anthropic\", \"deepseek\"), default=\"openai\", help=\"Model backend to use.\")\n parser.add_argument(\n \"--model\",\n default=None,\n help=\"Model name override. Defaults to qwen3.5:4b for Ollama, PICO_OPENAI_MODEL for openai, PICO_ANTHROPIC_MODEL for anthropic, and PICO_DEEPSEEK_MODEL for deepseek when set.\",\n )\n parser.add_argument(\"--host\", default=DEFAULT_OLLAMA_HOST, help=\"Ollama server URL.\")\n parser.add_argument(\"--base-url\", default=None, help=\"Provider API base URL for deepseek, openai, or anthropic.\")\n parser.add_argument(\"--ollama-timeout\", type=int, default=300, help=\"Ollama request timeout in seconds.\")\n parser.add_argument(\"--openai-timeout\", type=int, default=300, help=\"OpenAI-compatible request timeout in seconds.\")\n parser.add_argument(\"--resume\", default=None, help=\"Session id to resume or 'latest'.\")\n parser.add_argument(\"--approval\", choices=(\"ask\", \"auto\", \"never\"), default=\"ask\", help=\"Approval policy for risky tools.\")\n parser.add_argument(\n \"--secret-env-name\",\n dest=\"secret_env_names\",\n action=\"append\",\n default=[],\n help=\"Extra environment variable names to treat as secrets for trace/report redaction.\",\n )\n parser.add_argument(\"--max-steps\", type=int, default=6, help=\"Maximum tool/model iterations per request.\")\n parser.add_argument(\"--max-new-tokens\", type=int, default=512, help=\"Maximum model output tokens per step.\")\n parser.add_argument(\"--temperature\", type=float, default=0.2, help=\"Sampling temperature sent to Ollama.\")\n parser.add_argument(\"--top-p\", type=float, default=0.9, help=\"Top-p sampling value sent to Ollama.\")\n return parser\n\n\ndef main(argv=None):\n args = build_arg_parser().parse_args(argv)\n agent = build_agent(args)\n # 欢迎页里显示的 MODEL gpt-5.4\n model = getattr(agent.model_client, \"model\", getattr(args, \"model\", DEFAULT_OLLAMA_MODEL))\n # base_url 或 host 都行,优先级:model_client 里如果有就用它的,否则用 CLI 参数里的,再没有就用默认值。\n host = getattr(agent.model_client, \"host\", getattr(agent.model_client, \"base_url\", getattr(args, \"host\", DEFAULT_OLLAMA_HOST)))\n print(build_welcome(agent, model=model, host=host))\n\n if args.prompt:\n # one-shot 模式:只跑一次 ask,不进入 REPL 循环。\n prompt = \" \".join(args.prompt).strip()\n if prompt:\n print()\n try:\n print(agent.ask(prompt))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n return 1\n return 0\n\n while True:\n # 交互模式:每次读取一条用户输入,交给同一个 agent,\n # 因此 session history 和 working memory 会跨轮延续。\n try:\n user_input = input(\"\\nLUMO> \").strip()\n except (EOFError, KeyboardInterrupt):\n print(\"\")\n return 0\n\n if not user_input:\n continue\n if user_input in {\"/exit\", \"/quit\"}:\n return 0\n if user_input == \"/help\":\n print(HELP_DETAILS)\n continue\n if user_input == \"/memory\":\n print(agent.memory_text())\n continue\n if user_input == \"/session\":\n print(agent.session_path)\n continue\n if user_input == \"/reset\":\n agent.reset()\n print(\"session reset\")\n continue\n\n print()\n try:\n print(agent.ask(user_input))\n except RuntimeError as exc:\n print(str(exc), file=sys.stderr)\n"}}}}