Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion agent/agent_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -1468,7 +1468,20 @@ def init_agent(

# Show trajectory saving status
if agent.save_trajectories and not agent.quiet_mode:
print("📝 Trajectory saving enabled")
# Name the destination. This is the path datagen actually uses
# (AIAgent(save_trajectories=True)), and the file may be redirected out
# of a git checkout — "enabled" with no path left the user hunting for
# their training data. resolve_trajectory_path also reports a
# pre-existing in-checkout dataset here, before the run starts.
try:
from agent.trajectory import describe_trajectory_destination
_traj_dest = describe_trajectory_destination()
except Exception: # pragma: no cover - a status line must not break init
_traj_dest = None
if _traj_dest:
print(f"📝 Trajectory saving enabled → {_traj_dest}")
else:
print("📝 Trajectory saving enabled")

# Show ephemeral system prompt status
if agent.ephemeral_system_prompt and not agent.quiet_mode:
Expand Down
419 changes: 417 additions & 2 deletions agent/trajectory.py

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions cli-config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -821,6 +821,28 @@ agent:
# Recommended: 20-30 for focused tasks, 50-100 for open exploration
max_turns: 500

# Allow trajectory JSONL to be written into a git work tree (default: false).
#
# Trajectory saving (AIAgent(save_trajectories=True) /
# `run_agent.py --save_trajectories`) appends a full verbatim transcript —
# message text, tool results and tool-call arguments — to a CWD-relative
# file. Launched from a source checkout, that leaves an untracked
# trajectory_samples.jsonl next to your code, one `git add -A` away from
# being committed. By default such a write is redirected under
# <HERMES_HOME>/trajectories/<work-tree>/ (nothing is dropped, only
# relocated) and a one-time notice names the destination on the terminal.
# The <work-tree> component is per-repository, so two checkouts keep two
# datasets instead of merging into one file.
#
# Upgrading with a pipeline that already reads ./trajectory_samples.jsonl:
# that file is left exactly as it is and stops receiving new entries. Hermes
# says so on the terminal and in errors.log — repoint the pipeline at the
# new path, concatenate the two, or set this to true.
#
# Set true to restore writing to the working directory. Passing an absolute
# filename to save_trajectory() also bypasses the redirect.
# trajectory_allow_git_cwd: false

# Inactivity timeout for gateway agent runs (seconds, 0 = unlimited).
# The agent can run indefinitely when actively calling tools or receiving
# API responses. Only fires after the agent has been idle for this duration.
Expand Down
13 changes: 13 additions & 0 deletions hermes_cli/config_defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@
"max_live_sessions": 16,
"agent": {
"max_turns": 500,
# Allow trajectory JSONL to be written into a git work tree.
# save_trajectory() (AIAgent(save_trajectories=True) /
# `run_agent.py --save_trajectories`) appends a full verbatim
# transcript — message text, tool results, tool-call arguments — under
# a CWD-relative filename, so an agent run launched from a source
# checkout dropped one next to the user's code, one `git add -A` from
# being published (#77472). Default False redirects such a write under
# ``<HERMES_HOME>/trajectories/<work-tree>/`` and warns with the
# destination; nothing is dropped or truncated, only relocated, and the
# per-work-tree directory keeps one dataset per repo the way the
# CWD-relative path did. Set True to restore writing to the working
# directory (passing an absolute filename also bypasses the redirect).
"trajectory_allow_git_cwd": False,
# Inactivity timeout for gateway agent execution (seconds).
# The agent can run indefinitely as long as it's actively calling
# tools or receiving API responses. Only fires when the agent has
Expand Down
60 changes: 36 additions & 24 deletions run_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ def _session_source_for_agent(platform: Optional[str]) -> str:
)
from agent.trajectory import (
convert_scratchpad_to_think,
resolve_trajectory_path,
save_trajectory as _save_trajectory_to_file,
)
from agent.tool_dispatch_helpers import (
Expand Down Expand Up @@ -7900,6 +7901,9 @@ def main(
print("💾 Trajectory saving: ENABLED")
print(" - Successful conversations → trajectory_samples.jsonl")
print(" - Failed conversations → failed_trajectories.jsonl")
print(" - Inside a git checkout these are written under "
"<HERMES_HOME>/trajectories/<work-tree>/ instead "
"(agent.trajectory_allow_git_cwd: true to override)")

# Initialize agent with provided parameters
try:
Expand Down Expand Up @@ -7948,30 +7952,38 @@ def main(
# Save sample trajectory to UUID-named file if requested
if save_sample:
sample_id = str(uuid.uuid4())[:8]
sample_filename = f"sample_{sample_id}.json"

# Convert messages to trajectory format (same as batch_runner)
trajectory = agent._convert_to_trajectory_format(
result['messages'],
user_query,
result['completed']
)

entry = {
"conversations": trajectory,
"timestamp": datetime.now().isoformat(),
"model": model,
"completed": result['completed'],
"query": user_query
}

try:
with open(sample_filename, "w", encoding="utf-8") as f:
# Pretty-print JSON with indent for readability
f.write(json.dumps(entry, ensure_ascii=False, indent=2))
print(f"\n💾 Sample trajectory saved to: {sample_filename}")
except Exception as e:
print(f"\n⚠️ Failed to save sample: {e}")
# Same class of write as save_trajectory: a full verbatim trajectory
# under a relative name, so it lands in the CWD — a source checkout as
# often as not. Route it through the same guard.
sample_filename = resolve_trajectory_path(f"sample_{sample_id}.json")

# None means the guard could not place the file safely and refused to
# drop it in the checkout; it has already reported the reason.
if sample_filename is None:
print("\n⚠️ Sample trajectory not saved: could not place it outside a git checkout")
else:
# Convert messages to trajectory format (same as batch_runner)
trajectory = agent._convert_to_trajectory_format(
result['messages'],
user_query,
result['completed']
)

entry = {
"conversations": trajectory,
"timestamp": datetime.now().isoformat(),
"model": model,
"completed": result['completed'],
"query": user_query
}

try:
with open(sample_filename, "w", encoding="utf-8") as f:
# Pretty-print JSON with indent for readability
f.write(json.dumps(entry, ensure_ascii=False, indent=2))
print(f"\n💾 Sample trajectory saved to: {sample_filename}")
except Exception as e:
print(f"\n⚠️ Failed to save sample: {e}")

print("\n👋 Agent execution completed!")

Expand Down
Loading