Command Execute → Observation → Trajectory Step
Opening Explanation
This stage exists to turn “the agent asked to do something” into “the system now has a durable record of what happened next.” It owns the normal path after the model has chosen terminal commands: run them in the terminal, capture the result, decide what the agent should see as the next observation, and save that whole step into the trajectory. The trajectory is the run log the agent builds over time. This stage sits between decision and control flow. Upstream, the model has already produced commands and maybe claimed the task is done. Downstream, the loop needs a clean observation and a reliable completion signal so it can either continue or stop.
Main Flow
_execute_commands()(send the chosen keystrokes to the terminal and wait for output) runs the agent’s commands in a tmux session, a terminal you can drive remotely.Its job is simple: turn intent into real terminal effects, then bring back either output or a timeout message. Without this step, the agent would only be planning, never acting.
- The stage then converts raw terminal output into an observation the next model call can use.
This is not just formatting. It decides what the agent should be told next:
- if completion was already pending, the raw output can serve as the final confirmation path
- if the agent just claimed
is_task_completefor the first time, the stage does not stop immediately; it asks for confirmation instead - otherwise it gives the usual terminal output, trimmed to a safe length and possibly prefixed with warnings
- It also records what the agent just did as tool calls.
A tool call here is a structured record like “run this bash command” or “mark task complete.” This matters because the trajectory should show not just text, but the concrete actions the agent took.
- The stage appends a new step to the trajectory.
This is the canonical record of the run: what the agent said, what it tried, what came back, and basic metrics. If you want to reconstruct the run later, this is the source of truth.
- Finally,
_dump_trajectory()(write the trajectory JSON to disk) persists that record immediately.This exists for crash safety. If the process dies mid-run, you still keep the work done so far instead of losing the latest step.
State Flow
- writes:
reg-pending-completion— written when the agent first claims the task is complete; set to True so completion becomes a two-step confirmation instead of a one-shot stop - writes:
reg-pending-completion— written in the normal non-completion path as False so stale completion intent does not leak into later turns - writes:
reg-trajectory-steps— appended after command execution so this turn becomes part of the canonical run record - reads:
reg-pending-completion— snapshot before mutation to tell whether this is the first completion claim or the confirmed follow-up - clears:
reg-pending-completion— cleared in the normal path by setting it False when no completion confirmation should remain active - triggers downstream:
stage-4.10 Completion Gate + Loop Control— after observation and trajectory step are built, the next stage decides whether the loop should stop or continue
Pipeline Hand-Off
Upstream gives this stage parsed commands, the agent’s message, and any claimed completion intent. This stage turns that into terminal output, a user-facing observation, structured action records, and a persisted trajectory step; the next stage uses those results to decide whether to keep looping or exit cleanly.
Terminus2._run_agent_loopterminus_2.py:1428–1553 ↗
execute commands and append agent trajectory step
Stage context: This region is the main body of stage-4.9 after command parsing has already produced
commandsand the agent response has already been classified withis_task_complete. It runs_execute_commands(...), updates_pending_completionbased on the completion-confirmation state, then records the agent-sideStepin_trajectory_steps. In this stage, it sits between earlier response parsing and the later trajectory dump described for_dump_trajectory.
What this code does
This region executes the parsed commands against self._session, builds an observation string from terminal_output, and appends a new agent Step to self._trajectory_steps. It also conditionally updates self._pending_completion, conditionally updates self._last_response_model_name, and converts parsed commands and task-complete claims into trajectory tool_calls unless _save_raw_content_in_trajectory is enabled. The durable outputs here are state mutations and the appended trajectory record; timeout_occurred, was_pending_completion, and observation stay local in the shown code.
Interface · params / IO
(self) -> None
- params:
self:Terminus2— owner object; supplies session, settings, helpers, and mutable run state;commands:?— enclosing-scope parsed command list passed to_execute_commandsand optionally translated intoToolCalls;is_task_complete:?— enclosing-scope completion flag that selects the completion-confirmation branch and optionalmark_task_completetool call;feedback:?— enclosing-scope prior warning text; if it contains"WARNINGS:", the normal observation gets prefixed;chat:?— enclosing-scope chat accumulator used to compute token and cost deltas;tokens_before_cache:?— pre-call cache-token snapshot forcached_tokensdelta;cost_before:?— pre-call cost snapshot forcost_usddelta;tokens_before_input:?— pre-call prompt-token snapshot forprompt_tokensdelta;tokens_before_output:?— pre-call completion-token snapshot forcompletion_tokensdelta;episode:?— enclosing-scope episode number used to buildtool_call_idvalues;llm_response:?— enclosing-scope LLM response object; supplies model name, reasoning content, token ids, and logprobs;message_content:?— enclosing-scope agent message stored in the appendedStep.message - reads:
self._session,self._pending_completion,self._save_raw_content_in_trajectory,self._trajectory_steps,self._model_name,self._last_response_model_name - returns: None; the real product is a possible update to
self._pending_completion, an optional update toself._last_response_model_name, and one appended agentStepinself._trajectory_steps. - effects: awaits
self._execute_commands(commands, self._session)to interact with the live session; may setself._pending_completion = Trueon the firstis_task_completebranch; may setself._pending_completion = Falseon the non-complete branch; does not modifyself._pending_completionwhenis_task_completeis true and it was already true; may setself._last_response_model_nameifllm_response.model_nameis truthy; appends oneSteptoself._trajectory_steps
Execution flow
- It calls
await self._execute_commands(commands, self._session)and captures(timeout_occurred, terminal_output). In the shown region, onlyterminal_outputis used later;timeout_occurredis not referenced again. - It snapshots
was_pending_completion = self._pending_completionbefore any mutation. In the shown lines, that snapshot is only captured; no later branch readswas_pending_completionhere. - It builds
observationfrom three code paths keyed byis_task_complete,self._pending_completion, andfeedback: confirmed-complete uses rawterminal_output; first-time completion setsself._pending_completion = Trueand uses_get_completion_confirmation_message(terminal_output); the normal path setsself._pending_completion = Falseand uses_limit_output_length(terminal_output), optionally prefixed withfeedbackwhenfeedbackcontains"WARNINGS:". - It computes per-step usage deltas from the enclosing snapshots:
cache_tokens_used = chat.total_cache_tokens - tokens_before_cacheandstep_cost = chat.total_cost - cost_before. - It initializes
tool_callsandobservation_results, then branches onself._save_raw_content_in_trajectory. In non-raw mode, it creates oneToolCall(function_name="bash_command", ...)per item incommands, optionally appendsToolCall(function_name="mark_task_complete", arguments={}), and appendsObservationResult(content=observation)in exactly three cases: after any non-emptycommands; afteris_task_completewhencommandsis empty; or after neither commands nor task completion. In raw-content mode, it skipstool_callsgeneration but still appends oneObservationResult(content=observation). - It conditionally stores
llm_response.model_nameintoself._last_response_model_name, then appends aStepbuilt frommessage_content,llm_responsefields, the computedtool_calls,Observation(results=observation_results), andMetrics(...)token/cost deltas.
Source
timeout_occurred, terminal_output = await self._execute_commands(
commands,
self._session,
)
# Capture the pending completion state before potentially modifying it
was_pending_completion = self._pending_completion
# Construct the observation (what gets sent back to the LLM)
if is_task_complete:
if self._pending_completion:
observation = terminal_output
else:
self._pending_completion = True
observation = self._get_completion_confirmation_message(
terminal_output
)
else:
self._pending_completion = False
if feedback and "WARNINGS:" in feedback:
observation = (
f"Previous response had warnings:\n{feedback}\n\n"
f"{self._limit_output_length(terminal_output)}"
)
else:
observation = self._limit_output_length(terminal_output)
# Record the step in trajectory
cache_tokens_used = chat.total_cache_tokens - tokens_before_cache
step_cost = chat.total_cost - cost_before
# Create tool_calls array from commands and task completion
# Note: Although Terminus 2 doesn't offer native tool calling (it uses text-based command parsing),
# we represent parsed commands as tool calls for better trajectory analysis and compatibility with tooling.
# However, when raw_content mode is enabled, we skip tool_calls generation to preserve the raw LLM response.
tool_calls: list[ToolCall] | None = None
observation_results: list[ObservationResult] = []
if not self._save_raw_content_in_trajectory:
# Only create tool_calls when NOT in raw_content mode
tool_calls_list: list[ToolCall] = []
if commands:
for i, cmd in enumerate(commands):
tool_call_id = f"call_{episode}_{i + 1}"
tool_calls_list.append(
ToolCall(
tool_call_id=tool_call_id,
function_name="bash_command",
arguments={
"keystrokes": cmd.keystrokes,
"duration": cmd.duration_sec,
},
)
)
# Add observation result after all tool calls are created
# Note: All commands share the same terminal output in this architecture,
# so we omit source_call_id to indicate the observation applies to the entire step.
observation_results.append(
ObservationResult(
content=observation,
)
)
# Add task_complete as a tool call if the agent marked the task complete
if is_task_complete:
task_complete_call_id = f"call_{episode}_task_complete"
tool_calls_list.append(
ToolCall(
tool_call_id=task_complete_call_id,
function_name="mark_task_complete",
arguments={},
)
)
# If there are no commands, we still need to add an observation result
if not commands:
observation_results.append(
ObservationResult(
content=observation,
)
)
elif not commands:
# No commands and no task completion, just the observation
observation_results.append(
ObservationResult(
content=observation,
)
)
tool_calls = tool_calls_list or None
else:
# In raw_content mode, just add observation without tool_calls
observation_results.append(
ObservationResult(
content=observation,
)
)
# Build the step object using Pydantic models
if llm_response.model_name:
self._last_response_model_name = llm_response.model_name
self._trajectory_steps.append(
Step(
step_id=len(self._trajectory_steps) + 1,
timestamp=datetime.now(timezone.utc).isoformat(),
source="agent",
model_name=llm_response.model_name or self._model_name,
message=message_content,
reasoning_content=llm_response.reasoning_content,
tool_calls=tool_calls,
observation=Observation(results=observation_results),
metrics=Metrics(
prompt_tokens=chat.total_input_tokens - tokens_before_input,
completion_tokens=chat.total_output_tokens
- tokens_before_output,
cached_tokens=cache_tokens_used
if cache_tokens_used > 0
else None,
cost_usd=step_cost if step_cost > 0 else None,
prompt_token_ids=llm_response.prompt_token_ids,
completion_token_ids=llm_response.completion_token_ids,
logprobs=llm_response.logprobs,
),
)
)
Non-obvious design decisions
- The completion path uses a two-step gate around
_pending_completion: the firstis_task_completeclaim flips_pending_completiontoTrueand replaces the plain output with_get_completion_confirmation_message(...), while a lateris_task_completewhen_pending_completionis already true leaves the flag unchanged and keeps rawterminal_output. This preserves a confirmation round instead of treating the first completion claim as final. - The code records parsed text commands as synthetic
ToolCallentries even though execution already happened through_execute_commands(...). The comment ties this to trajectory analysis and tooling compatibility; the alternative would be to keep only free-form message text and lose structured command metadata. - Raw-content mode disables only
tool_callscreation. It still recordsObservationResult, wraps it inObservation, computesMetrics, and appends a normalStep, which preserves trajectory continuity while avoiding a derived command structure. - The metrics serializer stores
cached_tokensandcost_usdonly when their deltas are positive; otherwise it writesNoneinstead of0. That keeps the trajectory from implying a meaningful cached-token or cost measurement when the computed delta is not above zero.
Relations
- Callers: Terminus2._run_agent_loop
- Core callees: Terminus2._execute_commands; Terminus2._get_completion_confirmation_message; Terminus2._limit_output_length; ToolCall; ObservationResult; Observation; Metrics; Step; datetime.now
- Config / state sources: self._session; self._pending_completion; self._save_raw_content_in_trajectory; self._model_name; self._trajectory_steps
- Results to: self._pending_completion; self._last_response_model_name; self._trajectory_steps
reg-pending-completion— snapshot before mutation- branch on current state
reg-pending-completion— set True on first completion claimreg-pending-completion— set False on non-complete branchreg-trajectory-steps— append one agent Step record
Terminus2._execute_commandsterminus_2.py:1227–1257 ↗
batch tmux command executor with timeout fallback
Stage context: This helper handles the command-execution part of stage 4.9 before the stage builds an observation and appends a trajectory step.
_run_agent_loopcalls it after it has parsed the LLM response intoCommandobjects. Its result feeds the sibling logic in_run_agent_loopthat decides whether to show raw terminal output, a completion-confirmation prompt, or a warning-prefixed observation.
What this code does
_execute_commands runs each Command in commands against the provided session and returns a pair (timeout_occurred, terminal_output). It reads self._timeout_template to format timeout feedback and uses self._limit_output_length(...) to bound any terminal text it returns. The function does not mutate Terminus state; its only external effect is sending keystrokes to the TmuxSession and reading incremental terminal output from that session.
Interface · params / IO
async def _execute_commands( self, commands: list[Command], session: TmuxSession, ) -> tuple[bool, str]
- params:
commands:list[Command]— parsed terminal commands to execute in order;session:TmuxSession— active terminal session used to send keys and read output - reads:
self._timeout_template,self._limit_output_length - returns: A
tuple[bool, str]:Trueplus a formatted timeout message if anysession.send_keys(...)raisesTimeoutError, otherwiseFalseplus the length-limited incremental terminal output after the batch. - effects: awaits
session.send_keys(...)for each command; awaitssession.get_incremental_output()to read terminal output
Execution flow
- Iterate through
commandsin order and callawait session.send_keys(command.keystrokes, block=False, min_timeout_sec=command.duration_sec)for each one. - If
session.send_keys(...)raisesTimeoutError, stop the batch immediately and returnTruewithself._timeout_template.format(...), filling incommand.duration_sec,command.keystrokes, and a bounded snapshot fromself._limit_output_length(await session.get_incremental_output()). - If every command finishes without a timeout, fetch the latest incremental terminal text with
await session.get_incremental_output(), pass it throughself._limit_output_length(...), and return that string withFalse.
Source
async def _execute_commands(
self,
commands: list[Command],
session: TmuxSession,
) -> tuple[bool, str]:
"""Execute a batch of commands in the terminal.
Args:
commands: List of commands to execute
session: TmuxSession instance
Returns:
Tuple of (timeout_occurred, terminal_output)
"""
for command in commands:
try:
await session.send_keys(
command.keystrokes,
block=False,
min_timeout_sec=command.duration_sec,
)
except TimeoutError:
return True, self._timeout_template.format(
timeout_sec=command.duration_sec,
command=command.keystrokes,
terminal_state=self._limit_output_length(
await session.get_incremental_output()
),
)
return False, self._limit_output_length(await session.get_incremental_output())
Non-obvious design decisions
- It converts
TimeoutErrorinto ordinary text output instead of re-raising. That lets_run_agent_loophandle timeout feedback through the same observation path it uses for normal terminal output, rather than adding a separate exception path higher in the loop. - It returns as soon as one command times out. That keeps the terminal state aligned with the failing command; continuing to later commands after a timeout would blur which command produced the problem and could compound terminal state drift.
- It always bounds returned terminal text with
self._limit_output_length(...), including the timeout branch. That trades away full terminal history to keep observations manageable for the downstream agent loop and trajectory recording.
Relations
- Callers: Terminus2._run_agent_loop
- Core callees: TmuxSession.send_keys; TmuxSession.get_incremental_output; Terminus2._limit_output_length
- Config / state sources: self._timeout_template; self._limit_output_length
- Results to: Terminus2._run_agent_loop receives
timeout_occurred; Terminus2._run_agent_loop uses returned text as the basis forobservation; stage-4.9 completion-confirmation logic consumes the returned terminal output; stage-4.9 trajectory append logic records the resulting observation inreg-trajectory-stepsvia_run_agent_loop - Related siblings: Terminus2._run_agent_loop: consumes this helper's
(timeout_occurred, terminal_output)and turns it into the stage's observation and trajectory step.