{"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"}}}}