Python
"""命令行入口。
这个模块负责把"用户怎么启动 pico"翻译成 runtime 能理解的对象:
解析参数、挑模型后端、构建工作区快照、恢复或新建 session,
最后进入 one-shot 或交互式循环。
"""
import argparse
import os
import shutil
import sys
import textwrap
from .config import load_project_env, provider_env
from .models import AnthropicCompatibleModelClient, OllamaModelClient, OpenAICompatibleModelClient
from .runtime import Pico, SessionStore
from .workspace import WorkspaceContext, middle
DEFAULT_SECRET_ENV_NAMES = (
"PICO_OPENAI_API_KEY",
"OPENAI_API_KEY",
"OPENAI_API_TOKEN",
"PICO_ANTHROPIC_API_KEY",
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
"PICO_DEEPSEEK_API_KEY",
"DEEPSEEK_API_KEY",
"PICO_RIGHT_CODES_API_KEY",
"RIGHT_CODES_API_KEY",
"GITHUB_PAT",
"GH_PAT",
)
WELCOME_ART = (
" /\\___/\\\\",
" ( o o )",
" / ^ \\\\",
" /| |\\\\",
)
WELCOME_NAME = "pico"
WELCOME_SUBTITLE = "local coding agent"
WELCOME_STATUS = "calm shell, ready for work"
HELP_DETAILS = textwrap.dedent(
"""\
Commands:
/help Show this help message.
/memory Show the agent's distilled working memory.
/session Show the path to the saved session file.
/reset Clear the current session history and memory.
/exit Exit the agent.
"""
).strip()
DEFAULT_OLLAMA_MODEL = "qwen3.5:4b"
DEFAULT_OLLAMA_HOST = "http://127.0.0.1:11434"
DEFAULT_OPENAI_MODEL = "gpt-5.4"
DEFAULT_OPENAI_BASE_URL = "https://www.right.codes/codex/v1"
DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-6"
DEFAULT_ANTHROPIC_BASE_URL = "https://www.right.codes/claude/v1"
DEFAULT_DEEPSEEK_MODEL = "deepseek-v4-pro"
DEFAULT_DEEPSEEK_BASE_URL = "https://api.deepseek.com/anthropic"
LEGACY_SECRET_ENV_NAMES_VAR = "MINI_CODING_AGENT_SECRET_ENV_NAMES"
SECRET_ENV_NAMES_VAR = "PICO_SECRET_ENV_NAMES"
def _effective_model(args, provider):
# 模型选择优先级:
# 1. 用户显式传入 --model
# 2. provider 对应的环境变量
# 3. 代码里的默认值
explicit_model = getattr(args, "model", None)
if explicit_model:
return explicit_model
if provider == "openai":
model = provider_env("PICO_OPENAI_MODEL", ("OPENAI_MODEL",))
if model:
return model
return DEFAULT_OPENAI_MODEL
if provider == "anthropic":
model = provider_env("PICO_ANTHROPIC_MODEL", ("ANTHROPIC_MODEL",))
if model:
return model
return DEFAULT_ANTHROPIC_MODEL
if provider == "deepseek":
model = provider_env("PICO_DEEPSEEK_MODEL", ("DEEPSEEK_MODEL",))
if model:
return model
return DEFAULT_DEEPSEEK_MODEL
return DEFAULT_OLLAMA_MODEL
def _configured_secret_names(args):
configured_secret_names = set(DEFAULT_SECRET_ENV_NAMES)
configured_secret_names.update(str(name).upper() for name in args.secret_env_names)
extra_names = os.environ.get(SECRET_ENV_NAMES_VAR, "")
if not extra_names.strip():
extra_names = os.environ.get(LEGACY_SECRET_ENV_NAMES_VAR, "")
if extra_names.strip():
configured_secret_names.update(
item.strip().upper()
for item in extra_names.split(",")
if item.strip()
)
return sorted(configured_secret_names)
def _build_model_client(args):
provider = getattr(args, "provider", "openai")
# CLI 只负责把 provider 选择翻译成具体 client。
# 真正的提示词格式、缓存支持、HTTP 协议差异,都封装在 models.py 里。
if provider == "openai":
model = _effective_model(args, provider)
base_url = getattr(args, "base_url", None) or provider_env("PICO_OPENAI_API_BASE", ("OPENAI_API_BASE",), DEFAULT_OPENAI_BASE_URL)
api_key = provider_env("PICO_OPENAI_API_KEY", ("OPENAI_API_KEY",))
return OpenAICompatibleModelClient(
model=model,
base_url=base_url,
api_key=api_key,
temperature=args.temperature,
timeout=getattr(args, "openai_timeout", getattr(args, "ollama_timeout", 300)),
)
if provider == "anthropic":
model = _effective_model(args, provider)
base_url = getattr(args, "base_url", None) or provider_env("PICO_ANTHROPIC_API_BASE", ("ANTHROPIC_API_BASE",), DEFAULT_ANTHROPIC_BASE_URL)
api_key = provider_env(
"PICO_ANTHROPIC_API_KEY",
("ANTHROPIC_API_KEY", "PICO_RIGHT_CODES_API_KEY", "RIGHT_CODES_API_KEY", "PICO_OPENAI_API_KEY", "OPENAI_API_KEY"),
)
return AnthropicCompatibleModelClient(
model=model,
base_url=base_url,
api_key=api_key,
temperature=args.temperature,
timeout=getattr(args, "openai_timeout", getattr(args, "ollama_timeout", 300)),
)
if provider == "deepseek":
model = _effective_model(args, provider)
base_url = getattr(args, "base_url", None) or provider_env("PICO_DEEPSEEK_API_BASE", ("DEEPSEEK_API_BASE",), DEFAULT_DEEPSEEK_BASE_URL)
api_key = provider_env("PICO_DEEPSEEK_API_KEY", ("DEEPSEEK_API_KEY",))
return AnthropicCompatibleModelClient(
model=model,
base_url=base_url,
api_key=api_key,
temperature=args.temperature,
timeout=getattr(args, "openai_timeout", getattr(args, "ollama_timeout", 300)),
)
model = _effective_model(args, provider)
host = getattr(args, "host", DEFAULT_OLLAMA_HOST)
return OllamaModelClient(
model=model,
host=host,
temperature=args.temperature,
top_p=args.top_p,
timeout=args.ollama_timeout,
)
def build_welcome(agent, model, host):
width = max(68, min(shutil.get_terminal_size((80, 20)).columns, 84))
inner = width - 4
gap = 3
left_width = (inner - gap) // 2
right_width = inner - gap - left_width
def row(text):
body = middle(text, width - 4)
return f"| {body.ljust(width - 4)} |"
def divider(char="-"):
return "+" + char * (width - 2) + "+"
def center(text):
body = middle(text, inner)
return f"| {body.center(inner)} |"
def cell(label, value, size):
body = middle(f"{label:<9} {value}", size)
return body.ljust(size)
def pair(left_label, left_value, right_label, right_value):
left = cell(left_label, left_value, left_width)
right = cell(right_label, right_value, right_width)
return f"| {left}{' ' * gap}{right} |"
line = divider("=")
rows = [center(text) for text in WELCOME_ART]
rows.extend(
[
center(WELCOME_NAME),
center(WELCOME_SUBTITLE),
center(WELCOME_STATUS),
divider("-"),
row(""),
row("WORKSPACE " + middle(agent.workspace.cwd, inner - 11)),
pair("MODEL", model, "BRANCH", agent.workspace.branch),
pair("APPROVAL", agent.approval_policy, "SESSION", agent.session["id"]),
row(""),
]
)
return "\n".join([line, *rows, line])
def build_agent(args):
"""根据 CLI 参数装配出一个可运行的 Pico 实例。
为什么存在:
命令行参数只是字符串和开关,runtime 需要的是已经装配好的对象图:
model client、workspace snapshot、session store、secret 配置等。
这个函数负责把"启动参数"翻译成"agent 运行现场"。
输入 / 输出:
- 输入:`argparse` 解析后的 `args`
- 输出:一个新的 `Pico`,或一个从旧 session 恢复出来的 `Pico`
在 agent 链路里的位置:
它是整个程序启动链路里最靠近 runtime 的装配点。`main()` 先调它,
得到 agent 后,后面无论是 one-shot 还是 REPL 模式,都会落到 `ask()`。
"""
# 这里是 CLI 到 runtime 的装配点:
# 先采集工作区快照和加载项目级环境,再整理 secret 名单、模型后端和 session。
workspace = WorkspaceContext.build(args.cwd)
load_project_env(workspace.repo_root)
configured_secret_names = _configured_secret_names(args)
store = SessionStore(workspace.repo_root + "/.pico/sessions")
model = _build_model_client(args)
session_id = args.resume
if session_id == "latest":
session_id = store.latest()
if session_id:
return Pico.from_session(
model_client=model,
workspace=workspace,
session_store=store,
session_id=session_id,
approval_policy=args.approval,
max_steps=args.max_steps,
max_new_tokens=args.max_new_tokens,
secret_env_names=configured_secret_names,
)
return Pico(
model_client=model,
workspace=workspace,
session_store=store,
approval_policy=args.approval,
max_steps=args.max_steps,
max_new_tokens=args.max_new_tokens,
secret_env_names=configured_secret_names,
)
def build_arg_parser():
parser = argparse.ArgumentParser(
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="Minimal coding agent for Ollama, OpenAI-compatible, Anthropic-compatible, or DeepSeek models.",
)
parser.add_argument("prompt", nargs="*", help="Optional one-shot prompt.")
parser.add_argument("--cwd", default=".", help="Workspace directory.")
parser.add_argument("--provider", choices=("ollama", "openai", "anthropic", "deepseek"), default="openai", help="Model backend to use.")
parser.add_argument(
"--model",
default=None,
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.",
)
parser.add_argument("--host", default=DEFAULT_OLLAMA_HOST, help="Ollama server URL.")
parser.add_argument("--base-url", default=None, help="Provider API base URL for openai, anthropic, or deepseek.")
parser.add_argument("--ollama-timeout", type=int, default=300, help="Ollama request timeout in seconds.")
parser.add_argument("--openai-timeout", type=int, default=300, help="OpenAI-compatible request timeout in seconds.")
parser.add_argument("--resume", default=None, help="Session id to resume or 'latest'.")
parser.add_argument("--approval", choices=("ask", "auto", "never"), default="ask", help="Approval policy for risky tools.")
parser.add_argument(
"--secret-env-name",
dest="secret_env_names",
action="append",
default=[],
help="Extra environment variable names to treat as secrets for trace/report redaction.",
)
parser.add_argument("--max-steps", type=int, default=6, help="Maximum tool/model iterations per request.")
parser.add_argument("--max-new-tokens", type=int, default=512, help="Maximum model output tokens per step.")
parser.add_argument("--temperature", type=float, default=0.2, help="Sampling temperature sent to Ollama.")
parser.add_argument("--top-p", type=float, default=0.9, help="Top-p sampling value sent to Ollama.")
return parser
def main(argv=None):
args = build_arg_parser().parse_args(argv)
agent = build_agent(args)
model = getattr(agent.model_client, "model", getattr(args, "model", DEFAULT_OLLAMA_MODEL))
host = getattr(agent.model_client, "host", getattr(agent.model_client, "base_url", getattr(args, "host", DEFAULT_OLLAMA_HOST)))
print(build_welcome(agent, model=model, host=host))
if args.prompt:
# one-shot 模式:只跑一次 ask,不进入 REPL 循环。
prompt = " ".join(args.prompt).strip()
if prompt:
print()
try:
print(agent.ask(prompt))
except RuntimeError as exc:
print(str(exc), file=sys.stderr)
return 1
return 0
while True:
# 交互模式:每次读取一条用户输入,交给同一个 agent,
# 因此 session history 和 working memory 会跨轮延续。
try:
user_input = input("\npico> ").strip()
except (EOFError, KeyboardInterrupt):
print("")
return 0
if not user_input:
continue
if user_input in {"/exit", "/quit"}:
return 0
if user_input == "/help":
print(HELP_DETAILS)
continue
if user_input == "/memory":
print(agent.memory_text())
continue
if user_input == "/session":
print(agent.session_path)
continue
if user_input == "/reset":
agent.reset()
print("session reset")
continue
print()
try:
print(agent.ask(user_input))
except RuntimeError as exc:
print(str(exc), file=sys.stderr)
【这个模块解决什么问题】
cli.py 解决的是:
用户在命令行里怎么启动 Pico,以及这些启动参数怎么变成一个真正能运行的
Pico agent。
它不负责真正的 agent 主循环。真正主循环在 runtime.py 的 Pico.ask()。
cli.py 主要负责六件事:
markdown
1. 解析命令行参数
2. 确定工作目录 cwd
3. 创建 WorkspaceContext
4. 创建 model client
5. 处理 session / resume
6. 选择 one-shot 或 REPL 模式,然后调用 agent.ask()
所以你要把 cli.py 理解成:
Pico 的启动装配层。
【它在 Pico 主链路里的位置】
上一轮我们讲的主链路是:
scss
用户输入请求
→ CLI 解析参数
→ build_agent() 装配 workspace / model / session
→ Pico.ask() 进入主循环
→ 构建 prompt
→ 调用模型
→ parse 模型输出
→ run_tool()
→ 更新 memory / task_state / trace
→ final/report
这一章只覆盖前半段:
scss
用户输入请求
→ CLI 解析参数
→ build_agent() 装配 workspace / model / session
→ 调用 Pico.ask()
也就是说,cli.py 做的是"开机准备",不是"agent 思考"。
【我应该打开哪个文件】
你已经贴了 cli.py,这次重点看这些位置:
scss
1. build_arg_parser()
2. main()
3. build_agent()
4. _build_model_client()
5. _effective_model()
6. _configured_secret_names()
优先级最高的是:
scss
main()
build_agent()
_build_model_client()
这三个函数串起来,就是 Pico CLI 启动链路。
【我应该重点看哪些函数 / 类】
1. build_arg_parser()
它负责声明 Pico 支持哪些命令行参数。
核心参数有:
go
parser.add_argument("prompt", nargs="*", help="Optional one-shot prompt.")
parser.add_argument("--cwd", default=".", help="Workspace directory.")
parser.add_argument("--provider", choices=("ollama", "openai", "anthropic", "deepseek"), default="openai")
parser.add_argument("--model", default=None)
parser.add_argument("--resume", default=None)
parser.add_argument("--approval", choices=("ask", "auto", "never"), default="ask")
parser.add_argument("--max-steps", type=int, default=6)
parser.add_argument("--max-new-tokens", type=int, default=512)
你先别背参数,抓主线:
css
prompt 用户给 agent 的任务
--cwd 让 agent 在哪个目录下工作
--provider 用哪个模型服务
--model 用哪个具体模型
--resume 是否恢复旧 session
--approval 高风险工具怎么审批
--max-steps agent 最多循环几轮
这里最关键的是:
ini
parser.add_argument("prompt", nargs="*")
nargs="*" 的意思是:命令行最后可以接多个词,都会被收集成列表。
比如:
matlab
pico fix README typo
解析后大概是:
ini
args.prompt = ["fix", "README", "typo"]
后面在 main() 里会被拼成:
ini
prompt = " ".join(args.prompt).strip()
也就是:
matlab
fix README typo
所以 prompt 是否存在,决定了 Pico 是 one-shot 还是 REPL。
2. main()
main() 是真正入口。
它的结构非常清楚:
ini
def main(argv=None):
args = build_arg_parser().parse_args(argv)
agent = build_agent(args)
第一句:
scss
args = build_arg_parser().parse_args(argv)
意思是:
把命令行字符串解析成一个
args对象。
比如你运行:
bash
pico --provider openai --cwd ./my_project "帮我看 README"
大概会变成:
ini
args.provider = "openai"
args.cwd = "./my_project"
args.prompt = ["帮我看 README"]
第二句:
ini
agent = build_agent(args)
意思是:
根据这些参数装配出一个真正的
Pico实例。
注意:main() 不自己创建 workspace,不自己创建 model client,也不自己处理 session。它把这些工作交给 build_agent()。
然后打印欢迎界面:
ini
print(build_welcome(agent, model=model, host=host))
这个只是 UI,不是核心逻辑。
接下来出现关键分支。
【代码大概怎么跑】
第一条路线:one-shot 模式
代码是:
css
if args.prompt:
prompt = " ".join(args.prompt).strip()
if prompt:
print(agent.ask(prompt))
return 0
这表示:如果命令行里直接带了任务,就只执行一次 agent.ask()。
比如:
arduino
pico "总结这个项目怎么启动"
流程是:
scss
解析参数
→ build_agent()
→ agent.ask("总结这个项目怎么启动")
→ 打印结果
→ 程序退出
这叫 one-shot。
它适合:
bash
一次性任务
脚本调用
benchmark / eval
CI 里跑 agent
第二条路线:REPL 模式
如果你没有传 prompt,比如:
pico
就会进入:
python
while True:
user_input = input("\npico> ").strip()
这就是交互模式。
它会一直等你输入:
shell
pico> 帮我看 README
pico> 再看一下 setup.py
pico> 继续改一下错误
每次普通输入都会走:
scss
print(agent.ask(user_input))
关键点是:REPL 里用的是同一个 agent。
所以注释里说:
shell
# 因此 session history 和 working memory 会跨轮延续。
这句话很重要。
one-shot 是:
一个 prompt → ask 一次 → 结束
REPL 是:
bash
同一个 agent
→ 多次用户输入
→ 多次 ask()
→ history/memory 延续
【build_agent() 是这个文件的核心】
现在看最关键的函数:
ini
def build_agent(args):
workspace = WorkspaceContext.build(args.cwd)
load_project_env(workspace.repo_root)
configured_secret_names = _configured_secret_names(args)
store = SessionStore(workspace.repo_root + "/.pico/sessions")
model = _build_model_client(args)
session_id = args.resume
这一段就是 CLI 到 runtime 的装配点。
你可以按顺序理解。
1. cwd 是怎么确定的?
参数里有:
arduino
parser.add_argument("--cwd", default=".", help="Workspace directory.")
默认是:
erlang
.
也就是你启动 Pico 时所在的当前目录。
然后这里用它构建 workspace:
ini
workspace = WorkspaceContext.build(args.cwd)
这一步不是简单保存字符串。
它大概率会做这些事:
把 cwd 解析成绝对路径
识别 repo_root
识别 git branch
记录工作区信息
从后面的欢迎界面也能看出来,agent.workspace 至少有:
agent.workspace.cwd
agent.workspace.branch
所以 WorkspaceContext 不是普通路径变量,而是:
Pico 对当前代码工作区的结构化描述。
这对 coding agent 很重要,因为工具读写文件、路径安全、trace/report 都要知道"工作区边界"。
2. 项目环境变量什么时候加载?
紧接着:
scss
load_project_env(workspace.repo_root)
顺序很关键。
它不是一开始就加载环境变量,而是先得到:
workspace.repo_root
然后再去加载项目级 env。
为什么?
因为 .env 通常在项目根目录。
所以链路是:
scss
args.cwd
→ WorkspaceContext.build(args.cwd)
→ 得到 repo_root
→ load_project_env(repo_root)
这说明 Pico 的环境变量不是全局乱找,而是跟当前项目绑定。
3. secret 是怎么配置的?
先看默认名单:
makefile
DEFAULT_SECRET_ENV_NAMES = (
"PICO_OPENAI_API_KEY",
"OPENAI_API_KEY",
...
"GITHUB_PAT",
"GH_PAT",
)
然后:
ini
configured_secret_names = _configured_secret_names(args)
_configured_secret_names() 做三层合并:
diff
默认 secret 名单
+ 命令行 --secret-env-name 传进来的名字
+ 环境变量 PICO_SECRET_ENV_NAMES 里配置的名字
它的目的不是"创建 API key"。
它的目的更像是:
告诉 Pico 哪些环境变量名字是敏感信息,后面写 trace/report 的时候要做脱敏。
这就是 harness 味道。
普通 demo 可能直接把日志打印出来。
Pico 会考虑:
vbnet
工具输出里会不会泄露 token?
trace 里会不会写进 API key?
report 里会不会暴露 secret?
4. SessionStore 是什么时候创建的?
这一句:
ini
store = SessionStore(workspace.repo_root + "/.pico/sessions")
说明 session 放在项目根目录下面:
bash
.pico/sessions
这一步很关键,因为它把 session 和项目绑定了。
不是所有项目共用一个 session 目录,而是:
bash
当前 repo_root/.pico/sessions
所以你在不同项目里启动 Pico,session 存储位置不同。
5. model client 是怎么创建的?
这一句:
ini
model = _build_model_client(args)
进入 _build_model_client()。
它先读:
ini
provider = getattr(args, "provider", "openai")
然后根据 provider 分流:
openai → OpenAICompatibleModelClient
anthropic → AnthropicCompatibleModelClient
deepseek → AnthropicCompatibleModelClient
ollama → OllamaModelClient
这里你要理解一个设计点:
CLI 不直接调用模型 API,它只根据参数创建一个 model client 对象。
为什么?
因为 OpenAI、Anthropic、Ollama、DeepSeek 的 HTTP 协议、base_url、key、参数格式都不一样。
但 runtime 不应该关心这些差异。
runtime 后面只想做:
scss
model_client.generate(...)
或者类似调用。
所以 _build_model_client() 是适配层。
6. 模型名优先级是什么?
看 _effective_model(args, provider)。
它的优先级是:
markdown
1. 用户命令行显式传 --model
2. provider 对应的环境变量
3. 代码默认值
比如 provider 是 openai:
kotlin
if explicit_model:
return explicit_model
model = provider_env("PICO_OPENAI_MODEL", ("OPENAI_MODEL",))
if model:
return model
return DEFAULT_OPENAI_MODEL
这很实用。
你可以这样理解:
css
pico --model gpt-xxx
优先级最高。
如果没传,就看环境变量:
ini
PICO_OPENAI_MODEL=xxx
再没有,才用代码默认:
ini
DEFAULT_OPENAI_MODEL = "gpt-5.4"
7. session / resume 是怎么处理的?
核心代码:
ini
session_id = args.resume
if session_id == "latest":
session_id = store.latest()
if session_id:
return Pico.from_session(...)
return Pico(...)
这里有三种情况。
情况一:不 resume
pico
此时:
ini
args.resume = None
session_id = None
于是走:
kotlin
return Pico(...)
也就是新建一个 Pico 实例。
情况二:恢复指定 session
css
pico --resume abc123
此时:
ini
session_id = "abc123"
于是走:
ini
return Pico.from_session(..., session_id="abc123")
也就是从旧 session 恢复。
情况三:恢复最新 session
css
pico --resume latest
此时:
ini
if session_id == "latest":
session_id = store.latest()
store.latest() 会找到最近的 session id,然后:
scss
Pico.from_session(...)
这说明 resume 不是在 CLI 里手动读 JSON。
CLI 只是告诉 runtime:
我要恢复哪个 session
真正恢复逻辑交给:
scss
Pico.from_session()
这是合理的职责划分。
【build_agent() 最终返回了什么】
build_agent() 最终只返回两种东西之一:
新 agent
ini
return Pico(
model_client=model,
workspace=workspace,
session_store=store,
approval_policy=args.approval,
max_steps=args.max_steps,
max_new_tokens=args.max_new_tokens,
secret_env_names=configured_secret_names,
)
从旧 session 恢复的 agent
ini
return Pico.from_session(
model_client=model,
workspace=workspace,
session_store=store,
session_id=session_id,
approval_policy=args.approval,
max_steps=args.max_steps,
max_new_tokens=args.max_new_tokens,
secret_env_names=configured_secret_names,
)
所以你要记住:
build_agent()返回的不是答案,也不是模型 client,而是一个已经装配好的Picoruntime 对象。
后面无论 one-shot 还是 REPL,都会调用:
scss
agent.ask(...)
这就是 CLI 和 runtime 的连接点。
【用一个例子讲一遍】
假设你运行:
css
pico --cwd ./demo --provider openai --resume latest "帮我总结 README"
这条命令会这样走:
scss
1. main() 开始
2. build_arg_parser().parse_args(argv)
3. 得到 args
大概是:
ini
args.cwd = "./demo"
args.provider = "openai"
args.resume = "latest"
args.prompt = ["帮我总结 README"]
然后:
scss
4. build_agent(args)
里面继续:
scss
WorkspaceContext.build("./demo")
→ 确定 cwd / repo_root / branch
load_project_env(workspace.repo_root)
→ 加载项目级环境变量
_configured_secret_names(args)
→ 整理需要脱敏的 secret 名字
SessionStore(repo_root + "/.pico/sessions")
→ 创建 session 存储器
_build_model_client(args)
→ 创建 OpenAICompatibleModelClient
args.resume == "latest"
→ store.latest()
→ 找到最近 session id
Pico.from_session(...)
→ 恢复旧 agent
然后回到 main():
css
if args.prompt:
prompt = " ".join(args.prompt).strip()
print(agent.ask(prompt))
return 0
所以最后会调用:
arduino
agent.ask("帮我总结 README")
从这里开始,才进入下一章 runtime.py 的主循环。
【one-shot 和 REPL 的区别是什么】
这个你必须吃透。
one-shot
运行方式:
arduino
pico "帮我修复 README"
代码分支:
bash
if args.prompt:
print(agent.ask(prompt))
return 0
特点:
scss
启动一次
执行一次 ask()
打印答案
退出程序
适合:
一次性任务
自动化脚本
评测 benchmark
CI 流程
REPL
运行方式:
pico
代码分支:
css
while True:
user_input = input("\npico> ").strip()
...
print(agent.ask(user_input))
特点:
scss
启动一次
同一个 agent 留在内存里
用户可以多轮输入
每轮都调用 ask()
history / memory 可以延续
而且 REPL 支持内部命令:
bash
/help 查看命令
/memory 查看 working memory
/session 查看 session 路径
/reset 清空当前 session history 和 memory
/exit 退出
这个设计说明 Pico 不是只服务一次模型调用,而是支持一个长期交互式 coding session。
【和 LangChain / LangGraph / Claude Code 的对应关系】
对应 LangChain
cli.py 这一层类似于你写 LangChain demo 前面的初始化代码:
ini
llm = ChatOpenAI(...)
tools = [...]
agent = create_agent(...)
但 Pico 更工程化。
它不只是创建 LLM,还创建:
arduino
workspace
session store
secret redaction config
approval policy
max step limit
model provider adapter
所以它更接近真实 coding agent 的启动器。
对应 LangGraph
LangGraph 里你会先编译 graph:
ini
app = graph.compile(checkpointer=...)
Pico 的 build_agent() 有点像:
把 runtime 所需状态和组件装配好
只不过 Pico 没有显式 graph,而是后面用 Pico.ask() 手写 agent loop。
对应 Claude Code
Claude Code 启动时也要知道:
当前项目目录
当前会话
模型配置
工具权限
是否继续历史会话
Pico 的 cli.py 就是在做一个迷你版 Claude Code 启动层。
你以后面试可以说:
我不是只看模型调用,而是看了 coding agent 从 CLI 到 runtime 的装配过程,包括 workspace、model client、session store 和 approval policy 的初始化。
这句话比"我会调 API"强很多。
【一句面试话术】
cli.py是 Pico 的启动装配层,它把命令行参数解析成 runtime 需要的对象图:先根据--cwd构建WorkspaceContext,再加载项目环境变量、创建模型 client 和SessionStore,根据--resume决定新建或恢复Pico,最后进入 one-shot 或 REPL,并统一通过agent.ask()进入真正的 agent 主循环。
【自测问题】
你先回答这 3 个问题。
1.
为什么 build_agent() 不直接调用模型,而是只返回一个 Pico 对象?
2.
--cwd 为什么不能只是一个普通字符串?为什么要变成 WorkspaceContext?
3.
one-shot 和 REPL 最大的区别是什么?尤其是为什么 REPL 里的 memory/history 可以延续?
【下一步看什么】
等你答完,我会根据你的回答追问或补一轮。
如果你答得可以,下一步我们进入:
bash
pico/runtime.py
重点看:
scss
Pico.ask()
parse()
run_tool()
trace/report/task_state 更新位置