Terminus 2 手册

上下文摘要

side-S1Context Summarization7 个函数

这个 Stage 在解决上下文装不下的问题:当主 Agent(主执行体)聊天历史太长时,它把“已经发生了什么”压成一份可交接的短材料,让后面的回合还能继续稳定地问答。它本质上是一个上下文压缩 / 交接构造阶段。无论是提前发现 token(上下文容量单位)快不够,还是 LLM 调用时真的报超长,最后都会收束到同一条 _summarize() 路径。它不负责把交接内容塞回主对话;它只负责把交接包准备好。

  1. 先判断:是不是该“收历史”了。

    入口有两条,但目的相同。

    • 一条是主动路径:剩余 token 太少时,提前触发。这样做是为了别等到真正报错才补救。
    • 一条是被动路径:_query_llm 里真的遇到 ContextLengthExceededError(上下文超长错误)后,先腾出一点空间,再立刻进入 _summarize()。腾空间属于异常恢复;总结交接才是这个 Stage 的核心工作。
  2. _summarize()(把长历史压成可继续工作的交接材料)统一处理两条入口。

    这里为什么不只做一句短摘要?因为系统后面还要继续干活,不只是“知道个大概”。它需要一份能接棒的材料:做过什么、还卡在哪、现在到哪一步了。 所以这里不是单次总结,而是用三个 subagent(被主 Agent 调用的小 Agent)做一轮“总结 + 追问 + 补答”,尽量把交接信息补齐。

  3. 第一个 subagent 先写主摘要。

    它看已经保留下来的聊天历史,产出一份叙述:主要动作、关键信息、难点、当前状态。 这一步的意义是先把“故事主线”抽出来,不然后面只会拿到一堆零散对话。

  4. 第二个 subagent 专门挑缺口。

    它不继承前一个 subagent 的长历史,只看原始任务 + 刚写出的摘要 + 当前终端画面。 这样设计是为了站在“接手的人”视角提问:如果只拿到这份摘要,现在还缺哪些关键信息? 它的作用不是生成答案,而是逼系统发现摘要遗漏。

  5. 第三个 subagent 回答这些问题。

    它重新看聊天历史,再结合前面那份摘要和问题,把缺的细节补上。 于是最后得到的不是一段模糊摘要,而是一份更像“交接说明”的 handoff prompt(交接提示词)。

  6. _run_subagent()(跑一个 subagent,并顺手记下它自己的过程)负责把这三个 subagent 统一跑起来。

    为什么要单独留痕?因为总结本身也是系统行为。以后看轨迹时,不能只看到“突然出现一份 handoff”,还要知道它是怎么来的。 所以每个 subagent 的过程都会通过 _save_subagent_trajectory()(把 subagent 轨迹单独存档)留下引用,供后面挂到主轨迹上。

  7. 最后,这个 Stage 会把主对话改写成一个更短的新起点,并把 handoff 单独交出去。

    这样做的工程原因很直接:如果只是把摘要继续附加到原来的长历史后面,token 压力很快又会回来;而把后续工作改为从“短起点 + 交接材料”继续,才能真正把上下文负担降下来,让后面的轮次更稳。

上游真正给本 Stage 的,不是业务数据,而是一个信号:上下文空间紧张了,或者已经因为太长而报错。本 Stage 把这个信号变成两样可消费产物:一份 handoff prompt,以及一组 subagent 轨迹引用。再往后,[stage-4.5](stage-4.5.html) 负责挂轨迹,[stage-4.6](stage-4.6.html) 负责把 handoff 接回主对话。

函数细节7

Terminus2._summarizeterminus_2.py:740–954 ↗

三子代理上下文压缩与交接构造器

stage 上下文: 该函数是 side-S1「Context Summarization」的核心执行体,负责把当前对话压缩成可交接给后续 agent 的 handoff prompt。它可由两条触发路径进入:stage-4.2 的主动总结检查,或 stage-4.3 在上下文超限后的回退流程。与同 stage 的其它兄弟逻辑相比,本函数真正执行三次 subagent 调用、重写 chat 历史,并产出后续 stage-4.5 / 4.6 要消费的总结结果。

这段代码在干什么

函数读取 chat.messagesoriginal_instruction 与当前 session 终端画面,串联执行“生成总结—提出缺口问题—基于更全上下文回答问题”三步子代理流程。若聊天历史为空,则直接返回原始任务与 None;否则递增 self._summarization_count,收集 3 个 SubagentTrajectoryRef,并把 chat._messages 改写为新的 3 条消息上下文。最终返回值是供后续主 agent 继续工作的 handoff_prompt,其正文来自第三个子代理给出的回答。

接口 · 参数 / IO

(self, chat: Chat, original_instruction: str, session: TmuxSession) -> tuple[str, list[SubagentTrajectoryRef] | None]

  • 参数: chat: Chat — 当前主对话对象;提供待压缩的 messages,并在结束时被原地替换为精简历史; original_instruction: str — 最初用户任务描述;用于三步子代理提示词与空历史时的直接返回; session: TmuxSession — 当前终端会话;用于捕获 current_screen 供提问子代理感知现状
  • 读状态: self._session_id, self._summarization_count, self._model_name
  • 返回: 返回 (handoff_prompt, subagent_trajectory_refs);若 chat.messages 为空,则返回 (original_instruction, None)。正常路径下,handoff_prompt 为第三个子代理回答问题后的交接提示,subagent_trajectory_refs 为三个子代理各自的轨迹引用。
  • 副作用: 递增 self._summarization_count; 通过 _run_subagent 触发三次子代理执行,并间接写出各自 trajectory 文件; 调用 session.capture_pane(capture_entire=False) 读取当前 tmux 屏幕; 原地改写 chat._messages[system, question_prompt, model_questions]; 调用 chat.reset_response_chain() 重置对话响应链

执行流

  1. 先检查 len(chat.messages);若当前聊天历史为空,则不做总结流程,直接返回 original_instructionNone
  2. 进入正常路径后递增 self._summarization_count,计算 steps_to_include = 1 + (len(chat.messages) - 1) // 2,并用 _prepare_copied_trajectory_steps(...) 生成与当前保留下来的 chat.messages 对齐的复制轨迹步骤,供第一个总结子代理使用。
  3. 构造 summary_prompt,调用 _run_subagent(...) 执行“Summary Generation”;其输入消息历史就是当前已 unwind 后的 chat.messages,输出 summary_response 与对应 summary_trajectory_ref,后者被加入 subagent_trajectory_refs
  4. 调用 session.capture_pane(capture_entire=False) 获取 current_screen,把原始任务、上一步总结文本和终端画面拼成 question_prompt,再以空 message_history=[] 调用 _run_subagent(...) 执行“Question Asking”,得到 model_questions 及第二个轨迹引用。
  5. 为第三个“Answer Providing”子代理重新准备复制轨迹步骤,并额外追加两条 is_copied_context=TrueStep:一条表示 summary_prompt,一条表示 summary_response,使其轨迹显式包含总结调用本身;随后构造 answers_message_history = chat.messages + [summary_prompt, summary_response]answer_request_prompt,调用 _run_subagent(...) 获取回答内容及第三个轨迹引用。
  6. 流程结束时,把 chat._messages 重写为原系统消息、question_promptmodel_questions 这三条消息,并执行 chat.reset_response_chain();最后把第三个子代理的回答包装成 handoff_prompt 返回,同时返回三个 SubagentTrajectoryRef

源码

    async def _summarize(
        self, chat: Chat, original_instruction: str, session: TmuxSession
    ) -> tuple[str, list[SubagentTrajectoryRef] | None]:
        """Create a summary of the agent's work to pass to a new agent instance.

        This method implements a three-step context summarization process using separate
        subagents to compress conversation history while preserving critical information:

        **Step 1: Summary Generation (SUBAGENT 1)**
        - Message history: Unwound chat.messages (limited context after token freeing)
        - Prompt: Generate comprehensive summary of all work completed so far
        - Response: summary_response.content (detailed narrative of progress)

        **Step 2: Question Asking (SUBAGENT 2)**
        - Message history: [] (fresh start, no conversation context)
        - Prompt: Given the original task, summary from Step 1, and current terminal screen,
        generate questions about information missing from the summary.
        - Response: model_questions (list of clarifying questions)

        **Step 3: Answer Providing (SUBAGENT 3)**
        - Message history: chat.messages + [summary_prompt, summary_response] (extended context)
        - Prompt: Given the questions from Step 2, answer the questions based on full conversation history.
        - Response: answers_response.content (detailed answers)

        **Final handoff:**
        - Chat history replaced with: [system, question_prompt (which includes summary from step 1), model_questions]
        - Result: Compressed context that preserves critical task-specific information, allowing
        the main agent to continue working on the task without losing context.

        **Why three steps?** This question-answer approach ensures the summarization doesn't
        lose important details - the questions subagent identifies gaps, and the answers
        subagent (with full context) fills them in.

        Args:
            chat: Chat object containing conversation history (may be unwound)
            original_instruction: The original task instruction from user
            session: TmuxSession for capturing current terminal state

        Returns:
            tuple: (handoff_prompt, subagent_trajectory_refs)
                - handoff_prompt: The prompt to continue with (includes answers + instructions)
                - subagent_trajectory_refs: List of 3 SubagentTrajectoryRef objects, or None if summarization failed
        """
        if len(chat.messages) == 0:
            return original_instruction, None

        # Increment summarization count
        self._summarization_count += 1
        subagent_trajectory_refs = []

        # ===== SUBAGENT 1: Summary Generation =====
        summary_session_id = (
            f"{self._session_id}-summarization-{self._summarization_count}-summary"
        )

        # Trajectory needs to reflect what is sent to LLM: essentially the previous chat
        # history, minus the messages that were removed to free up tokens.
        # Calculate how many trajectory steps to include based on remaining chat messages:
        # - Chat has: [system, agent1, user1, agent2, user2, ...]
        # - Trajectory has: [step1_system, step2_agent, step3_agent, ...]
        # - Formula: steps_to_include = 1 + (num_messages - 1) // 2
        steps_to_include = 1 + (len(chat.messages) - 1) // 2
        summary_steps, step_id_counter = self._prepare_copied_trajectory_steps(
            steps_to_include
        )

        summary_prompt = f"""You are about to hand off your work to another AI agent.
            Please provide a comprehensive summary of what you have
            accomplished so far on this task:

Original Task: {original_instruction}

Based on the conversation history, please provide a detailed summary covering:
1. **Major Actions Completed** - List each significant command you executed
            and what you learned from it.
2. **Important Information Learned** - A summary of crucial findings, file
            locations, configurations, error messages, or system state discovered.
3. **Challenging Problems Addressed** - Any significant issues you
            encountered and how you resolved them.
4. **Current Status** - Exactly where you are in the task completion process.


Be comprehensive and detailed. The next agent needs to understand everything
            that has happened so far in order to continue."""

        summary_response, summary_trajectory_ref = await self._run_subagent(
            prompt=summary_prompt,
            message_history=chat.messages,
            steps=summary_steps,
            session_id=summary_session_id,
            agent_name="terminus-2-summarization-summary",
            filename_suffix="summary",
            summary_text=f"Context summarization {self._summarization_count}: Step 1 - Summary generation",
            subagent_name_for_logging="summary generation LLM call",
        )
        subagent_trajectory_refs.append(summary_trajectory_ref)

        # ===== SUBAGENT 2: Question Asking =====
        current_screen = await session.capture_pane(capture_entire=False)
        questions_session_id = (
            f"{self._session_id}-summarization-{self._summarization_count}-questions"
        )
        questions_steps = []

        question_prompt = f"""You are picking up work from a previous AI agent on this task:

**Original Task:** {original_instruction}

**Summary from Previous Agent:**
{summary_response.content}

**Current Terminal Screen:**
{current_screen}

Please begin by asking several questions (at least five, more if necessary)
about the current state of the solution that are not answered in the summary
from the prior agent. After you ask these questions you will be on your own,
so ask everything you need to know."""

        questions_response, questions_trajectory_ref = await self._run_subagent(
            prompt=question_prompt,
            message_history=[],
            steps=questions_steps,
            session_id=questions_session_id,
            agent_name="terminus-2-summarization-questions",
            filename_suffix="questions",
            summary_text=f"Context summarization {self._summarization_count}: Step 2 - Question asking",
            subagent_name_for_logging="questions subagent",
        )
        model_questions = questions_response.content
        subagent_trajectory_refs.append(questions_trajectory_ref)

        # ===== SUBAGENT 3: Answer Providing =====
        answers_session_id = (
            f"{self._session_id}-summarization-{self._summarization_count}-answers"
        )

        # Reuse the actual trajectory steps (same as summary subagent)
        # At this point, chat.messages has the same unwound history as in summary subagent
        answers_steps, step_id_counter = self._prepare_copied_trajectory_steps(
            steps_to_include
        )

        # Add the summary prompt and response steps that are part of the message history
        # Mark these as copied context since they were already part of the summary subagent trajectory
        answers_steps.append(
            Step(
                step_id=step_id_counter,
                timestamp=datetime.now(timezone.utc).isoformat(),
                source="user",
                message=summary_prompt,
                is_copied_context=True,
            )
        )
        step_id_counter += 1

        answers_steps.append(
            Step(
                step_id=step_id_counter,
                timestamp=datetime.now(timezone.utc).isoformat(),
                source="agent",
                model_name=summary_response.model_name or self._model_name,
                message=summary_response.content,
                reasoning_content=summary_response.reasoning_content,
                is_copied_context=True,
                extra={
                    "note": "Copied from summary subagent - metrics already recorded there"
                },
            )
        )
        step_id_counter += 1

        answer_request_prompt = (
            "The next agent has a few questions for you, please answer each of them one by one in detail:\n\n"
            + model_questions
        )

        # The answer subagent should see: unwound chat history + summary prompt + summary response
        answers_message_history = chat.messages + [
            {"role": "user", "content": summary_prompt},
            {"role": "assistant", "content": summary_response.content},
        ]

        answers_response, answers_trajectory_ref = await self._run_subagent(
            prompt=answer_request_prompt,
            message_history=answers_message_history,
            steps=answers_steps,
            session_id=answers_session_id,
            agent_name="terminus-2-summarization-answers",
            filename_suffix="answers",
            summary_text=f"Context summarization {self._summarization_count}: Step 3 - Answer providing",
            subagent_name_for_logging="answers subagent",
        )
        subagent_trajectory_refs.append(answers_trajectory_ref)

        # Update chat history with the handoff context
        # Note: We only include questions, not answers. The answers will be provided
        # via the handoff_prompt to the next agent iteration.
        chat._messages = [
            chat.messages[0],
            {"role": "user", "content": question_prompt},
            {"role": "assistant", "content": model_questions},
        ]
        chat.reset_response_chain()

        handoff_prompt = (
            "Here are the answers the other agent provided.\n\n"
            + answers_response.content
            + "\n\n"
            + "Continue working on this task from where the previous agent left off."
            " You can no longer ask questions. Please follow the spec to interact with "
            "the terminal."
        )

        return handoff_prompt, subagent_trajectory_refs

Non-obvious 设计决策

  • 空历史分支选择直接返回 (original_instruction, None),而不是强行运行总结子代理;这是对“无可压缩上下文”场景的显式短路,避免无意义的 LLM 开销,也避免制造空洞的 summary 结果。
  • 第一个和第三个子代理都不直接复用完整主轨迹,而是通过 steps_to_include_prepare_copied_trajectory_steps(...) 重建“与当前 chat.messages 一致”的复制轨迹;这是因为前置流程可能已 unwind 掉部分消息,若仍保留完整轨迹,会让子代理实际看到的上下文与保存下来的 trajectory 不一致。
  • 第二个提问子代理刻意使用空 message_history=[],只给它原任务、总结文本和当前终端屏幕,而不给完整对话;这样它的职责被限制为识别总结缺口,而不是从原始上下文里自行补全答案,否则“提问—补缺”这一步会失去诊断价值。
  • 第三个回答子代理获得比第二个子代理更宽的上下文:不仅有 chat.messages,还额外附加 summary_promptsummary_response;这体现了不对称设计——提问方负责发现遗漏,回答方负责利用更全材料补洞,从而降低总结遗漏造成的信息损失。
  • 最终写回 chat._messages 时只保留问题链路,不把答案直接并入对话历史;答案被放入返回的 handoff_prompt。这将“后续主 agent 继续对话的最小上下文”和“本次交接补充说明”分离,既压缩主聊天窗口,又保留可在下一阶段注入的 handoff 内容。

上下游关系

  • 调用方: Terminus2._check_proactive_summarization 触发主动总结时调用; Terminus2._query_llm 的 ContextLengthExceededError 回退路径调用; side-S1 的两条入口路径最终都汇入本函数; 需要在主 agent 继续前压缩上下文的内部控制流
  • 核心被调用: self._prepare_copied_trajectory_steps; self._run_subagent; session.capture_pane; chat.reset_response_chain; Step(...); datetime.now(timezone.utc).isoformat
  • 配置/状态来源: self._session_id:用于构造三个子代理的 session_id; self._summarization_count:用于编号本次总结并生成日志/会话标识; self._model_name:作为复制的 summary 响应步骤的兜底 model_name; chat.messages:作为当前可压缩上下文与步骤裁剪依据; original_instruction:作为三步提示词的共同任务锚点
  • 结果去向: 返回的 handoff_prompt 进入 reg-pending-handoff-prompt,供 stage-4.6 消费; 返回的 subagent_trajectory_refs 进入 reg-pending-subagent-refs,供 stage-4.5 记录; 递增后的 self._summarization_count 对应 reg-summarization-count,最终在 stage-5 surfaced 到 metadata; 被改写的 chat._messages 直接更新 live conversation history,对应 reg-chat-messages; 三个 _run_subagent 调用各自产生的轨迹文件与子代理引用,后续并入 trial 轨迹体系
寄存器交互
Terminus2._prepare_copied_trajectory_stepsterminus_2.py:1689–1703 ↗

子代理轨迹前缀清洗准备器

stage 上下文: 该函数属于 side-S1 的支撑性准备逻辑,不直接执行总结三子代理,而是为它们提供可复用的主轨迹快照。它在 _summarize 组装子代理上下文时被调用,用主运行的部分 self._trajectory_steps 生成一份可安全转交的前缀历史。与兄弟函数 _summarize 相比,这里不改写聊天消息,也不写入 handoff 结果,只负责整理轨迹输入材料。

这段代码在干什么

函数接收 steps_to_include,从 self._trajectory_steps 取出对应前缀并生成深拷贝,随后对这份副本执行指标字段清洗。返回值是二元组:清洗后的 copied_steps 与基于其长度推得的 next_step_id。函数本身不修改实例状态,作用是为后续子代理轨迹续写提供一份干净且与主轨迹隔离的历史基线。

接口 · 参数 / IO

(self, steps_to_include: int) -> tuple[list[Step], int]

  • 参数: steps_to_include: int — 指定从主轨迹 self._trajectory_steps 前缀中纳入多少个步骤
  • 读状态: self._trajectory_steps
  • 返回: 返回 (copied_steps, next_step_id):前者是去除 metrics 后的轨迹副本列表,后者是该副本之后可继续分配的下一个步骤编号。
  • 副作用: 无实例状态写入;仅对局部变量 copied_steps 调用清洗逻辑

执行流

  1. steps_to_include 截取 self._trajectory_steps 的前缀,并用 copy.deepcopy(...) 生成独立副本 copied_steps
  2. copied_steps 传给 self._remove_metrics_from_copied_steps(...),去掉副本中的 metrics 类字段,使其适合作为子代理历史。
  3. len(copied_steps) + 1 计算 next_step_id,表示子代理若在这份副本后续写,其首个新步骤应使用的编号。
  4. 返回清洗后的 copied_stepsnext_step_id

源码

    def _prepare_copied_trajectory_steps(
        self, steps_to_include: int
    ) -> tuple[list[Step], int]:
        """Prepare trajectory steps for subagent by copying and removing metrics.

        Args:
            steps_to_include: Number of steps to include from main trajectory

        Returns:
            Tuple of (copied_steps, next_step_id)
        """
        copied_steps = copy.deepcopy(self._trajectory_steps[:steps_to_include])
        self._remove_metrics_from_copied_steps(copied_steps)
        next_step_id = len(copied_steps) + 1
        return copied_steps, next_step_id

Non-obvious 设计决策

  • 这里显式使用 copy.deepcopy,体现的取舍是“隔离后续编辑”优先于复制成本:子代理后续若追加或改写这份历史,不会反向污染主运行的 self._trajectory_steps
  • 函数在复用轨迹前先调用 _remove_metrics_from_copied_steps,说明被转交给子代理的历史刻意排除了计量噪声;若直接复用原始步骤,子代理上下文会混入与任务语义无关的成本/指标信息。
  • next_step_id 不读取外部计数器,而是由 len(copied_steps) + 1 直接推导,保证编号与“实际纳入的副本长度”一致;即使只复制主轨迹前缀而非全部步骤,后续编号仍能局部自洽。

上下游关系

  • 调用方: Terminus2._summarize(为总结相关子代理准备可复用轨迹前缀)
  • 核心被调用: copy.deepcopy; Terminus2._remove_metrics_from_copied_steps
  • 配置/状态来源: self._trajectory_steps(主运行当前累计的轨迹步骤)
  • 结果去向: _summarize 内部后续子代理上下文构造; 子代理轨迹续写时的初始 step 编号确定
  • 同类 sibling: Terminus2._summarize:后者负责执行三子代理总结流程并改写 chat._messages;本函数仅提供其中可传递的轨迹副本与编号基线。
寄存器交互
Terminus2._remove_metrics_from_copied_stepsterminus_2.py:1670–1687 ↗

复制轨迹步的指标清洗器

stage 上下文: 该函数位于 side-S1 相关代码中,但从源码本身可见,它只处理传入的 steps 列表内容,不读取 self 或 stage 级上下文状态。其职责是把这些 Step 统一标记为复制上下文,并在已有指标时删除指标、补写说明,以便后续消费者看到的是去重后的轨迹副本。

这段代码在干什么

函数接收 list[Step],逐个遍历其中元素,并原地修改每个 Step 实例;列表本身不增删、不重排。对所有步骤都会写入 step.is_copied_context = True;仅当 step.metrics 为真值时,才会把 step.metrics 置为 None,并确保 step.extra 可写后写入说明文字。函数返回 None,真正产出是这些传入对象上的就地清洗结果。

接口 · 参数 / IO

(steps: list[Step]) -> None

  • 参数: steps: list[Step] — 要逐项遍历的轨迹步骤列表;函数只迭代该列表,但会原地修改其中每个 Step 对象
  • 返回: 返回 None;实际结果是传入列表内各 Step 的字段被原地改写
  • 副作用: 对每个 Step 写入 step.is_copied_context = True; 当 step.metrics 为真值时,将 step.metrics 清空为 None; 当 step.metrics 为真值且 step.extra is None 时,创建空字典并赋给 step.extra; 当 step.metrics 为真值时,写入并覆盖 step.extra["note"]

执行流

  1. 遍历参数 steps 中的每个 step,不修改列表结构,只处理其中对象。
  2. 对每个 step,无条件设置 step.is_copied_context = True,把该步骤标记为复制来的上下文。
  3. 检查 step.metrics 是否为真值;只有进入该分支时,才执行指标清洗逻辑。
  4. 在指标分支内,将 step.metrics 直接置为 None,对应注释中的目的“避免 duplication”。
  5. 若此时 step.extra is None,先创建空字典赋回 step.extra,以保证后续可写入附加说明。
  6. step.extra["note"] 写入固定说明字符串:Metrics omitted to avoid duplication - already recorded in parent trajectory;这一赋值是直接覆盖式的,即若原先已有 note 也会被替换。

源码

    def _remove_metrics_from_copied_steps(steps: list[Step]) -> None:
        """Remove metrics from copied trajectory steps and mark as copied context.

        Args:
            steps: List of trajectory steps to modify in-place
        """
        for step in steps:
            # Mark all copied steps with is_copied_context=True
            step.is_copied_context = True

            # Remove metrics to avoid duplication
            if step.metrics:
                step.metrics = None
                if step.extra is None:
                    step.extra = {}
                step.extra["note"] = (
                    "Metrics omitted to avoid duplication - already recorded in parent trajectory"
                )

Non-obvious 设计决策

  • 函数把“复制上下文标记”做成无条件动作,而把“删除 metrics”做成条件动作:前者确保所有传入步骤都带有来源标识,后者只在确有 step.metrics 时触发,避免对本就无指标的步骤做额外说明写入。
  • 清洗 metrics 后同时补写 step.extra["note"],说明删除原因是“避免 duplication,且这些指标已记录在 parent trajectory”。这不是单纯丢弃数据,而是保留一个可追溯的人工说明,减少下游把 metrics=None 误解为缺失采集的风险。
  • step.extra 采用显式 None 检查并在需要时补空字典,是为了兼容 extra 尚未初始化的步骤对象;否则后续下标写入会失败。
  • 指标分支内对 step.extra["note"] 采取无条件覆盖,而不是保留旧值或追加内容;该取舍让“为何移除 metrics”拥有统一且确定的最终表述,但代价是若原先已有 note,其内容会被覆盖。

上下游关系

  • 调用方: 未知(提供的源码片段未显示调用点)
  • 核心被调用: 无显式函数调用; 使用 steps 的迭代协议进行 for step in steps 遍历
  • 配置/状态来源: 无; 输入仅来自形参 steps 及各 Step 当前字段值(metricsextra
  • 结果去向: 返回值为 None; 结果留存在传入列表内各 Stepis_copied_contextmetricsextra["note"] 字段上
  • 同类 sibling: 与 Terminus2._prepare_copied_trajectory_steps 主题相邻:后者按其名称看负责准备复制步骤,本函数则专注于已传入步骤对象的原地清洗;但当前片段未直接展示两者调用关系。
Terminus2._run_subagentterminus_2.py:661–738 ↗

统一执行单个子代理轮次的助手

stage 上下文: 该函数位于 side-S1 的子代理执行层,负责把一次“子代理提示词 → LLM 响应 → 轨迹落盘”的重复流程封装为统一入口。它本身不决定何时总结,也不构造三段式总结流程;其职责是为上层提供一致的计时、指标、轨迹与返回值形态。与同 stage 的 _prepare_copied_trajectory_steps_remove_metrics_from_copied_steps 相比,本函数是真正发起子代理 LLM 调用并产出子轨迹引用的执行单元。

这段代码在干什么

函数接收子代理提示词 prompt、上下文消息 message_history、可变轨迹列表 steps 以及一组命名/落盘参数,完整执行一次子代理 LLM 交互。它先把提示词作为新的 Step 追加到 steps,随后调用 self._llm.call(...) 获得 response,读取其中的 response.usage 同时用于子代理指标更新与后续轨迹保存。函数返回二元组 (response, trajectory_ref);除返回值外,还会就地扩充 steps,并经辅助方法产生计时记录、子代理指标累积、rollout 明细收集与轨迹持久化等副作用。

接口 · 参数 / IO

(self, prompt: str, message_history: list[dict], steps: list[Step], session_id: str, agent_name: str, filename_suffix: str, summary_text: str, subagent_name_for_logging: str = "subagent") -> tuple[LLMResponse, SubagentTrajectoryRef]

  • 参数: prompt: str — 要发送给子代理 LLM 的本轮提示词,同时被写入轨迹中的 user 步; message_history: list[dict] — 提供给 self._llm.call(...) 的历史消息上下文; steps: list[Step] — 子代理轨迹步骤列表;函数会原地追加 prompt 步与响应步; session_id: str — 传给 _save_subagent_trajectory(...) 的会话标识; agent_name: str — 传给 _save_subagent_trajectory(...) 的子代理名称; filename_suffix: str — 传给 _save_subagent_trajectory(...) 的轨迹文件名后缀; summary_text: str — 传给 _save_subagent_trajectory(...) 的人类可读摘要; subagent_name_for_logging: str — 传给 _append_subagent_response_step(...) 的日志用名称
  • 读状态: self._llm, self._llm_call_kwargs, datetime.now(timezone.utc), time.time(), response.usage
  • 返回: 返回 (LLMResponse, SubagentTrajectoryRef):前者是本次 self._llm.call(...) 的原始响应对象,后者是 _save_subagent_trajectory(...) 返回的子代理轨迹引用。
  • 副作用: 原地修改传入的 steps:先追加一个 source="user" 的 prompt 步,再通过 _append_subagent_response_step(...) 追加响应步; 通过 _track_api_request_time(...) 记录一次 API 请求耗时; 通过 _update_subagent_metrics(response.usage) 间接更新子代理指标累积; 通过 _collect_subagent_rollout_detail(response) 收集本次响应的 rollout 细节; 通过 _save_subagent_trajectory(...) 持久化子代理轨迹

执行流

  1. 先以 len(steps) + 1 计算 prompt_step_id,并使用 datetime.now(timezone.utc).isoformat() 生成时间戳,向 steps 追加一个 Step(step_id=..., source="user", message=prompt);随后把响应步编号预定为 response_step_id = prompt_step_id + 1
  2. 调用前用 time.time() 记录 start_time,然后执行 await self._llm.call(prompt=prompt, message_history=message_history, **self._llm_call_kwargs) 获取 response
  3. LLM 调用返回后,立即把先前的 start_time 交给 _track_api_request_time(start_time),把这次调用耗时纳入统一计时记录。
  4. 从响应对象提取 usage_info = response.usage;该值随后被一份多用:先传给 _update_subagent_metrics(usage_info) 更新子代理资源消耗统计。
  5. 再调用 _append_subagent_response_step(steps, response_step_id, response, usage_info, subagent_name_for_logging),把模型响应及其 usage 信息写成新的轨迹步骤,继续原地扩充 steps
  6. 随后调用 _collect_subagent_rollout_detail(response) 收集响应相关的 rollout 明细,再以关键字参数形式调用 _save_subagent_trajectory(session_id=..., agent_name=..., steps=steps, usage_info=usage_info, filename_suffix=filename_suffix, summary_text=summary_text) 持久化轨迹并取得 trajectory_ref
  7. 最后返回 (response, trajectory_ref),把本次子代理的原始响应对象与保存后的轨迹引用一并交还调用方。

源码

    async def _run_subagent(
        self,
        prompt: str,
        message_history: list[dict],
        steps: list[Step],
        session_id: str,
        agent_name: str,
        filename_suffix: str,
        summary_text: str,
        subagent_name_for_logging: str = "subagent",
    ) -> tuple[LLMResponse, SubagentTrajectoryRef]:
        """Run a subagent and return its response and trajectory reference.

        This helper encapsulates the common pattern of:
        1. Appending the prompt step to the trajectory
        2. Calling the LLM with the prompt and message history
        3. Tracking API request time
        4. Updating subagent metrics
        5. Appending the response step to the trajectory
        6. Collecting rollout details
        7. Saving the subagent trajectory

        Args:
            prompt: The prompt to send to the LLM
            message_history: The message history to provide context
            steps: The trajectory steps (will be modified to include prompt and response steps)
            session_id: Session ID for the subagent
            agent_name: Name of the subagent (e.g., "terminus-2-summarization-summary")
            filename_suffix: Suffix for trajectory filename (e.g., "summary", "questions", "answers")
            summary_text: Human-readable summary for the trajectory ref
            subagent_name_for_logging: Name used in logging messages

        Returns:
            tuple: (LLM response, SubagentTrajectoryRef)
        """
        # Append the prompt step
        prompt_step_id = len(steps) + 1
        steps.append(
            Step(
                step_id=prompt_step_id,
                timestamp=datetime.now(timezone.utc).isoformat(),
                source="user",
                message=prompt,
            )
        )
        response_step_id = prompt_step_id + 1

        start_time = time.time()
        response: LLMResponse = await self._llm.call(
            prompt=prompt,
            message_history=message_history,
            **self._llm_call_kwargs,
        )
        self._track_api_request_time(start_time)

        usage_info = response.usage
        self._update_subagent_metrics(usage_info)

        self._append_subagent_response_step(
            steps,
            response_step_id,
            response,
            usage_info,
            subagent_name_for_logging,
        )

        self._collect_subagent_rollout_detail(response)

        trajectory_ref = self._save_subagent_trajectory(
            session_id=session_id,
            agent_name=agent_name,
            steps=steps,
            usage_info=usage_info,
            filename_suffix=filename_suffix,
            summary_text=summary_text,
        )

        return response, trajectory_ref

Non-obvious 设计决策

  • 该函数把“追加 prompt 步 → 发起 LLM 调用 → 记时/计量 → 追加响应步 → 保存轨迹”收束到单一辅助入口,目的是让不同子代理复用同一套会计与轨迹书写顺序;这在代码中体现为对 _track_api_request_time_update_subagent_metrics_append_subagent_response_step_collect_subagent_rollout_detail_save_subagent_trajectory 的固定串联调用。
  • 提示词步骤是在 self._llm.call(...) 之前先追加到内存中的 steps,说明实现选择优先保留“尝试发问”的过程顺序;但本函数没有 try/except,且真正持久化发生在 _save_subagent_trajectory(...) 末尾,因此若 LLM 调用中途抛错,代码只保证 steps 已被就地修改,不保证这次尝试一定落盘。
  • usage_info = response.usage 被显式提取为局部变量,并同时传给 _update_subagent_metrics(...)_save_subagent_trajectory(...),这是避免两处各自从 response 重新取值的写法,也使“统计口径”和“落盘口径”共享同一份 usage 数据源。
  • 轨迹保存函数采用全关键字参数调用 _save_subagent_trajectory(session_id=..., agent_name=..., ...),使会话标识、代理名、步骤集、usage、文件名后缀和摘要文本之间的对应关系在调用点上可直接辨认,降低多字符串参数并列时的位置误配风险。

上下游关系

  • 调用方: Terminus2._summarize(该 stage 中负责串联多个子代理调用的上层函数)
  • 核心被调用: self._llm.call; self._track_api_request_time; self._update_subagent_metrics; self._append_subagent_response_step; self._collect_subagent_rollout_detail; self._save_subagent_trajectory; Step
  • 配置/状态来源: self._llm; self._llm_call_kwargs; datetime.now(timezone.utc); time.time()
  • 结果去向: 返回给调用方的 LLMResponse; 返回给调用方的 SubagentTrajectoryRef; 传入的 steps 列表被继续扩充后供后续保存与上层使用; 子代理轨迹持久化产物由 _save_subagent_trajectory(...) 产生; 子代理计时与指标累计经辅助方法写入实例级状态
  • 同类 sibling: Terminus2._summarize:上层负责编排多个子代理,本函数只负责单次子代理执行; Terminus2._prepare_copied_trajectory_steps:为子代理准备可安全续写的步骤副本,本函数消费这类 steps 并继续追加内容; Terminus2._remove_metrics_from_copied_steps:清洗复制来的历史指标,本函数随后为新响应重新记录 usage/指标
寄存器交互
Terminus2._collect_subagent_rollout_detailterminus_2.py:594–616 ↗

子代理 rollout 明细归档器

stage 上下文: 该函数位于 side-S1 的上下文总结子流程内部,不直接参与三步总结内容生成,而是为子代理 LLM 调用补充可选的 rollout 级观测数据。它由兄弟函数 _run_subagent 在拿到子代理 response 后调用,与 _save_subagent_trajectory_append_subagent_response_step 一样属于总结流程的配套记账逻辑。相较于 _summarize 负责改写 chat._messages 和产出 handoff prompt,本函数只处理子代理响应中的调试/分析明细。

这段代码在干什么

函数接收一个子代理 LLMResponse,从中抽取 prompt_token_idscompletion_token_idslogprobsextra 这些可选 rollout 字段。若实例开关 self._collect_rollout_details 关闭,则立即返回;若开启,则把存在的字段规范化组装为一个 RolloutDetail 字典,并在字典非空时追加到 self._subagent_rollout_details。函数本身返回 None,真正产出是对已缓存子代理 rollout 明细列表的追加写入。

接口 · 参数 / IO

(self, response: LLMResponse) -> None

  • 参数: response: LLMResponse — 子代理一次 LLM 调用返回的响应对象,提供 token ids、logprobs 与 extra 元数据来源
  • 读状态: self._collect_rollout_details, self._subagent_rollout_details
  • 返回: 返回 None;真正结果是在满足条件时向 self._subagent_rollout_details 追加一条 RolloutDetail 记录。
  • 副作用: 当 self._collect_rollout_details 为真且响应中至少有一个相关字段时,写入 self._subagent_rollout_details.append(rollout_detail)

执行流

  1. 先检查实例开关 self._collect_rollout_details;若为假,直接返回,不做任何提取或写入。
  2. 初始化空的 rollout_detail: RolloutDetail = {},作为本次子代理响应的待收集明细容器。
  3. 按字段顺序检查 response.prompt_token_idsresponse.completion_token_idsresponse.logprobs;每个字段只有在显式不是 None 时才写入 rollout_detail,且统一包成单元素列表。
  4. response.extra 不是 None,则把其中每个 key: value 转换成 key: [value] 的形式后整体写入 rollout_detail["extra"]
  5. 最后检查 rollout_detail 是否非空;只有至少收集到一个字段时,才将其追加到 self._subagent_rollout_details,否则静默结束。

源码

    def _collect_subagent_rollout_detail(self, response: LLMResponse) -> None:
        """Collect rollout details from a subagent LLM response.

        Args:
            response: The LLM response containing token IDs and logprobs
        """
        if not self._collect_rollout_details:
            return

        rollout_detail: RolloutDetail = {}
        if response.prompt_token_ids is not None:
            rollout_detail["prompt_token_ids"] = [response.prompt_token_ids]
        if response.completion_token_ids is not None:
            rollout_detail["completion_token_ids"] = [response.completion_token_ids]
        if response.logprobs is not None:
            rollout_detail["logprobs"] = [response.logprobs]
        if response.extra is not None:
            rollout_detail["extra"] = {
                key: [value] for key, value in response.extra.items()
            }

        if rollout_detail:
            self._subagent_rollout_details.append(rollout_detail)

Non-obvious 设计决策

  • 函数把总开关 self._collect_rollout_details 放在最前面短路返回,体现的是“默认不承担额外采集成本”的取舍;否则即便调用方总是传入 response,这里也会持续构造明细对象并占用存储。
  • 各字段都使用 is not None 而不是一般真值判断,说明设计上要区分“字段为空/零值但有效”与“字段缺失”;这样不会误丢空列表、空结构等可能有意义的 rollout 数据。
  • 无论是 prompt_token_idscompletion_token_idslogprobs,还是 extra 的各个 value,都会被包成单元素列表,这一选择保留了 rollout-oriented 的统一模式,使子代理单次响应与其他可能的多样本/多步明细结构保持形状一致。
  • 函数仅在 rollout_detail 非空时才追加到 self._subagent_rollout_details,避免写入无信息的空字典;这比无条件追加更利于后续消费方把列表长度理解为“确实采集到明细的响应次数”。

上下游关系

  • 调用方: Terminus2._run_subagent
  • 核心被调用: list.append(用于 self._subagent_rollout_details.append(...)); dict.items(遍历 response.extra.items() 生成规范化副本)
  • 配置/状态来源: self._collect_rollout_details(控制是否启用采集); response.prompt_token_ids; response.completion_token_ids; response.logprobs; response.extra
  • 结果去向: self._subagent_rollout_details(缓存子代理 rollout 明细); 后续与子代理轨迹/调试信息相关的消费路径; side-S1 子代理执行的附属记账结果; 与 _run_subagent 产出的其他副作用并列保存
  • 同类 sibling: Terminus2._run_subagent:负责发起子代理 LLM 调用并在收到 response 后调用本函数收集 rollout 明细。; Terminus2._summarize:负责 orchestrate 三个子代理,本函数只服务于其中每次 _run_subagent 的附属数据采集,不参与 handoff prompt 生成。
Terminus2._save_subagent_trajectoryterminus_2.py:1757–1822 ↗

子代理轨迹落盘与引用生成器

stage 上下文: 该函数位于 summarization 侧流程的收尾位置,职责是把某个子代理已经形成的 steps 与用量信息封装成可持久化的 Trajectory。它不参与三步总结仪式本身的推理,只负责为该仪式产出一个独立轨迹文件与对应的 SubagentTrajectoryRef。相较同 stage 的 _run_subagent,这里聚焦的是落盘与引用构造,而不是发起 LLM 调用。

这段代码在干什么

函数接收子代理的 session_idagent_namestepsusage_info、文件名后缀和摘要文本,先从 usage_info 推导 token/cost 汇总,再据此构造一个 Trajectory 对象。随后它尝试把该轨迹写入 self.logs_dir 下的 summarization 专用 JSON 文件,并无论写入是否成功都返回一个 SubagentTrajectoryRef。可见副作用仅为文件写入与日志记录,不修改任何实例内寄存状态。

接口 · 参数 / IO

(self, session_id: str, agent_name: str, steps: list[Step], usage_info, filename_suffix: str, summary_text: str) -> SubagentTrajectoryRef

  • 参数: session_id: str — 写入子代理 Trajectory.session_id,并同步写入返回引用的 session_id; agent_name: str — 写入 Agent.name,标识当前保存的是哪个子代理轨迹; steps: list[Step] — 作为轨迹正文写入 Trajectory.steps; usage_info: ? — 交给 _extract_usage_metrics(...) 推导 prompt/completion/cached/cost 汇总; filename_suffix: str — 参与生成轨迹文件名 trajectory.summarization-{count}-{suffix}.json,也用于日志文案; summary_text: str — 写入返回的 SubagentTrajectoryRef.extra['summary']
  • 读状态: self._model_name, self._session_id, self._summarization_count, self.logs_dir, self.logger, self.version(), self._extract_usage_metrics
  • 返回: 返回一个 SubagentTrajectoryRef:其中 session_id 取自入参,trajectory_path 为生成出的文件名,extra.summary 为入参 summary_text。另外,轨迹内的 Agent.version 来自 self.version() or "unknown"
  • 副作用: 尝试向 self.logs_dir / f"trajectory.summarization-{self._summarization_count}-{filename_suffix}.json" 写入 JSON 轨迹文件; 写入成功时调用 self.logger.debug(...) 记录保存位置; 写入失败时调用 self.logger.error(...) 记录异常

执行流

  1. 先调用 self._extract_usage_metrics(usage_info),把输入用量信息拆成 total_prompttotal_completiontotal_cachedtotal_cost 四个汇总值。
  2. 基于这些汇总值与入参构造 Trajectory:其中 Agent.name 来自 agent_nameAgent.versionself.version() or "unknown"Agent.model_nameself._model_nameAgent.extra 固定写入 parent_session_id=self._session_idsummarization_index=self._summarization_count
  3. 同时构造 FinalMetrics:prompt/completion/cached token 直接填入;total_cost_usd 仅在 total_cost > 0 时保留原值,否则显式写为 None
  4. 根据 self.logs_dirself._summarization_countfilename_suffix 生成目标路径 trajectory.summarization-...json,随后在 try 块中用 open(..., "w")format_trajectory_json(trajectory.to_json_dict()) 执行落盘。
  5. 若落盘成功,则通过 self.logger.debug(...) 记录“哪类子代理轨迹已保存到哪个路径”;若抛出异常,则在 except 中通过 self.logger.error(...) 记录失败原因。
  6. 最后无条件返回 SubagentTrajectoryRef(session_id=session_id, trajectory_path=str(trajectory_path.name), extra={"summary": summary_text});返回值只引用目标文件名,不检查文件是否实际存在。

源码

    def _save_subagent_trajectory(
        self,
        session_id: str,
        agent_name: str,
        steps: list[Step],
        usage_info,
        filename_suffix: str,
        summary_text: str,
    ) -> SubagentTrajectoryRef:
        """Save a subagent trajectory to disk and return its reference.

        Args:
            session_id: Session ID for the subagent
            agent_name: Name of the subagent (e.g., "terminus-2-summarization-summary")
            steps: List of trajectory steps
            usage_info: Usage information from LLM response
            filename_suffix: Suffix for trajectory filename (e.g., "summary", "questions", "answers")
            summary_text: Human-readable summary for the trajectory ref extra field

        Returns:
            SubagentTrajectoryRef for inclusion in parent trajectory
        """
        total_prompt, total_completion, total_cached, total_cost = (
            self._extract_usage_metrics(usage_info)
        )

        trajectory = Trajectory(
            session_id=session_id,
            agent=Agent(
                name=agent_name,
                version=self.version() or "unknown",
                model_name=self._model_name,
                extra={
                    "parent_session_id": self._session_id,
                    "summarization_index": self._summarization_count,
                },
            ),
            steps=steps,
            final_metrics=FinalMetrics(
                total_prompt_tokens=total_prompt,
                total_completion_tokens=total_completion,
                total_cached_tokens=total_cached,
                total_cost_usd=total_cost if total_cost > 0 else None,
            ),
        )

        trajectory_path = (
            self.logs_dir
            / f"trajectory.summarization-{self._summarization_count}-{filename_suffix}.json"
        )
        try:
            with open(trajectory_path, "w") as f:
                f.write(format_trajectory_json(trajectory.to_json_dict()))
            self.logger.debug(
                f"{filename_suffix.capitalize()} subagent trajectory saved to {trajectory_path}"
            )
        except Exception as save_error:
            self.logger.error(
                f"Failed to save {filename_suffix} subagent trajectory: {save_error}"
            )

        return SubagentTrajectoryRef(
            session_id=session_id,
            trajectory_path=str(trajectory_path.name),
            extra={"summary": summary_text},
        )

Non-obvious 设计决策

  • 选择在本函数内立即把 usage_info 归约为 FinalMetrics,而不是把原始用量对象直接塞进轨迹;这样保存产物只暴露统一的汇总字段,代价是更细粒度的原始结构不会被保留在该文件对象中。
  • 选择把 Agent.version 写成 self.version() or "unknown",即当 self.version() 返回假值时回落到字符串常量;这是代码中明确的容错分支,避免轨迹里出现空版本值。
  • 选择把 total_cost_usdtotal_cost <= 0 时改写为 None,而不是原样写入 0 或负值;这是字段层面的显式规范化,代码只证明了这种值映射行为。
  • 选择将文件写入包在 try/except Exception 中,并在失败后继续返回 SubagentTrajectoryRef;这一取舍说明“生成引用对象”与“写盘成功”被解耦,调用方不会因为保存失败而拿不到引用结构。

上下游关系

  • 调用方: 未在本函数源码中直接体现; 该方法作为实例私有辅助函数被外部同类方法调用,具体调用点不在此代码段内
  • 核心被调用: self._extract_usage_metrics; self.version; format_trajectory_json; trajectory.to_json_dict; open; self.logger.debug; self.logger.error
  • 配置/状态来源: self._model_name:填入 Agent.model_name; self._session_id:填入 Agent.extra.parent_session_id; self._summarization_count:填入 Agent.extra.summarization_index 并参与文件名构造; self.logs_dir:决定轨迹落盘目录; self.version():决定 Agent.version,假值时回落到 "unknown"
  • 结果去向: 返回给调用方的 SubagentTrajectoryRef; 写入 self.logs_dir 下的 summarization 轨迹 JSON 文件; 调试日志中的成功记录; 错误日志中的失败记录
  • 同类 sibling: Terminus2._run_subagent:后者负责拿到 steps 与 LLM 响应,本函数只负责把这些结果封装并落盘; Terminus2._prepare_copied_trajectory_steps:后者准备可复用的历史步骤,本函数消费现成 steps 生成独立轨迹文件; Terminus2._collect_subagent_rollout_detail:后者收集 rollout 明细,本函数收集并保存的是汇总后的轨迹与 final metrics
寄存器交互
Terminus2._convert_chat_messages_to_stepsterminus_2.py:1824–1882 ↗

聊天消息到轨迹步骤的转换器

stage 上下文: 该单元把 chat_messages 形式的消息列表改写为 Step 对象列表,属于一种表示层转换。源码本身未显式体现 stage 内触发条件;从函数体可见,它只依赖传入参数与少量模型名状态,独立完成转换并返回结果。它与同类轨迹处理函数的关系,在本片段中唯一可证的是:产物类型同为 Step

这段代码在干什么

函数接收 list[dict] 形式的聊天消息,逐条读取必需键 msg["role"]msg["content"],构造新的 Step 列表并返回。遇到 role == "assistant" 时,会把步骤来源改写为 "agent",并为该步骤补上 model_name=self._last_response_model_name or self._model_name;其他角色则直接沿用原 role,且不写入 model_name。若 mark_as_copied 为真,函数会把本轮生成的每个步骤都标记为 is_copied_context=True。最后仅在 if additional_user_message: 成立时,才额外追加一条新的 user 步骤,且该条不会经过 copied 标记逻辑。

接口 · 参数 / IO

(self, chat_messages: list[dict], additional_user_message: str | None = None, mark_as_copied: bool = False) -> list[Step]

  • 参数: chat_messages: list[dict] — 输入消息序列;每个元素都要可用 msg["role"]msg["content"] 直接取值; additional_user_message: str | None — 可选附加用户消息;仅在 truthy 时追加为最后一条 Step; mark_as_copied: bool — 控制是否给循环中生成的每个步骤写入 is_copied_context=True
  • 读状态: self._last_response_model_name, self._model_name
  • 返回: 返回新构造的 list[Step]。列表包含对 chat_messages 的逐条转换结果,以及在 if additional_user_message: 为真时追加的一条最终 user 步骤。
  • 副作用: 不修改 self 上的实例状态; 会修改本函数内部新建的 Step 对象字段,例如 model_nameis_copied_context

执行流

  1. 初始化空列表 steps = [] 与起始编号 step_id = 1
  2. 遍历 chat_messages;对每个 msg 直接读取 msg["role"]msg["content"],代码中没有缺键容错分支。
  3. role == "assistant",则把 source 设为 "agent",并创建带 model_name=self._last_response_model_name or self._model_nameStep;否则把 source 直接设为原 role,创建不含 model_name 参数的 Step
  4. mark_as_copied 为真,则对刚创建的该条 Step 写入 step.is_copied_context = True
  5. 把该 Step 追加到 steps,并将 step_id 递增 1。
  6. 循环结束后,执行 if additional_user_message: 判断;条件成立时,再追加一条 source="user"message=additional_user_message 的新 Step,其 step_id 使用当前值。
  7. 返回 steps

源码

    def _convert_chat_messages_to_steps(
        self,
        chat_messages: list[dict],
        additional_user_message: str | None = None,
        mark_as_copied: bool = False,
    ) -> list[Step]:
        """Convert chat messages to trajectory steps.

        Args:
            chat_messages: List of chat messages with 'role' and 'content' fields
            additional_user_message: Optional additional user message to append as final step
            mark_as_copied: If True, mark all steps with is_copied_context=True (for continuation trajectories)

        Returns:
            List of Step objects representing the chat history
        """
        steps = []
        step_id = 1

        for msg in chat_messages:
            role = msg["role"]
            content = msg["content"]

            # Map chat role to trajectory source
            if role == "assistant":
                source = "agent"
                step = Step(
                    step_id=step_id,
                    source=source,
                    message=content,
                    model_name=self._last_response_model_name or self._model_name,
                )
            else:
                source = role
                step = Step(
                    step_id=step_id,
                    source=source,
                    message=content,
                )

            # Mark as copied context if this is for a continuation trajectory
            if mark_as_copied:
                step.is_copied_context = True

            steps.append(step)
            step_id += 1

        # Add the additional user message if provided
        # Note: The additional user message is NOT marked as copied since it's the new handoff prompt
        if additional_user_message:
            steps.append(
                Step(
                    step_id=step_id,
                    source="user",
                    message=additional_user_message,
                )
            )

        return steps

Non-obvious 设计决策

  • 函数显式把聊天角色 "assistant" 重映射为轨迹来源 "agent",说明聊天协议中的角色命名与 Step.source 的目标命名并不完全相同;代码只对这一种角色做特殊处理,其他角色保持原值。
  • assistant 分支使用 self._last_response_model_name or self._model_name 作为模型名回退链,体现出“优先使用最近一次响应模型名,缺失时退回默认模型名”的选择;这避免了 assistant 步骤在前者为空时没有 model_name
  • 只有 assistant 派生的 Step 会写入 model_name,而非 assistant 步骤不会补该字段;这是构造参数层面的刻意区分,而不是统一赋默认值。
  • copied 标记只作用于循环中由 chat_messages 转换出的步骤;内联注释明确说明,额外追加的用户消息即使存在也不标记为 copied。
  • 附加用户步骤采用 if additional_user_message: 的 truthy 判断,而不是显式检查 is not None;因此空字符串等假值不会被追加,这是一种比类型注解更收紧的实际行为。

上下游关系

  • 调用方: 源码片段未展示直接调用方;本函数被定义为 Terminus2 的实例方法
  • 核心被调用: Step(...) 构造函数
  • 配置/状态来源: self._last_response_model_name; self._model_name; 参数 chat_messages; 参数 additional_user_message; 参数 mark_as_copied
  • 结果去向: 返回给调用者的 list[Step]; 列表中每个元素都是新建的 Step 实例