Run Onset
Opening Explanation
This stage exists to turn a prepared environment into a clean, first-class run that the agent can actually reason over. Environment setup gave Terminus 2 a place to act. Run Onset makes that place usable for this specific task. It clears leftovers from any earlier run, captures the current terminal view, builds the full starting instruction, and records that starting point as the first item in the run’s trajectory (the canonical step-by-step record of what happened). This stage sits right before the core loop because the loop should only begin once the agent has a fresh identity, a clear prompt, and an accurate picture of the terminal. Without this stage, the loop would start half-blind and with stale state mixed in.
Main Flow
_reset_per_run_state()(clear run-specific memory) runs first.Its job is to wipe data that only makes sense for one run: old trajectory steps, counters, pending handoffs, subagent bookkeeping, timestamps, and the current session id. This matters because the same
Terminus2instance can be reused. Without a reset here, one task could leak into the next.- A fresh
Chat(...)object is created.Chat is the thin wrapper around the LLM call. A new one is made here so the upcoming run has its own clean conversation context rather than inheriting transient state from a prior run.
- The agent captures the initial terminal screen with
session.get_incremental_output().This is the agent’s first real observation. The terminal is the world it acts in, so the loop should start from what is actually on screen now, not from assumptions.
- The user instruction is expanded before the first prompt is built.
If MCP servers are present, their description is added. MCP servers are external tools exposed to the agent through a standard interface. Then
_build_skills_section()(assemble the dynamic list of available skills) adds the current capabilities the agent can use. This matters because the model should know both the task and the tools it has before it plans. - The initial prompt is formatted from the instruction plus terminal state.
This creates the starting package the model will reason over: what to do, what tools exist, and what the terminal currently looks like.
- That prompt is written as trajectory step 1 with
source="user".This is important for traceability. The run now has an explicit starting record, so later steps can be understood relative to a known beginning.
- Only then does control pass into the iteration loop, wrapped in
try/finally.The key idea is that the core loop should begin only after initialization is complete and the run has a durable starting record.
State Flow
- writes:
reg-trajectory-steps— writes the initial prompt as step 1 so the run has a canonical starting record - reads:
reg-trajectory-steps— this stage treats it as prior-run data that must not carry forward - clears:
reg-trajectory-steps—_reset_per_run_state()clears it so stale history does not leak into the new run - triggers downstream:
stage-4 Iteration Loop ★ Core ★— once reset, prompt construction, and initial observation are complete
Pipeline Hand-Off
Upstream gives this stage a ready environment: the terminal/session exists, but it is not yet packaged as a fresh run. This stage turns that into a clean run start — fresh per-run state, a complete initial prompt, the first terminal snapshot, and trajectory step 1 — which the iteration loop then consumes as the basis for all further thinking and action.
Terminus2._reset_per_run_stateterminus_2.py:1569–1584 ↗
Per-run state reset and session id selection
Stage context: This helper opens stage-3 by clearing state that must not leak across separate
Terminus2.run()invocations.Terminus2.runcalls it before it creates the freshChat, captures terminal output, and seeds the initial prompt. In this stage, it is the boundary between long-lived instance configuration and run-scoped accumulators.
What this code does
_reset_per_run_state reinitializes all run-scoped fields on self before a new run() begins. It takes no inputs beyond self, reads _user_provided_session_id, and overwrites the trajectory list, timing list, counters, pending handoff/completion state, marker storage, subagent tracking, and _session_id. Its only product is this reset state on the reused Terminus2 instance.
Interface · params / IO
(self) -> None
- params:
self:?— TheTerminus2instance whose run-scoped state is being reinitialized. - reads:
self._user_provided_session_id - returns: Returns
None; it prepares a clean per-run state onselffor the upcomingrun()invocation. - effects: Writes
self._trajectory_steps; Writesself._api_request_times; Writesself._n_episodes; Writesself._summarization_count; Writesself._subagent_metrics; Writesself._subagent_rollout_details; Writesself._pending_completion; Writesself._pending_subagent_refs; Writesself._pending_handoff_prompt; Writesself._timestamped_markers; Writesself._session_id; Constructs a newSubagentMetrics()instance; May generate a UUID viauuid.uuid4()when no user session id is supplied
Execution flow
- Replace per-run collection fields with empty containers:
_trajectory_steps,_api_request_times,_subagent_rollout_details, and_timestamped_markersbecome fresh empty lists. - Reset run counters to their starting values by setting
_n_episodesand_summarization_countto0. - Replace
_subagent_metricswith a newSubagentMetrics()accumulator for this run. - Clear cross-iteration pending state by setting
_pending_completiontoFalseand_pending_subagent_refsand_pending_handoff_prompttoNone. - Choose the run's session id by reusing
_user_provided_session_idwhen it is truthy, otherwise generatingstr(uuid.uuid4())and storing it in_session_id.
Source
def _reset_per_run_state(self) -> None:
"""Reset all per-run state. The same Terminus2 instance is reused
across multiple `run()` invocations in multi-step trials, so any
accumulator that should be scoped to a single step must be reset here.
"""
self._trajectory_steps = []
self._api_request_times = []
self._n_episodes = 0
self._summarization_count = 0
self._subagent_metrics = SubagentMetrics()
self._subagent_rollout_details = []
self._pending_completion = False
self._pending_subagent_refs = None
self._pending_handoff_prompt = None
self._timestamped_markers = []
self._session_id = self._user_provided_session_id or str(uuid.uuid4())
Non-obvious design decisions
- The function centralizes all run-scoped resets in one place because the class instance survives across multiple
run()calls. Without this dedicated scrub step, old_trajectory_steps, counters, or pending flags could bleed into the next trial step. - It creates fresh containers and a fresh
SubagentMetrics()object instead of trying to clear nested state in place. That avoids carrying over references or partially reset subagent accounting. - It preserves a caller-supplied
_user_provided_session_idbut falls back to a generated UUID when none is present. This balances stable external session naming with automatic uniqueness for ordinary runs.
Relations
- Callers: Terminus2.run
- Core callees: SubagentMetrics; uuid.uuid4
- Config / state sources: self._user_provided_session_id
- Results to: stage-3
Terminus2.runsetup that constructs a freshChat; stage-3 initial prompt seeding into_trajectory_steps; stage-4 iteration loop, which starts counting episodes from_n_episodes == 0; stage-4.5 / 4.6 / 4.8 / 4.9, which append new run data into cleared accumulators; stage-5 metadata and totals assembly, which reads per-run counters and timings
reg-pending-completion— start new run with no completion pendingreg-pending-handoff-prompt— clear stale summary handoff promptreg-pending-subagent-refs— clear stale subagent trajectory refsreg-n-episodes— restart iteration counter at zeroreg-summarization-count— restart summarization count for runreg-trajectory-steps— begin fresh trajectory for this runreg-asciinema-markers— discard markers from prior runreg-subagent-metrics— replace subagent accounting accumulatorreg-api-request-times— clear per-request timing history
Terminus2.runterminus_2.py:1592–1631 ↗
run setup and initial prompt seeding
Stage context: This region performs the front half of
Terminus2.run()before the main iteration loop starts. It followsTerminus2._reset_per_run_state, then establishes the fresh chat/session context and seeds the first trajectory entry that the loop will build on. Within stage 3, it is the bridge from reset state to a fully prepared first prompt.
What this code does
This region prepares a new run from the current instruction, environment, and context. It resets run-scoped state, creates a fresh Chat in self._chat, verifies that self._session exists, captures the current terminal output, augments the instruction with mcp_servers details and _build_skills_section(environment), formats _prompt_template, and records that formatted prompt as the first Step in _trajectory_steps. It does not return a value; its product is initialized run state plus an initial user-authored trajectory record.
Interface · params / IO
(self, instruction: str, environment: dict[str, str], context: ContextT) -> TrialT
- params:
self:Terminus2— runner instance that owns session, prompt, and run-scoped state;instruction:str— base task instruction to augment and embed in the initial prompt;environment:dict[str, str]— environment mapping passed into_build_skills_section;context:ContextT— per-run context object stored onself._context - reads:
self._llm,self._interleaved_thinking,self._session,self.mcp_servers,self._prompt_template,self._trajectory_steps - returns: No direct return in this region; it produces initialized run state on
self, a formatted initial prompt, and the first appended trajectory step. - effects: calls
self._reset_per_run_state()to clear and rotate run-local state; writesself._chatwith a newChat(self._llm, interleaved_thinking=self._interleaved_thinking); writesself._context; awaitsself._session.get_incremental_output()to read external terminal state; callsself._limit_output_length(...)to truncate terminal output for prompting; awaitsself._build_skills_section(environment); appends a newStep(...)toself._trajectory_steps
Execution flow
- Reset run-scoped fields with
self._reset_per_run_state(), create a freshChatinself._chatfrom_llmand_interleaved_thinking, and store the incomingcontextonself._context. - Validate that
self._sessionis present; if it isNone, stop immediately withRuntimeError("Session is not set"). - Read the current terminal snapshot by awaiting
self._session.get_incremental_output(), then pass that output through_limit_output_length(...)to produceterminal_statefor the first prompt. - Start from
instructionasaugmented_instruction, and ifself.mcp_serversis non-empty, append a generatedMCP Servers:section that describes each server from itsname,transport, and eithercommandplusargsorurl. - Build a dynamic skills block with
await self._build_skills_section(environment)and append it toaugmented_instructionwhen the returnedskills_sectionis truthy. - Format
self._prompt_templatewithinstruction=augmented_instructionandterminal_state=terminal_state, then append that prompt toself._trajectory_stepsasStep(step_id=1, source="user", ...)with a UTC ISO timestamp.
Source
self._reset_per_run_state()
self._chat = Chat(self._llm, interleaved_thinking=self._interleaved_thinking)
self._context = context
if self._session is None:
raise RuntimeError("Session is not set")
# Get the terminal state for the initial prompt
terminal_state = self._limit_output_length(
await self._session.get_incremental_output()
)
augmented_instruction = instruction
if self.mcp_servers:
mcp_info = "\n\nMCP Servers:\nThe following MCP servers are available for this task.\n"
for s in self.mcp_servers:
if s.transport == "stdio":
args_str = " ".join(s.args)
mcp_info += f"- {s.name}: stdio transport, command: {s.command} {args_str}\n"
else:
mcp_info += f"- {s.name}: {s.transport} transport, url: {s.url}\n"
augmented_instruction = instruction + mcp_info
skills_section = await self._build_skills_section(environment)
if skills_section:
augmented_instruction += skills_section
initial_prompt = self._prompt_template.format(
instruction=augmented_instruction,
terminal_state=terminal_state,
)
self._trajectory_steps.append(
Step(
step_id=1,
timestamp=datetime.now(timezone.utc).isoformat(),
source="user",
message=initial_prompt,
)
)
Non-obvious design decisions
- It creates a brand-new
Chatafter_reset_per_run_state()instead of reusing an existing one. That keepschat.messagesisolated per run; reuse would risk carrying old model history into a new trial. - It raises on
self._session is Nonebefore prompt construction. That fails fast at the first point where terminal state is required; a later failure would leave partially initialized run state and a misleading first prompt. - It captures terminal output through
_limit_output_length(await self._session.get_incremental_output())before formatting_prompt_template. This bounds prompt size using a dedicated limiter instead of embedding raw terminal output, which would make prompt growth depend directly on session backlog. - It embeds
mcp_serversand_build_skills_section(environment)into the instruction text itself rather than storing them separately on the step. That makes the exact prompt content visible in the first trajectoryStep, at the cost of duplicating derived context into the recorded message.
Relations
- Callers:
Terminus2.run - Core callees:
Terminus2._reset_per_run_state;Chat;Terminus2._limit_output_length;TmuxSession.get_incremental_outputviaself._session;Terminus2._build_skills_section;self._prompt_template.format;Step - Config / state sources:
self._llm;self._interleaved_thinking;self.mcp_servers;self._prompt_template;self._session - Results to:
self._chatfor later LLM interaction;self._contextfor later run stages;self._trajectory_stepsas the seed of the run record; the subsequent iteration loop inTerminus2.run - Related siblings:
Terminus2._reset_per_run_state: clears all run-scoped registers before this region seeds fresh state
reg-pending-completion— cleared indirectly by_reset_per_run_statereg-pending-handoff-prompt— cleared indirectly by_reset_per_run_statereg-pending-subagent-refs— cleared indirectly by_reset_per_run_statereg-n-episodes— zeroed indirectly by_reset_per_run_statereg-summarization-count— zeroed indirectly by_reset_per_run_statereg-trajectory-steps— cleared before seeding first stepreg-trajectory-steps— append initial prompt as user stepreg-asciinema-markers— cleared indirectly by_reset_per_run_statereg-subagent-metrics— cleared indirectly by_reset_per_run_statereg-api-request-times— cleared indirectly by_reset_per_run_state
Terminus2._build_skills_sectionterminus_2.py:422–473 ↗
Remote skills XML section builder
Stage context: This helper runs during stage-3 prompt assembly, before
Terminus2.run()enters its main loop.run()uses it alongside MCP-server augmentation to expand the initial instruction text. Unlike_reset_per_run_state, it does not prepare mutable run state; it only probes the providedenvironmentand returns optional prompt content.
What this code does
_build_skills_section inspects self.skills_dir in the given environment and looks for SKILL.md files one directory below it. For each discovered file, it reads the file content, parses frontmatter through self._parse_skill_frontmatter, and collects (fm["name"], fm["description"], skill_md_path) entries. It returns None when self.skills_dir is falsy, environment.is_dir(self.skills_dir) is false, the find command fails or yields blank output, or no files survive the cat and frontmatter checks. Otherwise it returns an indented <available_skills> XML string and does not mutate instance state.
Interface · params / IO
(self, environment: BaseEnvironment) -> str | None
- params:
environment:BaseEnvironment— Remote filesystem and command-execution backend used for directory checks and file reads - reads:
self.skills_dir,self._parse_skill_frontmatter - returns:
Nonefor all early-exit cases, or a newline-prefixed XML string fromxml.etree.ElementTree.tostring(...)containing one<skill>per surviving entry - effects: Calls
environment.is_dir(self.skills_dir); Runsenvironment.exec(..., timeout_sec=10)with afind ... -name SKILL.md -type f | sortcommand; Runsenvironment.exec(f"cat {shlex.quote(skill_md_path)}", timeout_sec=10)for each discovered path
Execution flow
- Return
Noneimmediately ifself.skills_diris falsy, then returnNoneagain ifawait environment.is_dir(self.skills_dir)is false. - Scan the remote directory by calling
environment.exec(...)withfind {shlex.quote(self.skills_dir)} -mindepth 2 -maxdepth 2 -name SKILL.md -type f | sortandtimeout_sec=10; returnNoneif the command fails, ifstdoutis missing, or ifstdout.strip()is empty. - Split the
findoutput intoskill_md_paths, initializeentries, and for each path runenvironment.exec(f"cat {shlex.quote(skill_md_path)}", timeout_sec=10). - Skip a path when
cat_result.return_code != 0orcat_result.stdoutis falsy; otherwise parse the file text withself._parse_skill_frontmatter(cat_result.stdout)and skip the path again if that parser returnsNone. - For each successful parse, append exactly
(fm["name"], fm["description"], skill_md_path)toentries. - Return
Noneifentriesis empty; otherwise importElement,SubElement,indent, andtostring, build an<available_skills>tree with<name>,<description>, and<location>children, indent it with two spaces, and return"\n" + tostring(root, encoding="unicode").
Source
async def _build_skills_section(self, environment: BaseEnvironment) -> str | None:
"""Discover Agent Skills in skills_dir and return an <available_skills> XML block.
Follows the Agent Skills spec: scans for subdirectories containing SKILL.md
inside the remote environment, parses YAML frontmatter for name/description,
and provides the absolute path so the model can ``cat`` the file to activate
a skill.
"""
if not self.skills_dir:
return None
if not await environment.is_dir(self.skills_dir):
return None
# List subdirectories containing SKILL.md in the remote environment
result = await environment.exec(
f"find {shlex.quote(self.skills_dir)} -mindepth 2 -maxdepth 2"
" -name SKILL.md -type f | sort",
timeout_sec=10,
)
if result.return_code != 0 or not result.stdout or not result.stdout.strip():
return None
skill_md_paths = result.stdout.strip().splitlines()
entries: list[tuple[str, str, str]] = [] # (name, description, location)
for skill_md_path in skill_md_paths:
cat_result = await environment.exec(
f"cat {shlex.quote(skill_md_path)}", timeout_sec=10
)
if cat_result.return_code != 0 or not cat_result.stdout:
continue
fm = self._parse_skill_frontmatter(cat_result.stdout)
if fm is None:
continue
entries.append((fm["name"], fm["description"], skill_md_path))
if not entries:
return None
from xml.etree.ElementTree import Element, SubElement, indent, tostring
root = Element("available_skills")
for name, description, location in entries:
skill = SubElement(root, "skill")
SubElement(skill, "name").text = name
SubElement(skill, "description").text = description
SubElement(skill, "location").text = location
indent(root, space=" ")
return "\n" + tostring(root, encoding="unicode")
Non-obvious design decisions
- The function treats every discovery problem as absence, not as an exception path. The repeated
return Nonechecks aroundself.skills_dir,environment.is_dir(...), thefindresult,catresults, andself._parse_skill_frontmatter(...)keep prompt construction tolerant of missing or unusable skill data instead of makingrun()fail. - It performs all discovery through
environment.exec(...)andenvironment.is_dir(...)instead of local filesystem APIs. That choice keeps the scan aligned with the remote execution context that ownsskills_dir; using local file access here would inspect the wrong machine. - It preserves the discovered
skill_md_pathverbatim in the<location>element. The code does not normalize or reinterpret the path, which avoids inventing path semantics and makes the XML reflect exactly what the remotefindcommand returned. - The XML helpers are imported only after
if not entries: return None. That defers XML-tree setup work to the only path that needs it and keeps the no-skills path cheap.
Relations
- Callers: Terminus2.run
- Core callees: environment.is_dir; environment.exec; self._parse_skill_frontmatter; shlex.quote; xml.etree.ElementTree.Element; xml.etree.ElementTree.SubElement; xml.etree.ElementTree.indent; xml.etree.ElementTree.tostring
- Config / state sources: self.skills_dir
- Results to: Terminus2.run uses the returned string from
_build_skills_section(environment)while augmenting the initial instruction - Related siblings: Terminus2.run; Terminus2._reset_per_run_state