配置固化
这个 Stage 在解决什么问题?它把用户传进来的零散配置,一次性定成一个稳定的 Agent 对象,让后面所有 Stage 都能假设“模型、解析器、模板、计数器、会话字段都已经就位”。它只在 Terminus2 实例创建时跑一次,不处理某个具体任务,而是先把“这台 Agent 机器”装好;如果这一步没定清,后面的 Environment Setup 就不知道该按什么规则建环境。
- 先读构造参数,并把兼容问题在这里一次处理完。
Terminus2.__init__()(创建 Agent 实例)先接住用户给的约 25 个配置项。这里最重要的不是“存起来”,而是先把历史兼容和优先级讲清:比如旧参数episodes和新参数max_episodes同时出现时,到底听谁的。 这一步存在的原因很直接:配置冲突如果拖到运行中才暴露,后面每个 Stage 都要反复防御;在入口处定一次,后面就能按一个口径工作。 - 再把核心依赖真的准备好。
然后它初始化 LLM(大语言模型)客户端,也就是后面真正拿去发请求的 Chat 封装;接着按 parser 名称选 parser(把模型输出转成 Agent 可用结果的解析器)。 这一步的意义是:后续 Stage 不该再关心“模型对象有没有建好”“解析器是哪一种”。它们只该直接使用。 这里也有明确的失败边界:如果模型信息无法解析、LLM 起不来,或 parser 名称不合法,本阶段就直接报错并停下,不进入下游。
- 再把提示词模板的位置定下来。
它会决定 prompt template(提示词模板)和 timeout template(超时场景下用的模板)从哪里读。 为什么这一步要放在这里?因为模板其实也是配置的一部分。先把路径定死,后面运行任务时就不用再猜“该用哪套提示词”,也避免不同任务临时选模板造成行为漂移。
- 最后把所有“每次运行会变”的字段先放回默认值。
比如轨迹步数、待处理 completion、待处理 subagent(子 Agent,被主 Agent 调用的小 Agent)引用、handoff prompt、asciinema 标记、子 Agent 指标、API 请求时间、总结次数、episode 计数、session id。 这些字段虽然是运行期状态,但它们的“初始空白值”必须在实例创建时就声明清楚。这样同一个 Agent 实例在第一次跑任务前,就是一个干净、可预测的起点。
- 成功后,才把这个“已经定型”的对象交给下游。
到这一步,系统拿到的已经不是“用户给的一堆参数”,而是“一个状态完整、依赖齐全、默认值明确的 Agent”。下一个 Stage 可以直接开始建环境,而不用回头处理配置细节。
- 无 register 读写 — 输入说明已明确:本 stage 在 skeleton 中未被任何 register 显式提及,本阶段不产生可验证的 register 流
- 触发下游:
stage-2 Environment Setup— 仅当构造参数归一化完成、LLM/ parser 初始化成功、模板路径确定、运行期字段默认化完成后进入;若本阶段报错,则不进入下游
非寄存器对象字段(可验证的实例状态):
- 写:
_llm— 在实例创建时写入;把用户的模型配置变成可调用的 Chat 客户端,供后续所有 LLM 请求复用 - 读:
_parser_name— 在实例创建时读取;用来决定选哪种 parser - 写: prompt template path(由
_get_prompt_template_path()决定)— 在实例创建时确定;让后续取提示词时有固定位置 - 写: timeout template path(由
_get_timeout_template_path()决定)— 在实例创建时确定;让超时分支也有固定模板 - 写:
max_episodes— 在实例创建时根据episodes/max_episodes的兼容与优先级规则定值;后续运行直接使用统一结果 - 写: 运行期默认字段 — 在实例创建时写入默认空值或初值;目的是保证首次任务运行前对象处于干净起点
上游其实只有一件事:用户传进来的构造参数。这个 Stage 把它们压成稳定的对象状态;下游 Environment Setup 消费的不是原始参数,而是一个已经选好模型、解析器、模板并且带着默认运行字段的 Agent。若这里失败,流水线就在实例创建阶段结束,不会进入环境搭建。
Terminus2.__init__terminus_2.py:144–331 ↗
代理配置冻结与运行态字段初始化入口
stage 上下文: 该条目对应 stage-1 的首个构造单元:把构造参数与少量兼容性 kwargs 解析为实例上的
self._*配置和默认运行态字段。触发条件就是外部直接调用Terminus2(...)。本 stage 目前未给出其他兄弟函数;因此这里承担后续实例方法可依赖的初始配置基线。
这段代码在干什么
Terminus2.__init__ 接收日志目录、模型/解析器/采样/总结/轨迹/tmux/LLM 后端等参数,并把它们固化到实例字段中。函数会先校验 model_name,再解析模型信息、创建 self._llm、选择 self._parser、读取 prompt/timeout 模板文本,然后处理 max_turns 与兼容参数 kwargs['max_episodes']、kwargs['episodes'] 的优先级。其真正产出不是返回值,而是对大量 self._* 字段的写入,以及在显式使用废弃或限流参数时通过 self.logger.warning(...) 发出告警。
接口 · 参数 / IO
(self, logs_dir: Path, model_name: str | None = None, max_turns: int | None = None, parser_name: str = "json", api_base: str | None = None, temperature: float | None = None, reasoning_effort: Literal["none", "minimal", "low", "medium", "high", "xhigh", "max", "default"] | None = None, collect_rollout_details: bool = False, session_id: str | None = None, enable_summarize: bool = True, proactive_summarization_threshold: int = 8000, max_thinking_tokens: int | None = None, model_info: dict | None = None, trajectory_config: TrajectoryConfig | None = None, tmux_pane_width: int = 160, tmux_pane_height: int = 40, store_all_messages: bool = False, record_terminal_session: bool = True, interleaved_thinking: bool = False, suppress_max_turns_warning: bool = False, use_responses_api: bool = False, llm_backend: LLMBackend | str = LLMBackend.LITELLM, llm_kwargs: dict | None = None, llm_call_kwargs: dict[str, Any] | None = None, extra_env: dict[str, str] | None = None, *args, **kwargs)
- 参数:
logs_dir:Path— 传给父类构造函数的日志目录;model_name:str | None— 目标模型名;缺失时直接报错;max_turns:int | None— 新的最大回合/episode 限制入口,优先级最高;parser_name:str— 解析器名称,写入self._parser_name并驱动_get_parser();api_base:str | None— 传给_init_llm的 API 基地址;temperature:float | None— 采样温度;同时写入self._temperature并传给_init_llm;reasoning_effort:Literal[...] | None— 推理力度配置;写入实例并传给_init_llm;collect_rollout_details:bool— 是否采集 rollout 细节;写入实例并传给_init_llm;session_id:str | None— 外部提供的会话标识;若为空则生成 UUID;enable_summarize:bool— 是否启用总结能力,写入self._enable_summarize;proactive_summarization_threshold:int— 主动总结阈值,写入self._proactive_summarization_threshold;max_thinking_tokens:int | None— 传给_init_llm的 thinking token 上限;model_info:dict | None— 可选模型元信息,先经_resolve_model_info处理;trajectory_config:TrajectoryConfig | None— 轨迹配置;为空时退化为{},否则原样保留引用;tmux_pane_width:int— tmux pane 宽度配置;tmux_pane_height:int— tmux pane 高度配置;store_all_messages:bool— 是否保存全部消息到实例配置;record_terminal_session:bool— 是否记录终端会话;interleaved_thinking:bool— 是否启用交错 thinking 历史;suppress_max_turns_warning:bool— 是否抑制显式最大回合限制告警;use_responses_api:bool— 传给_init_llm的 API 选择开关;llm_backend:LLMBackend | str— LLM 后端类型,传给_init_llm;llm_kwargs:dict | None— LLM 构造附加参数;传给_init_llm并保存到self._llm_kwargs;llm_call_kwargs:dict[str, Any] | None— LLM 调用级附加参数;若非空则复制后保存;extra_env:dict[str, str] | None— 额外环境变量映射,保存到self._extra_env;args:tuple— 透传给父类构造函数的附加位置参数;kwargs:dict— 透传给父类且用于兼容读取episodes/max_episodes的附加关键字参数 - 读状态:
self.logger,self._resolve_model_info,self._init_llm,self._get_parser,self._get_prompt_template_path,self._get_timeout_template_path - 返回: 返回
None;真正产出是实例配置、默认运行态字段与模板文本的初始化。 - 副作用: 调用
super().__init__(logs_dir, model_name, *args, **kwargs)初始化父类状态; 当model_name is None时抛出ValueError('model_name is required for Terminus 2'); 写入self._extra_env、self._model_name、self._last_response_model_name、self._parser_name、self._collect_rollout_details、self._reasoning_effort; 调用_resolve_model_info(...)并把结果传入_init_llm(...); 写入self._llm、self._parser、self._prompt_template、self._timeout_template、self._temperature; 根据max_turns/kwargs['max_episodes']/kwargs['episodes']计算并写入self._max_episodes; 在检测到kwargs['episodes']时通过self.logger.warning(...)发出废弃参数告警; 在存在显式最大回合限制且未抑制时通过self.logger.warning(...)发出限制告警; 写入self._chat、self._context、self._timestamped_markers、self._pending_completion、self._session、self._api_request_times、self._n_episodes; 写入self._user_provided_session_id与self._session_id(必要时生成 UUID); 写入self._trajectory_steps、self._record_terminal_session、self._llm_call_kwargs; 写入self._summarization_count、self._pending_subagent_refs、self._pending_handoff_prompt、self._subagent_metrics、self._subagent_rollout_details; 写入self._enable_summarize、self._proactive_summarization_threshold、self._tmux_pane_width、self._tmux_pane_height; 写入self._trajectory_config、self._save_raw_content_in_trajectory、self._linear_history、self._store_all_messages、self._interleaved_thinking、self._llm_kwargs
执行流
- 先调用父类构造函数
super().__init__(logs_dir, model_name, *args, **kwargs),随后保存extra_env,并对model_name做显式空值校验;若为None立即抛出ValueError,不继续初始化。 - 把基础模型相关配置冻结到实例:写入
self._model_name、self._parser_name、self._collect_rollout_details、self._reasoning_effort,并通过_resolve_model_info(model_name, model_info)取得传给 LLM 初始化的模型元信息。 - 调用
_init_llm(...)构造self._llm,参数中纳入模型名、温度、rollout 开关、api_base、session_id、max_thinking_tokens、reasoning_effort、解析后的model_info以及use_responses_api;随后经_get_parser()生成self._parser,并从_get_prompt_template_path()、_get_timeout_template_path()读取模板文本到self._prompt_template与self._timeout_template。 - 处理兼容参数:先读取
kwargs.get('episodes'),若存在则单独发出“已废弃,请改用max_turns”的告警;然后按max_turns>kwargs['max_episodes']>kwargs['episodes']的顺序求出final_max_episodes。 - 若求得显式上限,则在
suppress_max_turns_warning为假时发出“人为限制max_turns/max_episodes”的告警,并写入self._max_episodes;若未提供任何上限,则把self._max_episodes设为1000000。 - 声明运行态默认值:把聊天/上下文/tmux/计数器/轨迹/请求计时等字段初始化为空或零值,包括
self._chat = None、self._context = None、self._timestamped_markers = []、self._pending_completion = False、self._session = None、self._api_request_times = []、self._n_episodes = 0、self._trajectory_steps = []。 - 完成会话与总结相关配置冻结:保留用户传入的
session_id到self._user_provided_session_id,同时把self._session_id设为传入值或新生成 UUID;并初始化self._summarization_count = 0、self._pending_subagent_refs = None、self._pending_handoff_prompt = None、self._subagent_metrics = SubagentMetrics()、self._subagent_rollout_details = [],再写入总结阈值与 tmux 尺寸。 - 最后固化轨迹与调用级配置:
self._trajectory_config = trajectory_config or {},从中读取raw_content与linear_history标志;把llm_call_kwargs在非空时复制为新字典后存入self._llm_call_kwargs,并写入self._store_all_messages、self._interleaved_thinking、self._llm_kwargs。
源码
def __init__(
self,
logs_dir: Path,
model_name: str | None = None,
max_turns: int | None = None,
parser_name: str = "json",
api_base: str | None = None,
temperature: float | None = None,
reasoning_effort: Literal[
"none", "minimal", "low", "medium", "high", "xhigh", "max", "default"
]
| None = None,
collect_rollout_details: bool = False,
session_id: str | None = None,
enable_summarize: bool = True,
proactive_summarization_threshold: int = 8000,
max_thinking_tokens: int | None = None,
model_info: dict | None = None,
trajectory_config: TrajectoryConfig | None = None,
tmux_pane_width: int = 160,
tmux_pane_height: int = 40,
store_all_messages: bool = False,
record_terminal_session: bool = True,
interleaved_thinking: bool = False,
suppress_max_turns_warning: bool = False,
use_responses_api: bool = False,
llm_backend: LLMBackend | str = LLMBackend.LITELLM,
llm_kwargs: dict | None = None,
llm_call_kwargs: dict[str, Any] | None = None,
extra_env: dict[str, str] | None = None,
*args,
**kwargs,
):
"""Initialize Terminus 2 agent.
Args:
logs_dir: Directory to store logs
model_name: Name of the model to use
max_episodes: Maximum number of episodes (default: 1000000)
parser_name: Parser to use - "json" or "xml" (default: "json")
api_base: Base URL for the API endpoint
temperature: Optional sampling temperature. If unset, no temperature is
passed to the LLM backend. (default: None)
reasoning_effort: Qualitative or quantitative measure of effort (default: None)
collect_rollout_details: Whether to collect detailed rollout data including token IDs.
NOTE: Rollout details will be incomplete if context summarization occurs.
See class docstring for details. (default: False)
session_id: Session ID for the agent (default: None)
enable_summarize: Whether to enable context summarization (default: True)
proactive_summarization_threshold: Number of free tokens below which to trigger
proactive summarization. Set to 0 to disable proactive summarization. (default: 8000)
max_thinking_tokens: Maximum thinking tokens for Anthropic extended thinking mode.
Minimum value is 1024. (default: None)
model_info: Optional dict containing model information for custom models.
Used to register the model with litellm. Common fields include:
- max_input_tokens: Maximum input tokens (context length)
- max_output_tokens: Maximum output tokens
- input_cost_per_token: Cost per input token (optional)
- output_cost_per_token: Cost per output token (optional)
(default: None)
trajectory_config: Optional TrajectoryConfig containing trajectory-related configurations.
Available options:
- raw_content (bool): If True, dump raw LLM responses into trajectory without
parsing into tool_calls. Useful for SFT data export. (default: False)
- linear_history (bool): If True, split trajectory into separate files when context
summarization occurs, ensuring each trajectory represents a continuous linear
history sent to the LLM. When False, keep all steps from the main agent
in a single trajectory file despite chat history resets. (default: False)
(default: None)
tmux_pane_width: Starting tmux pane width (maps to `tmux -x`, default: 160)
tmux_pane_height: Starting tmux pane height (maps to `tmux -y`, default: 40)
record_terminal_session: Whether to capture terminal recordings via asciinema.
(default: True)
interleaved_thinking: Whether to include reasoning content in chat history
and send to litellm in next round's conversation (default: False)
suppress_max_turns_warning: Whether to suppress the warning about artificially
limiting max_turns (default: False)
llm_backend: LLM backend to use. Use LLMBackend.LITELLM or "litellm".
(default: LLMBackend.LITELLM)
llm_kwargs: Additional kwargs to pass to the LLM constructor.
(default: None)
llm_call_kwargs: Extra kwargs to forward to LLM calls (e.g., extra_body).
**kwargs: Additional arguments
"""
super().__init__(logs_dir, model_name, *args, **kwargs)
self._extra_env = extra_env
if model_name is None:
raise ValueError("model_name is required for Terminus 2")
self._model_name = model_name
self._last_response_model_name: str | None = None
self._parser_name = parser_name
self._collect_rollout_details = collect_rollout_details
self._reasoning_effort = reasoning_effort
resolved_model_info = self._resolve_model_info(model_name, model_info)
self._llm = self._init_llm(
llm_backend=llm_backend,
model_name=model_name,
temperature=temperature,
collect_rollout_details=collect_rollout_details,
llm_kwargs=llm_kwargs,
api_base=api_base,
session_id=session_id,
max_thinking_tokens=max_thinking_tokens,
reasoning_effort=reasoning_effort,
model_info=resolved_model_info,
use_responses_api=use_responses_api,
)
self._parser = self._get_parser()
self._prompt_template = self._get_prompt_template_path().read_text()
self._timeout_template = self._get_timeout_template_path().read_text()
self._temperature = temperature
# Handle deprecated 'episodes' kwarg
episodes_from_kwargs = kwargs.get("episodes")
if episodes_from_kwargs is not None:
self.logger.warning(
"The 'episodes' parameter is deprecated and will be removed in a future version. "
"Please use 'max_turns' instead."
)
# Determine the final max episodes value with proper precedence:
# 1. max_turns (new parameter)
# 2. max_episodes (deprecated but still supported)
# 3. episodes from kwargs (deprecated)
# 4. Default value of 1000000
final_max_episodes = None
if max_turns is not None:
final_max_episodes = max_turns
elif kwargs.get("max_episodes") is not None:
final_max_episodes = kwargs.get("max_episodes")
elif episodes_from_kwargs is not None:
final_max_episodes = episodes_from_kwargs
if final_max_episodes is not None:
if not suppress_max_turns_warning:
self.logger.warning(
f"max_turns (f.k.a. max_episodes) artificially limited to {final_max_episodes}. "
"Consider removing this limit for better task completion."
)
self._max_episodes = final_max_episodes
else:
self._max_episodes = 1000000
self._chat: Chat | None = None
self._context: AgentContext | None = None
self._timestamped_markers: list[tuple[float, str]] = []
self._pending_completion = False
self._session: TmuxSession | None = None
self._api_request_times: list[float] = []
self._n_episodes: int = 0
self._user_provided_session_id: str | None = session_id
self._session_id = session_id if session_id else str(uuid.uuid4())
self._trajectory_steps: list[Step] = []
self._record_terminal_session = record_terminal_session
self._llm_call_kwargs = dict(llm_call_kwargs) if llm_call_kwargs else {}
self._summarization_count: int = (
0 # Track number of summarization subagents created
)
self._pending_subagent_refs: list[SubagentTrajectoryRef] | None = (
None # Track subagent refs to include in next step
)
self._pending_handoff_prompt: str | None = (
None # Track handoff prompt to include as user step
)
self._subagent_metrics = SubagentMetrics() # Track subagent metrics separately
self._subagent_rollout_details: list[
RolloutDetail
] = [] # Track rollout details for each subagent
self._enable_summarize = (
enable_summarize # Toggle for proactive and context limit summarization
)
self._proactive_summarization_threshold = proactive_summarization_threshold
self._tmux_pane_width = tmux_pane_width
self._tmux_pane_height = tmux_pane_height
# Trajectory configuration
self._trajectory_config = trajectory_config or {}
self._save_raw_content_in_trajectory = self._trajectory_config.get(
"raw_content", False
)
self._linear_history = self._trajectory_config.get("linear_history", False)
# Optional: include full chat messages in TrialResult metadata (can be large)
self._store_all_messages = store_all_messages
self._interleaved_thinking = interleaved_thinking
self._llm_kwargs = llm_kwargs
Non-obvious 设计决策
- 对
model_name使用显式失败策略而非延后容错:代码在完成父类初始化后立刻检查if model_name is None: raise ValueError(...)。这样避免后续_resolve_model_info、_init_llm等路径在缺关键标识时产生更晚且更模糊的错误。 - 兼容参数采用双层处理而不是简单别名替换:
kwargs['episodes']仅用于兼容读取,并单独触发废弃告警;真正的上限解析再与max_turns、kwargs['max_episodes']一起进入优先级判定。这把“接口迁移提醒”和“人为限制回合数提醒”拆成了两个独立分支,避免一条告警混杂两种含义。 - 最大回合数的优先级是显式新参数优先:
max_turns覆盖旧的max_episodes与更旧的episodes,未提供时回退到1000000。这体现出向新接口迁移同时保留旧调用兼容性的取舍;若不做优先级分层,旧参数可能悄然覆盖新参数。 - 会话标识保留“双轨”信息:
self._user_provided_session_id保存外部输入原值,而self._session_id保证始终有值(必要时生成 UUID)。这样实例内部总有可用会话 ID,同时不丢失“是否由用户显式提供”的事实。 - 配置快照策略并不一致,且是代码中的真实取舍:
llm_call_kwargs在保存前用dict(...)复制,避免外部后续修改影响实例;但trajectory_config仅做trajectory_config or {},当传入真值对象时并未复制,而是保留原引用。也就是说,前者被快照隔离,后者没有完全隔离。
上下游关系
- 调用方: 直接实例化
Terminus2(...)的外部调用点; 任何子类若未覆盖构造函数而沿用本构造逻辑; 测试代码中创建Terminus2实例的入口; 框架/脚本层通过构造器注入配置时的对象创建路径 - 核心被调用: Base/父类
__init__(经super().__init__(...));self._resolve_model_info(model_name, model_info);self._init_llm(...);self._get_parser();self._get_prompt_template_path().read_text();self._get_timeout_template_path().read_text();uuid.uuid4();SubagentMetrics();self.logger.warning(...) - 配置/状态来源: 构造参数
model_name、parser_name、temperature、reasoning_effort、api_base; 构造参数enable_summarize、proactive_summarization_threshold、trajectory_config; 构造参数tmux_pane_width、tmux_pane_height、record_terminal_session、store_all_messages、interleaved_thinking; 构造参数llm_backend、llm_kwargs、llm_call_kwargs、use_responses_api、max_thinking_tokens;kwargs.get('episodes')与kwargs.get('max_episodes')的兼容输入;_get_prompt_template_path()与_get_timeout_template_path()指向的模板文件文本 - 结果去向: 实例字段
self._llm、self._parser、self._prompt_template、self._timeout_template; 实例字段self._max_episodes、self._temperature、self._llm_call_kwargs、self._llm_kwargs; 实例字段self._session_id、self._user_provided_session_id、self._extra_env; 实例字段self._trajectory_config、self._save_raw_content_in_trajectory、self._linear_history; 实例字段self._chat、self._context、self._trajectory_steps、self._api_request_times; 实例字段self._pending_completion、self._pending_subagent_refs、self._pending_handoff_prompt、self._summarization_count
reg-pending-completion— 构造时置为 Falsereg-pending-handoff-prompt— 构造时置为 Nonereg-pending-subagent-refs— 构造时置为 Nonereg-n-episodes— 构造时置为 0reg-summarization-count— 构造时置为 0reg-trajectory-steps— 构造时初始化为空列表reg-chat-messages— 构造时self._chat = Nonereg-asciinema-markers— 构造时初始化为空列表reg-subagent-metrics— 构造时新建指标对象reg-api-request-times— 构造时初始化为空列表
Terminus2._resolve_model_infoterminus_2.py:333–345 ↗
模型元信息选择与告警入口
stage 上下文: 该函数是一个很小的配置裁决点:在两个输入源之间决定返回哪份模型元信息,或明确返回
None。与同阶段里负责固化大量实例字段的兄弟函数相比,它本身不写入实例状态,只提供一个可被上层采用的返回值。其特殊性仅体现在对model_name中"hosted_vllm"模式的告警处理。
这段代码在干什么
函数 _resolve_model_info 接收 model_name 与 provided_model_info,优先返回真值的 provided_model_info。若 provided_model_info 为假值,则在 model_name 为真值且包含子串 "hosted_vllm" 时,通过 self.logger.warning(...) 发出一条配置告警。除此之外它不修改任何实例字段,最终返回值要么是传入的那份字典,要么是 None`;空字典等假值不会被直接返回,而会继续落入后续分支。
接口 · 参数 / IO
(self, model_name: str | None, provided_model_info: dict | None) -> dict | None
- 参数:
model_name:str | None— 候选模型名称;仅在其为真值且包含hosted_vllm时参与告警判断;provided_model_info:dict | None— 调用方传入的模型元信息;只有在其为真值时才被原样返回 - 读状态:
self.logger - 返回: 返回
dict | None:若provided_model_info为真值则返回该对象;否则返回None。 - 副作用: 当
model_name为真值且包含hosted_vllm,并且provided_model_info为假值时,调用self.logger.warning(...)输出一条告警日志
执行流
- 先检查
provided_model_info的真值性;若条件if provided_model_info:成立,则立即返回该参数本身,不再执行后续逻辑。 - 若前一步未返回,再检查
model_name的真值性以及表达式"hosted_vllm" in model_name是否成立;只有两者同时满足时,才调用self.logger.warning(...)记录告警。 - 无论是否发出告警,只要没有走到首个提前返回分支,函数最后都执行
return None。
源码
def _resolve_model_info(
self, model_name: str | None, provided_model_info: dict | None
) -> dict | None:
if provided_model_info:
return provided_model_info
if model_name and "hosted_vllm" in model_name:
self.logger.warning(
"Model info is required when using hosted_vllm models. "
"Please set `model_info` in your Terminus 2 configuration with "
"`max_input_tokens`, `max_output_tokens`, and cost fields. "
"Falling back to LiteLLM defaults, which may cause context or pricing issues."
)
return None
Non-obvious 设计决策
- 函数把“接受调用方给出的模型信息”写成
if provided_model_info:,这意味着只有真值字典才会被采用;空字典{}虽然类型上仍是dict,但会被当作未命中而继续走后续判断。这是一个不那么显眼但非常具体的取舍。 - 对
hosted_vllm的处理选择了“告警但不阻断”:代码只记录 warning,没有抛异常、没有补写默认值、也没有在本函数内执行任何回退逻辑。关于“Falling back to LiteLLM defaults, which may cause context or pricing issues.” 的表述仅出现在告警消息文本中,是提示语义,而不是该函数额外实现的行为。 - 特殊分支采用简单的子串匹配
"hosted_vllm" in model_name,而不是更严格的结构化解析;因此它只依据名称模式触发提醒,保持函数职责局限在轻量判定与提示。
上下游关系
- 调用方: 上层配置装配代码,向其请求本次应采用的模型元信息; 任何持有
self、model_name与provided_model_info的实例方法 - 核心被调用: self.logger.warning
- 配置/状态来源: 参数
model_name; 参数provided_model_info; 实例上的self.logger - 结果去向: 返回给调用方的一份
dict模型元信息; 返回给调用方的None; 日志系统中的一条 warning 记录 - 同类 sibling: Terminus2.__init__:兄弟函数负责把大量配置写入实例字段;本函数只做一处模型元信息选择并可能发出告警。
Terminus2._init_llmterminus_2.py:73–142 ↗
LLM 后端实例化分发点
stage 上下文: 该函数处于 stage-1 的早段,负责把
Terminus2.__init__已接收但尚未固化的 LLM 相关配置,收束为一个可直接使用的聊天后端对象。触发条件是构造器初始化期间需要建立self._llm。与同 stage 的_resolve_model_info相比,它不再判断模型元信息来源,而是消费这些已解析配置并完成后端分派。
这段代码在干什么
函数 _init_llm 根据 llm_backend 选择具体的 LLM 客户端实现,并返回一个 BaseLLM 兼容实例。它只读取传入参数:后端选择、模型名、采样温度、rollout 细节开关、通用 llm_kwargs,以及 LiteLLM 专属的 api_base、session_id、max_thinking_tokens、reasoning_effort、model_info、use_responses_api。函数本身不写入任何 self 状态;其产出会被 __init__ 用来赋给 self._llm。若后端值不在支持列表内,则抛出带支持项清单的 ValueError。
接口 · 参数 / IO
(self, llm_backend: LLMBackend | str, model_name: str, temperature: float | None, collect_rollout_details: bool, llm_kwargs: dict | None, api_base: str | None, session_id: str | None, max_thinking_tokens: int | None, reasoning_effort: str | None, model_info: dict | None, use_responses_api: bool) -> BaseLLM
- 参数:
llm_backend:LLMBackend | str— 后端选择器;既接受枚举值也接受字符串值,用于决定实例化LiteLLM还是TinkerLLM;model_name:str— 传给具体 LLM 构造器的模型标识;temperature:float | None— 显式配置的采样温度;仅在非None时并入构造参数;collect_rollout_details:bool— 是否收集 token id / logprob 等 rollout 细节,向各后端透传;llm_kwargs:dict | None— 调用方提供的通用后端构造参数,会先复制再补充专门字段;api_base:str | None— LiteLLM 分支的 API 基址;session_id:str | None— LiteLLM 分支的会话标识;max_thinking_tokens:int | None— LiteLLM 扩展 thinking 配额控制;reasoning_effort:str | None— LiteLLM 推理强度等级;model_info:dict | None— LiteLLM 自定义模型信息字典;通常来自兄弟函数_resolve_model_info的结果;use_responses_api:bool— LiteLLM 是否改走 Responses API - 返回: 返回一个已初始化的
BaseLLM兼容对象:当后端为litelm时返回LiteLLM,当后端为tinker时返回TinkerLLM。 - 副作用: 无
self状态写入; 在tinker分支发生一次延迟导入:from harbor.llms.tinker import TinkerLLM; 非法后端时抛出ValueError终止初始化流程
执行流
- 先把
llm_backend规范化为可匹配的字符串backend_value:若传入的是LLMBackend枚举,则取其.value,否则直接使用原字符串。 - 基于
llm_kwargs or {}构造一份新的constructor_kwargs,避免直接复用调用方传入字典;若temperature is not None,再把temperature显式并入该字典。 - 进入
match backend_value分派:当值等于LLMBackend.LITELLM.value时,实例化并返回LiteLLM,同时传入 LiteLLM 专属参数api_base、session_id、max_thinking_tokens、reasoning_effort、model_info、use_responses_api,以及通用的model_name、collect_rollout_details和展开后的constructor_kwargs。 - 当值等于
LLMBackend.TINKER.value时,先在分支内延迟导入TinkerLLM,再用model_name、collect_rollout_details和constructor_kwargs实例化并返回。 - 若
backend_value不匹配任何已知分支,则抛出ValueError;错误消息同时包含原始llm_backend表示值和[b.value for b in LLMBackend]生成的支持后端列表。
源码
def _init_llm(
self,
llm_backend: LLMBackend | str,
model_name: str,
temperature: float | None,
collect_rollout_details: bool,
llm_kwargs: dict | None,
# LiteLLM-specific args
api_base: str | None,
session_id: str | None,
max_thinking_tokens: int | None,
reasoning_effort: str | None,
model_info: dict | None,
use_responses_api: bool,
) -> BaseLLM:
"""Initialize the LLM backend based on llm_backend parameter.
Args:
llm_backend: The LLM backend to use.
model_name: Name of the model.
temperature: Sampling temperature, if explicitly configured.
collect_rollout_details: Whether to collect token IDs and logprobs.
llm_kwargs: Additional kwargs passed to the LLM constructor.
api_base: Base URL for LiteLLM API endpoint.
session_id: Session ID for LiteLLM.
max_thinking_tokens: Max thinking tokens for LiteLLM extended thinking.
reasoning_effort: Reasoning effort level for LiteLLM.
model_info: Model info dict for LiteLLM custom models.
use_responses_api: Whether to use the Responses API.
Returns:
An initialized LLM instance.
Raises:
ValueError: If llm_backend is not a recognized backend.
"""
# Normalize enum to string value for matching
backend_value = (
llm_backend.value if isinstance(llm_backend, LLMBackend) else llm_backend
)
constructor_kwargs = dict(llm_kwargs or {})
if temperature is not None:
constructor_kwargs["temperature"] = temperature
match backend_value:
case LLMBackend.LITELLM.value:
return LiteLLM(
model_name=model_name,
api_base=api_base,
collect_rollout_details=collect_rollout_details,
session_id=session_id,
max_thinking_tokens=max_thinking_tokens,
reasoning_effort=reasoning_effort,
model_info=model_info,
use_responses_api=use_responses_api,
**constructor_kwargs,
)
case LLMBackend.TINKER.value:
from harbor.llms.tinker import TinkerLLM
return TinkerLLM(
model_name=model_name,
collect_rollout_details=collect_rollout_details,
**constructor_kwargs,
)
case _:
raise ValueError(
f"Unknown llm_backend: {llm_backend!r}. "
f"Supported backends: {[b.value for b in LLMBackend]}"
)
Non-obvious 设计决策
- 该函数同时接受
LLMBackend枚举和裸字符串,并在开头统一折叠到backend_value。这样调用方可以保留更宽松的配置入口,而后续匹配逻辑只需处理一种比较形式;若不先规范化,match分支就必须同时覆盖枚举对象与字符串两套形态。 constructor_kwargs = dict(llm_kwargs or {})体现的是“复制后增强”而非“原地修改”。这避免了把temperature等补充参数回写到调用方持有的原始字典里,使_init_llm保持纯构造语义。- 对
temperature使用显式is not None检查,而不是泛真值判断。这保证了像0这样的合法配置不会被误判为缺省值,从而能够被真实传入后端构造器。 TinkerLLM放在对应分支里延迟导入,说明该依赖只在选择 tinker 后端时才需要解析。这样可以减少无关后端的导入负担,也避免在未使用 tinker 时因为缺少相关依赖而过早失败。- 未知后端不是静默回退,而是立即抛出附带支持项列表的
ValueError。这一取舍把配置错误暴露在初始化阶段,避免实例进入半初始化状态;同时错误信息直接给出合法候选值,降低排查成本。
上下游关系
- 调用方: Terminus2.__init__:在实例初始化时调用本函数并把结果写入
self._llm - 核心被调用: LiteLLM(...); harbor.llms.tinker.TinkerLLM(...); LLMBackend 枚举值比较; ValueError(...)
- 配置/状态来源: 来自
Terminus2.__init__形参的llm_backend配置; 来自Terminus2.__init__形参的model_name与temperature; 来自Terminus2.__init__形参的collect_rollout_details与llm_kwargs; 来自_resolve_model_info产出的model_info,再经__init__传入本函数; 来自Terminus2.__init__形参的 LiteLLM 专属设置:api_base、session_id、max_thinking_tokens、reasoning_effort、use_responses_api - 结果去向:
Terminus2.__init__的self._llm字段初始化; 后续 stage 中所有经self._llm发起的模型请求路径; 与_get_parser并列,作为 agent 运行时依赖装配的一部分 - 同类 sibling: Terminus2.__init__:负责汇总用户配置并消费本函数返回值; Terminus2._resolve_model_info:先决定
model_info,再把结果提供给本函数中的 LiteLLM 分支
Terminus2._get_parserterminus_2.py:375–384 ↗
按解析器名称分派解析实例
stage 上下文: 该单元位于配置固化阶段中,职责很窄:把实例上已有的
self._parser_name解释为一个具体解析器对象。触发条件从函数体本身只能确认是“当外部代码调用_get_parser()时”。与同 stage 的_resolve_model_info、_init_llm类似,它本身不写入实例状态,而是返回一个供外部继续使用的组件对象。
这段代码在干什么
函数 _get_parser 读取 self._parser_name,仅支持两种取值:"json" 与 "xml"。当值分别匹配时,它返回一个新建的 TerminusJSONPlainParser() 或 TerminusXMLPlainParser() 实例;除此之外不产生任何状态写入。若配置值不在这两个分支内,则立即抛出 ValueError,并在错误信息中直接列出允许值。
接口 · 参数 / IO
(self)
- 参数:
self:Terminus2— 提供实例配置状态,函数仅从中读取self._parser_name - 读状态:
self._parser_name - 返回: 返回一个新建的解析器实例:当
self._parser_name == "json"时返回TerminusJSONPlainParser();当self._parser_name == "xml"时返回TerminusXMLPlainParser();若两者都不是,则不返回而是抛出ValueError("Unknown parser_name: {self._parser_name}. Use 'json' or 'xml'.")。
执行流
- 先读取实例字段
self._parser_name,判断其是否等于字符串"json"。 - 若为
"json",立即构造并返回一个新的TerminusJSONPlainParser()实例。 - 否则继续判断
self._parser_name是否等于字符串"xml"。 - 若为
"xml",立即构造并返回一个新的TerminusXMLPlainParser()实例。 - 若前述两个分支都不匹配,则进入
else,抛出ValueError;错误文本会插入当前的self._parser_name,并明确提示只允许'json'或'xml'。
源码
def _get_parser(self):
"""Return the appropriate parser instance for this format."""
if self._parser_name == "json":
return TerminusJSONPlainParser()
elif self._parser_name == "xml":
return TerminusXMLPlainParser()
else:
raise ValueError(
f"Unknown parser_name: {self._parser_name}. Use 'json' or 'xml'."
)
Non-obvious 设计决策
- 代码采用封闭式二选一分派,而不是默认回退到某个解析器;这样未识别配置会被
ValueError立刻拒绝,不会静默降级。 - 异常消息不是泛泛报错,而是把实际收到的
self._parser_name与允许值'json'、'xml'一并写出,便于直接定位配置错误。 - 两个成功分支都通过直接调用类构造器返回新对象,说明该函数选择“每次调用新建实例”,而不是复用共享单例。
上下游关系
- 调用方: 未知外部调用方:源码片段本身未显示谁调用
_get_parser - 核心被调用: TerminusJSONPlainParser(); TerminusXMLPlainParser(); ValueError(...)
- 配置/状态来源: self._parser_name
- 结果去向: 返回给
_get_parser的直接调用者; 异常路径返回给调用栈上的错误处理方 - 同类 sibling: Terminus2._resolve_model_info:同样是读取输入/状态后返回结果,本体不写实例字段; Terminus2._init_llm:同样承担组件选择/构造职责,但
_get_parser的分派集合更小且只依赖self._parser_name
Terminus2._get_prompt_template_pathterminus_2.py:386–395 ↗
按解析器名选择 prompt 模板路径
stage 上下文: 该单元是一个纯查询式辅助函数:根据当前
self._parser_name计算模板文件路径。它本身不写入实例状态,只在当前模块目录下的templates子目录中二选一返回文件路径;若配置值不受支持,则立即报错。
这段代码在干什么
函数 _get_prompt_template_path 读取 self._parser_name,并基于模块文件位置 __file__ 组装对应 prompt 模板的 Path。当解析器名为 "json" 时返回 terminus-json-plain.txt,为 "xml" 时返回 terminus-xml-plain.txt。除返回值外无其他产出;若取值不是这两者,则抛出 ValueError。
接口 · 参数 / IO
(self) -> Path
- 参数:
self:?— 提供实例配置字段self._parser_name;__file__:str— 当前模块文件路径;经Path(__file__).parent作为模板目录定位基准 - 读状态:
self._parser_name,__file__ - 返回: 返回一个
pathlib.Path,其共同模式为Path(__file__).parent / "templates" / <filename>;当self._parser_name不为json或xml时改为抛出ValueError。
执行流
- 先读取
self._parser_name并判断是否等于字符串"json"。 - 若为
"json",返回Path(__file__).parent / "templates" / "terminus-json-plain.txt"。 - 否则继续判断
self._parser_name是否等于字符串"xml"。 - 若为
"xml",返回Path(__file__).parent / "templates" / "terminus-xml-plain.txt"。 - 若上述两种取值都不匹配,则抛出
ValueError;异常消息同时包含实际值self._parser_name与允许值"json"、"xml"。
源码
def _get_prompt_template_path(self) -> Path:
"""Return the path to the prompt template for this format."""
if self._parser_name == "json":
return Path(__file__).parent / "templates" / "terminus-json-plain.txt"
elif self._parser_name == "xml":
return Path(__file__).parent / "templates" / "terminus-xml-plain.txt"
else:
raise ValueError(
f"Unknown parser_name: {self._parser_name}. Use 'json' or 'xml'."
)
Non-obvious 设计决策
- 模板路径采用
Path(__file__).parent作为基准,而不是依赖外部传入目录;这使查找位置固定为当前模块旁的templates子目录。 - 成功分支只接受两种显式字符串:
"json"与"xml";代码没有默认回退模板,未知取值会直接失败。 - 异常信息不是泛化报错,而是把非法的
self._parser_name实际值嵌入消息,并明确给出可用选项Use 'json' or 'xml'.,便于定位配置错误。
上下游关系
- 调用方: 未知;给定源码片段未显示调用点
- 核心被调用: Path(__file__)
- 配置/状态来源: self._parser_name; __file__
- 结果去向: 函数调用方接收返回的
Path; 异常路径下由调用方接收ValueError
Terminus2._get_timeout_template_pathterminus_2.py:397–399 ↗
超时模板路径构造器
stage 上下文: 该函数本体只证明一件事:基于当前源码文件位置构造一个固定模板路径并返回。它不读取任何
self实例状态,也不执行文件存在性检查或内容加载。就可见代码而言,它是一个纯路径定位辅助函数。
这段代码在干什么
函数 _get_timeout_template_path 接收 self,但函数体并未使用任何实例字段。它直接返回表达式 Path(__file__).parent / "templates" / "timeout.txt" 生成的 Path 对象。除该返回值外无其他产出,也不会修改对象状态。
接口 · 参数 / IO
(self: Terminus2) -> Path
- 参数:
self:Terminus2— 方法接收者;在此函数体内未被读取 - 返回: 返回精确由
Path(__file__).parent / "templates" / "timeout.txt"构造出的Path;不读取文件、不验证路径是否存在。
执行流
- 调用
Path(__file__)取得当前模块源码文件对应的路径对象。 - 取其
.parent作为基目录,并依次拼接子路径"templates"与"timeout.txt"。 - 返回得到的
Path对象作为超时模板文件路径。
源码
def _get_timeout_template_path(self) -> Path:
"""Return the path to the timeout template for this format."""
return Path(__file__).parent / "templates" / "timeout.txt"
Non-obvious 设计决策
- 路径锚定在
__file__所在目录,而不是当前工作目录或实例配置;这一选择使返回值仅由模块物理位置决定。 - 函数只负责构造路径,不在此处检查文件是否存在、是否可读,因而职责被限定为定位而非验证。
上下游关系
- 调用方: 未在该函数体内体现;外部任意需要超时模板路径的调用点
- 核心被调用: pathlib.Path; Path.parent; Path.__truediv__
- 配置/状态来源: __file__ 模块级文件路径常量
- 结果去向: 返回给调用方作为
templates/timeout.txt的路径值