Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s
518 lines
26 KiB
Diff
518 lines
26 KiB
Diff
diff --git a/agent/trajectory.py b/agent/trajectory.py
|
||
index 90696eb8a..264c536ff 100644
|
||
--- a/agent/trajectory.py
|
||
+++ b/agent/trajectory.py
|
||
@@ -7,12 +7,73 @@ the file-write logic live here.
|
||
|
||
import json
|
||
import logging
|
||
+import re
|
||
from datetime import datetime
|
||
from typing import Any, Dict, List
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
+def derive_trajectory_signals(trajectory: List[Dict[str, Any]], completed: bool) -> Dict[str, Any]:
|
||
+ """Derive conservative, evidence-backed signals from a saved trajectory.
|
||
+
|
||
+ Tool turns are normally a bundle of ``<tool_response>`` XML elements whose
|
||
+ payloads are JSON objects. Only recognized, valid payloads contribute to
|
||
+ the signal; malformed or unsupported values fail closed.
|
||
+ """
|
||
+ tool_results = 0
|
||
+ tool_errors = 0
|
||
+ response_pattern = re.compile(r"<tool_response>\s*(.*?)\s*</tool_response>", re.DOTALL)
|
||
+
|
||
+ def inspect_payload(payload: Any) -> None:
|
||
+ nonlocal tool_results, tool_errors
|
||
+ if isinstance(payload, dict):
|
||
+ tool_results += 1
|
||
+ failed = payload.get("success") is False
|
||
+ content = payload.get("content")
|
||
+ if isinstance(content, dict):
|
||
+ failed = failed or content.get("success") is False
|
||
+ elif isinstance(content, str):
|
||
+ try:
|
||
+ nested = json.loads(content)
|
||
+ except (TypeError, json.JSONDecodeError):
|
||
+ nested = None
|
||
+ if isinstance(nested, dict):
|
||
+ failed = failed or nested.get("success") is False
|
||
+ if failed:
|
||
+ tool_errors += 1
|
||
+
|
||
+ for turn in trajectory:
|
||
+ if turn.get("from") != "tool":
|
||
+ continue
|
||
+ value = turn.get("value", "")
|
||
+ if not isinstance(value, str):
|
||
+ continue
|
||
+ wrapped = response_pattern.findall(value)
|
||
+ if wrapped:
|
||
+ for raw_payload in wrapped:
|
||
+ try:
|
||
+ payload = json.loads(raw_payload)
|
||
+ except (TypeError, json.JSONDecodeError):
|
||
+ continue
|
||
+ inspect_payload(payload)
|
||
+ continue
|
||
+ try:
|
||
+ parsed = json.loads(value)
|
||
+ except (TypeError, json.JSONDecodeError):
|
||
+ continue
|
||
+ candidates = parsed if isinstance(parsed, list) else [parsed]
|
||
+ for item in candidates:
|
||
+ inspect_payload(item)
|
||
+
|
||
+ return {
|
||
+ "outcome": "completed" if completed else "failed",
|
||
+ "tool_errors": tool_errors,
|
||
+ "tool_results": tool_results,
|
||
+ "process_warning": "tool_errors_present" if tool_errors else None,
|
||
+ }
|
||
+
|
||
+
|
||
def convert_scratchpad_to_think(content: str) -> str:
|
||
"""Convert <REASONING_SCRATCHPAD> tags to <think> tags."""
|
||
if not content or "<REASONING_SCRATCHPAD>" not in content:
|
||
@@ -27,6 +88,34 @@ def has_incomplete_scratchpad(content: str) -> bool:
|
||
return "<REASONING_SCRATCHPAD>" in content and "</REASONING_SCRATCHPAD>" not in content
|
||
|
||
|
||
+def build_trajectory_entry(
|
||
+ trajectory: List[Dict[str, Any]],
|
||
+ *,
|
||
+ completed: bool,
|
||
+ model: str = None,
|
||
+ timestamp: str = None,
|
||
+ **fields: Any,
|
||
+) -> Dict[str, Any]:
|
||
+ """Build a persisted trajectory entry while preserving caller-specific fields.
|
||
+
|
||
+ Optional fields are omitted when not supplied so batch output retains its
|
||
+ existing schema; callers that need a custom filename/format still own the
|
||
+ surrounding file I/O.
|
||
+ """
|
||
+ entry: Dict[str, Any] = {
|
||
+ "conversations": trajectory,
|
||
+ "evaluation": derive_trajectory_signals(trajectory, completed),
|
||
+ }
|
||
+ if timestamp is not None:
|
||
+ entry["timestamp"] = timestamp
|
||
+ if model is not None:
|
||
+ entry["model"] = model
|
||
+ entry["completed"] = completed
|
||
+ entry.update(fields)
|
||
+ return entry
|
||
+
|
||
+
|
||
+
|
||
def save_trajectory(trajectory: List[Dict[str, Any]], model: str,
|
||
completed: bool, filename: str = None):
|
||
"""Append a trajectory entry to a JSONL file.
|
||
@@ -41,12 +130,12 @@ def save_trajectory(trajectory: List[Dict[str, Any]], model: str,
|
||
if filename is None:
|
||
filename = "trajectory_samples.jsonl" if completed else "failed_trajectories.jsonl"
|
||
|
||
- entry = {
|
||
- "conversations": trajectory,
|
||
- "timestamp": datetime.now().isoformat(),
|
||
- "model": model,
|
||
- "completed": completed,
|
||
- }
|
||
+ entry = build_trajectory_entry(
|
||
+ trajectory,
|
||
+ timestamp=datetime.now().isoformat(),
|
||
+ model=model,
|
||
+ completed=completed,
|
||
+ )
|
||
|
||
try:
|
||
with open(filename, "a", encoding="utf-8") as f:
|
||
diff --git a/batch_runner.py b/batch_runner.py
|
||
index 61014b49b..905bd8b0b 100644
|
||
--- a/batch_runner.py
|
||
+++ b/batch_runner.py
|
||
@@ -47,6 +47,7 @@ logger = logging.getLogger(__name__)
|
||
import fire
|
||
|
||
from run_agent import AIAgent
|
||
+from agent.trajectory import build_trajectory_entry
|
||
from toolset_distributions import (
|
||
list_distributions,
|
||
sample_toolsets_from_distribution,
|
||
@@ -470,17 +471,17 @@ def _process_batch_worker(args: Tuple) -> Dict[str, Any]:
|
||
}
|
||
tool_error_counts = _normalize_tool_error_counts(raw_error_counts)
|
||
|
||
- trajectory_entry = {
|
||
- "prompt_index": prompt_index,
|
||
- "conversations": result["trajectory"],
|
||
- "metadata": result["metadata"],
|
||
- "completed": result["completed"],
|
||
- "partial": result.get("partial", False), # True if stopped due to invalid tool calls
|
||
- "api_calls": result["api_calls"],
|
||
- "toolsets_used": result["toolsets_used"],
|
||
- "tool_stats": tool_stats, # Full stats: {tool: {count, success, failure}} - normalized
|
||
- "tool_error_counts": tool_error_counts # Simple: {tool: failure_count} - normalized
|
||
- }
|
||
+ trajectory_entry = build_trajectory_entry(
|
||
+ result["trajectory"],
|
||
+ completed=result["completed"],
|
||
+ prompt_index=prompt_index,
|
||
+ metadata=result["metadata"],
|
||
+ partial=result.get("partial", False), # True if stopped due to invalid tool calls
|
||
+ api_calls=result["api_calls"],
|
||
+ toolsets_used=result["toolsets_used"],
|
||
+ tool_stats=tool_stats, # Full stats: {tool: {count, success, failure}} - normalized
|
||
+ tool_error_counts=tool_error_counts, # Simple: {tool: failure_count} - normalized
|
||
+ )
|
||
|
||
# Append to batch output file
|
||
with open(batch_output_file, 'a', encoding='utf-8') as f:
|
||
diff --git a/mini_swe_runner.py b/mini_swe_runner.py
|
||
index 2853abc9a..5d32f36e5 100644
|
||
--- a/mini_swe_runner.py
|
||
+++ b/mini_swe_runner.py
|
||
@@ -35,6 +35,7 @@ from typing import List, Dict, Any, Optional
|
||
import fire
|
||
from dotenv import load_dotenv
|
||
from agent.tool_dispatch_helpers import make_tool_result_message
|
||
+from agent.trajectory import build_trajectory_entry
|
||
|
||
# Load environment variables
|
||
load_dotenv()
|
||
@@ -559,16 +560,16 @@ Complete the user's task step by step."""
|
||
# Convert to Hermes trajectory format
|
||
trajectory = self._convert_to_hermes_format(messages, task, completed)
|
||
|
||
- return {
|
||
- "conversations": trajectory,
|
||
- "completed": completed,
|
||
- "api_calls": api_call_count,
|
||
- "metadata": {
|
||
+ return build_trajectory_entry(
|
||
+ trajectory,
|
||
+ completed=completed,
|
||
+ api_calls=api_call_count,
|
||
+ metadata={
|
||
"model": self.model,
|
||
"env_type": self.env_type,
|
||
- "timestamp": datetime.now().isoformat()
|
||
- }
|
||
- }
|
||
+ "timestamp": datetime.now().isoformat(),
|
||
+ },
|
||
+ )
|
||
|
||
def run_batch(
|
||
self,
|
||
@@ -608,13 +609,13 @@ Complete the user's task step by step."""
|
||
|
||
except Exception as e:
|
||
self.logger.error(f"Error on task {i}: {e}")
|
||
- error_result = {
|
||
- "conversations": [],
|
||
- "completed": False,
|
||
- "api_calls": 0,
|
||
- "error": str(e),
|
||
- "metadata": {"timestamp": datetime.now().isoformat()}
|
||
- }
|
||
+ error_result = build_trajectory_entry(
|
||
+ [],
|
||
+ completed=False,
|
||
+ api_calls=0,
|
||
+ error=str(e),
|
||
+ metadata={"timestamp": datetime.now().isoformat()},
|
||
+ )
|
||
results.append(error_result)
|
||
f.write(json.dumps(error_result, ensure_ascii=False) + "\n")
|
||
f.flush()
|
||
diff --git a/run_agent.py b/run_agent.py
|
||
index 9a6542925..6599b3e83 100644
|
||
--- a/run_agent.py
|
||
+++ b/run_agent.py
|
||
@@ -200,6 +200,7 @@ from agent.tool_result_classification import (
|
||
FILE_MUTATING_TOOL_NAMES as _FILE_MUTATING_TOOLS,
|
||
file_mutation_result_landed,
|
||
)
|
||
+from agent.trajectory import build_trajectory_entry
|
||
from agent.trajectory import (
|
||
convert_scratchpad_to_think,
|
||
save_trajectory as _save_trajectory_to_file,
|
||
@@ -7527,13 +7528,13 @@ def main(
|
||
result['completed']
|
||
)
|
||
|
||
- entry = {
|
||
- "conversations": trajectory,
|
||
- "timestamp": datetime.now().isoformat(),
|
||
- "model": model,
|
||
- "completed": result['completed'],
|
||
- "query": user_query
|
||
- }
|
||
+ entry = build_trajectory_entry(
|
||
+ trajectory,
|
||
+ timestamp=datetime.now().isoformat(),
|
||
+ model=model,
|
||
+ completed=result['completed'],
|
||
+ query=user_query,
|
||
+ )
|
||
|
||
try:
|
||
with open(sample_filename, "w", encoding="utf-8") as f:
|
||
diff --git a/BOOK_SELF_EVOLUTION_REPORT.md b/BOOK_SELF_EVOLUTION_REPORT.md
|
||
new file mode 100644
|
||
index 000000000..ea4d3f52e
|
||
--- /dev/null
|
||
+++ b/BOOK_SELF_EVOLUTION_REPORT.md
|
||
@@ -0,0 +1,102 @@
|
||
+# Book-Driven Self-Evolution Report
|
||
+
|
||
+## Run identity
|
||
+
|
||
+- Starting commit: `85c8956ec7f2b4607509980794995e1c5e21e292` (the pinned HEAD at inspection time).
|
||
+- Model/provider: `openai/gpt-5.6-luna` via `openrouter`.
|
||
+- Book source read: `/home/ubuntu/ai-agent-book/book-en/`, all ten chapter files inspected by heading scans, targeted searches, and section reads. The book repository was not modified.
|
||
+
|
||
+## Opportunities identified and disposition
|
||
+
|
||
+1. **Evidence-backed trajectory signals (implemented).** Chapter 8, “Deriving Learning Signals from Operational Trajectories” (§8, lines 19–61), says evolution must start from evaluation, preserve immutable raw trajectories, and distinguish outcome/process evidence rather than trusting a scalar or self-summary. Hermes already persisted trajectories through `run_agent.py:2279-2292`, `agent/agent_runtime_helpers.py:76-243`, and `agent/trajectory.py:30-61`, but the JSONL record had no structured, deterministic evaluation signal. I added `agent.trajectory.derive_trajectory_signals()` and persisted its result under `evaluation`. It only inspects observable tool results (`success: false`), records completion status, and leaves the original messages unchanged.
|
||
+
|
||
+2. **Offline evolution with validation and rollback (already substantially present; no duplicate implementation).** Chapter 8, “Building a Continual-Evolution Closed Loop” (§8, lines 239–369), requires candidate changes, validation, release, and rollback. Hermes has the curator lifecycle in `agent/curator.py:1496-1760`, skill validation in `tools/skill_manager_tool.py:566-623`, and recoverable snapshots/rollback in `agent/curator_backup.py:216-638`, with behavior tests in `tests/agent/test_curator.py` and `tests/agent/test_curator_backup.py`. Adding another update system would duplicate existing infrastructure and increase mutation risk, so this was rejected.
|
||
+
|
||
+3. **Ablation and observability campaign (deferred as a campaign, not silently claimed).** Chapter 6, “Ablation Infrastructure” (§6, lines 662–706), and Chapter 1, “Context ablation” (§1, lines 141–159), call for fixed baselines, one-feature-at-a-time removal, and operational evidence. Hermes has trajectory persistence, curator reports, usage accounting, and tests, but this run did not have a fixed task corpus, deterministic model fixture, or safe experiment runner. Building a broad benchmark harness here would be speculative and larger than the smallest cohesive improvement. The report defines the campaign below.
|
||
+
|
||
+4. **Multi-agent independent cross-validation (deferred).** Chapter 10, “When Is Multi-Agent Truly Better Than a Single Agent?” (§10, lines 69–96) and “Cascading Amplification of Errors” (§10, lines 577–598), require independent information or evidence, not merely more agents. Hermes already has delegation and kanban infrastructure (`tools/delegate_tool.py`, `plugins/kanban/`, and the delegation configuration), so a generic reviewer would add cost and coordination surface without a concrete task contract. No change made.
|
||
+
|
||
+5. **Context-cache stability (preserved, not changed).** Chapter 2, “KV Cache-Friendly Context Design” (§2, lines 404–546), says stable system/tool prefixes are architectural constraints. The implementation adds metadata only to persisted JSONL trajectories and does not alter prompts, tools, or in-conversation messages.
|
||
+
|
||
+## Changes made
|
||
+
|
||
+- Added `derive_trajectory_signals()` to `agent/trajectory.py`.
|
||
+- Added an `evaluation` object to saved JSONL entries. It contains `outcome`, `tool_errors`, `tool_results`, and a conservative `process_warning`.
|
||
+- Added behavior-contract tests in `tests/agent/test_trajectory.py` covering tool-error detection and the invariant that persisted messages are unchanged.
|
||
+
|
||
+The implementation deliberately does not call an LLM judge, rewrite skills, alter prompts, infer success from prose, or change role ordering. This keeps the core tool surface and safety gates unchanged.
|
||
+
|
||
+## Deliberately rejected or deferred
|
||
+
|
||
+- No automatic skill/prompt/program/model mutation from one trajectory. Chapter 8 explicitly warns that unverified online updates amplify noise and prompt injection (§8, lines 3–11, 297–369).
|
||
+- No new core tool, environment variable, or dependency.
|
||
+- No broad evaluation dashboard or automatic ablation scheduler without a fixed corpus and acceptance criteria.
|
||
+- No claim that deterministic tool-error counts are a complete verifier; they are only low-level evidence.
|
||
+
|
||
+## Review round correction
|
||
+
|
||
+The independent review identified that the first implementation parsed only bare JSON, while the production conversion path emits bundled `<tool_response>` XML containing JSON envelopes with nested `content`. The implementation now recognizes multiple wrapped entries, parses nested object/string content conservatively, and ignores malformed or unsupported shapes. The new end-to-end test exercises `agent.agent_runtime_helpers.convert_to_trajectory_format()` before saving and verifies one failed result among two bundled responses. The original `conversations` list is asserted unchanged.
|
||
+
|
||
+## Second review round correction
|
||
+
|
||
+The fresh review found two real persistence paths that bypassed `save_trajectory()`: the JSONL entry assembled in `batch_runner.py` and the pretty-printed sample entry assembled in `run_agent.py` when `save_sample` is enabled. I inspected both paths and introduced `agent.trajectory.build_trajectory_entry()` as the shared entry builder. `agent/trajectory.py:91-128` now uses it for the existing append writer; `batch_runner.py:49-50,473-486` uses it while retaining its existing JSONL file I/O, flush/fsync behavior, filenames, and batch-specific fields; `run_agent.py:203-204,7530-7540` uses it while retaining the sample filename, pretty-printing, error handling, and query field. The builder preserves the supplied `conversations` object and adds the same backward-compatible `evaluation` field to all three persistence paths.
|
||
+
|
||
+The focused contract test verifies shared-field preservation. The existing conversion-and-save test continues to verify production-shaped XML tool results and unchanged conversation data.
|
||
+
|
||
+## Third review round correction
|
||
+
|
||
+The third review found two issues. First, a single recognized envelope could increment `tool_errors` once for top-level `success: false` and again for nested `content.success: false`. The signal derivation now computes one `failed` boolean per recognized payload, so each tool result contributes at most one error. A regression test covers both flags together.
|
||
+
|
||
+Second, a repository-wide search for direct ShareGPT/trajectory-shaped persistence found `mini_swe_runner.py` as the remaining production trajectory producer. Its `run_task()` result and `run_batch()` JSONL path now use `build_trajectory_entry()` while retaining the existing result fields, output filename handling, immediate flushes, and error record behavior. The empty error result also receives the metadata contract. The search also found unrelated records containing a `conversations` key (`mcp_serve.py`, session/audit exports, gateway state, plugins, and compression/transformation utilities); these are not ShareGPT trajectory producers and were deliberately left unchanged. The scope is therefore all identified Hermes ShareGPT trajectory producers: standard `save_trajectory()`, `run_agent.py` sample output, `batch_runner.py`, and `mini_swe_runner.py`.
|
||
+
|
||
+## Verification
|
||
+
|
||
+Exact commands and results:
|
||
+
|
||
+```text
|
||
+python3 - <<'PY' ... # book file inventory and all-chapter heading/search inspection
|
||
+# Result: all ten chapter*.md files inspected; targeted section output was collected.
|
||
+
|
||
+.venv/bin/python -m pytest tests/agent/test_trajectory.py -q
|
||
+# Result: failed before test execution: No module named pytest
|
||
+
|
||
+scripts/run_tests.sh tests/agent/test_trajectory.py -q
|
||
+# Result: failed because no configured virtualenv with pytest exists.
|
||
+
|
||
+uv run --with pytest pytest tests/agent/test_trajectory.py -q
|
||
+# Result (third review round): 6 passed in 0.12s
|
||
+
|
||
+uv run --with pytest pytest tests/agent/test_trajectory.py tests/test_batch_runner_checkpoint.py tests/test_batch_runner_durability.py tests/integration/test_batch_runner.py tests/test_trajectory_compressor.py -q
|
||
+# Result (third review round): 44 passed in 0.62s
|
||
+
|
||
+python3 -m py_compile agent/trajectory.py agent/agent_runtime_helpers.py batch_runner.py run_agent.py mini_swe_runner.py tests/agent/test_trajectory.py
|
||
+# Result (third review round): passed
|
||
+
|
||
+git diff --check
|
||
+# Result (third review round): passed
|
||
+```
|
||
+
|
||
+The repository's prescribed runner was attempted exactly and could not run because the checkout's `.venv` lacks pytest; the equivalent isolated `uv run` verification passed both new tests. No full suite was claimed.
|
||
+
|
||
+## Limitations
|
||
+
|
||
+- One implementation run and two unit tests show only that the metadata contract works. They are not evidence that trajectory signals improve task success, reduce cost, or improve safety.
|
||
+- `success: false` detection depends on tool handlers exposing that field in JSON; non-JSON output and domain-specific failures remain uncertain.
|
||
+- `completed=True` means the existing runtime completion path reported completion, not that the external task is correct. The field is intentionally named `outcome` and accompanied by process evidence rather than treated as ground truth.
|
||
+- No live model calls, benchmark tasks, or user data were used.
|
||
+
|
||
+## Proposed ablation campaign
|
||
+
|
||
+Use a fixed, versioned task corpus with isolated temporary Hermes homes, pinned model/provider/config, fixed tool availability, and deterministic seeds where supported. Record success, tool-error rate, policy/safety violations, latency, token usage, and cache-related request metadata. Run enough repetitions for confidence intervals and keep a held-out task split.
|
||
+
|
||
+Baseline: current Hermes with trajectory persistence and the new evaluation metadata enabled, but no automatic mutation. Compare one feature disabled at a time:
|
||
+
|
||
+1. Disable structured trajectory evaluation metadata; retain raw trajectory persistence.
|
||
+2. Disable raw trajectory persistence; retain ordinary task execution.
|
||
+3. Disable curator offline review while retaining metadata.
|
||
+4. Disable curator backup/rollback, only in a disposable sandbox, to measure operational risk—not production behavior.
|
||
+5. Disable context compression while retaining stable prefixes.
|
||
+6. Disable delegation for tasks that have a defined delegation path.
|
||
+7. Disable individual skill injection/progressive disclosure for matching task families.
|
||
+
|
||
+For every comparison, require that the fixed baseline and ablation use identical prompts, toolsets, model, credentials, task order, and safety/approval settings. Treat any regression in safety or policy compliance as a release blocker even if task success rises. The campaign should first validate that the new metadata predicts independently verified failures, then test whether using it in a separately reviewed offline evolution process improves held-out performance without negative transfer. A single run must never authorize self-modification.
|
||
diff --git a/tests/agent/test_trajectory.py b/tests/agent/test_trajectory.py
|
||
new file mode 100644
|
||
index 000000000..8b45ce5b7
|
||
--- /dev/null
|
||
+++ b/tests/agent/test_trajectory.py
|
||
@@ -0,0 +1,139 @@
|
||
+"""Behavior contracts for persisted trajectory learning signals."""
|
||
+
|
||
+import json
|
||
+
|
||
+from agent.agent_runtime_helpers import convert_to_trajectory_format
|
||
+from mini_swe_runner import MiniSWERunner
|
||
+from agent.trajectory import (
|
||
+ build_trajectory_entry,
|
||
+ derive_trajectory_signals,
|
||
+ save_trajectory,
|
||
+)
|
||
+
|
||
+
|
||
+def test_trajectory_signals_distinguish_completed_run_with_tool_failure():
|
||
+ trajectory = [
|
||
+ {"from": "system", "value": "system"},
|
||
+ {"from": "human", "value": "do it"},
|
||
+ {"from": "gpt", "value": "<tool_call>"},
|
||
+ {"from": "tool", "value": '{"success": false, "error": "permission denied"}'},
|
||
+ ]
|
||
+
|
||
+ signals = derive_trajectory_signals(trajectory, completed=True)
|
||
+
|
||
+ assert signals == {
|
||
+ "outcome": "completed",
|
||
+ "tool_errors": 1,
|
||
+ "tool_results": 1,
|
||
+ "process_warning": "tool_errors_present",
|
||
+ }
|
||
+
|
||
+
|
||
+def test_saved_trajectory_contains_structured_signals_without_changing_messages(tmp_path):
|
||
+ output = tmp_path / "trajectories.jsonl"
|
||
+ messages = [{"from": "human", "value": "hello"}]
|
||
+
|
||
+ save_trajectory(messages, "test-model", completed=False, filename=str(output))
|
||
+
|
||
+ entry = json.loads(output.read_text().splitlines()[0])
|
||
+ assert entry["conversations"] == messages
|
||
+ assert entry["evaluation"] == {
|
||
+ "outcome": "failed",
|
||
+ "tool_errors": 0,
|
||
+ "tool_results": 0,
|
||
+ "process_warning": None,
|
||
+ }
|
||
+
|
||
+
|
||
+def test_production_conversion_persists_bundled_nested_tool_failure(tmp_path):
|
||
+ class _Agent:
|
||
+ def _format_tools_for_system_message(self):
|
||
+ return ""
|
||
+
|
||
+ internal_messages = [
|
||
+ {"role": "user", "content": "run both"},
|
||
+ {
|
||
+ "role": "assistant",
|
||
+ "content": "",
|
||
+ "tool_calls": [
|
||
+ {"function": {"name": "first", "arguments": "{}"}},
|
||
+ {"function": {"name": "second", "arguments": "{}"}},
|
||
+ ],
|
||
+ },
|
||
+ {
|
||
+ "role": "tool",
|
||
+ "tool_call_id": "one",
|
||
+ "content": '{"success": false, "error": "denied"}',
|
||
+ },
|
||
+ {
|
||
+ "role": "tool",
|
||
+ "tool_call_id": "two",
|
||
+ "content": '{"success": true}',
|
||
+ },
|
||
+ ]
|
||
+ trajectory = convert_to_trajectory_format(_Agent(), internal_messages, "run both", completed=True)
|
||
+ output = tmp_path / "converted.jsonl"
|
||
+
|
||
+ save_trajectory(trajectory, "test-model", completed=True, filename=str(output))
|
||
+
|
||
+ entry = json.loads(output.read_text().splitlines()[0])
|
||
+ assert entry["conversations"] == trajectory
|
||
+ assert entry["evaluation"]["tool_results"] == 2
|
||
+ assert entry["evaluation"]["tool_errors"] == 1
|
||
+ assert entry["evaluation"]["process_warning"] == "tool_errors_present"
|
||
+
|
||
+
|
||
+def test_shared_entry_builder_preserves_batch_and_sample_fields():
|
||
+ trajectory = [{"from": "human", "value": "hello"}]
|
||
+ entry = build_trajectory_entry(
|
||
+ trajectory,
|
||
+ completed=True,
|
||
+ prompt_index=3,
|
||
+ metadata={"source": "batch"},
|
||
+ query="hello",
|
||
+ )
|
||
+
|
||
+ assert entry["conversations"] is trajectory
|
||
+ assert entry["prompt_index"] == 3
|
||
+ assert entry["metadata"] == {"source": "batch"}
|
||
+ assert entry["query"] == "hello"
|
||
+ assert entry["completed"] is True
|
||
+ assert entry["evaluation"]["tool_results"] == 0
|
||
+
|
||
+
|
||
+def test_each_wrapped_tool_result_contributes_at_most_one_error():
|
||
+ trajectory = [{
|
||
+ "from": "tool",
|
||
+ "value": (
|
||
+ '<tool_response>\n'
|
||
+ '{"content": {"success": false}, "success": false}'
|
||
+ '\n</tool_response>'
|
||
+ ),
|
||
+ }]
|
||
+
|
||
+ signals = derive_trajectory_signals(trajectory, completed=False)
|
||
+
|
||
+ assert signals["tool_results"] == 1
|
||
+ assert signals["tool_errors"] == 1
|
||
+
|
||
+
|
||
+def test_mini_swe_result_uses_shared_evaluation_entry():
|
||
+ runner = MiniSWERunner.__new__(MiniSWERunner)
|
||
+ runner.model = "test-model"
|
||
+ runner.env_type = "local"
|
||
+ trajectory = [{
|
||
+ "from": "tool",
|
||
+ "value": '<tool_response>\n{"content": {"success": false}}\n</tool_response>',
|
||
+ }]
|
||
+
|
||
+ # Exercise the same result contract used by run_task without starting an environment.
|
||
+ result = build_trajectory_entry(
|
||
+ trajectory,
|
||
+ completed=False,
|
||
+ api_calls=1,
|
||
+ metadata={"model": runner.model, "env_type": runner.env_type},
|
||
+ )
|
||
+
|
||
+ assert result["conversations"] is trajectory
|
||
+ assert result["evaluation"]["tool_errors"] == 1
|
||
+ assert result["api_calls"] == 1
|