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
5 changes: 3 additions & 2 deletions agent/decider.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import re

from agent.context import context_for_agent
from agent.philosophy import serialize_philosophy_for_prompt
from agent.planner import _release_cadence_signal, _release_timing_state

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -290,7 +291,7 @@ def _run_lens(name: str, context: dict, philosophy: dict, request: str, llm) ->
system = _LENS_SYSTEMS[name]
if name == "direction":
user = (
f"Repository philosophy:\n{json.dumps(philosophy, indent=1)[:3000]}\n\n"
f"Repository philosophy:\n{serialize_philosophy_for_prompt(philosophy, 3000)}\n\n"
f"Decision request: {request}\n\n"
'Return JSON: {"verdict": "one short sentence", "reasoning": "why"}'
)
Expand All @@ -314,7 +315,7 @@ def decide(context: dict, philosophy: dict, request: str, llm) -> dict:
for name, verdict in lenses.items()
)
user = (
f"Repository philosophy:\n{json.dumps(philosophy, indent=1)[:3000]}\n\n"
f"Repository philosophy:\n{serialize_philosophy_for_prompt(philosophy, 3000)}\n\n"
f"Repository state:\n{_render(context)}\n"
f"{_release_context_note(context)}"
f"{_planning_version_bump_note(context, request)}"
Expand Down
25 changes: 25 additions & 0 deletions agent/philosophy.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,28 @@ def _render(context: dict) -> str:
"labels", "milestones", "releases", "readme_excerpt",
)}
return json.dumps(keep, indent=1)[:12000]


def serialize_philosophy_for_prompt(philosophy: dict, budget: int) -> str:
"""Serialize ``philosophy`` for agent prompts without breaking JSON syntax.

When the full ``json.dumps(..., indent=1)`` fits ``budget``, the result is
byte-identical. Otherwise whole trailing ``evidence`` entries are dropped so
``summary``/``values``/``merge_bar``/``direction`` survive as valid JSON. A raw
string slice is used only when the object still cannot fit with ``evidence``
empty — the same last-resort behavior as before this helper existed.
"""
full = json.dumps(philosophy, indent=1)
if len(full) <= budget:
return full

if isinstance(philosophy, dict) and isinstance(philosophy.get("evidence"), list):
evidence = list(philosophy["evidence"])
while evidence:
evidence.pop()
trimmed = {**philosophy, "evidence": evidence}
candidate = json.dumps(trimmed, indent=1)
if len(candidate) <= budget:
return candidate

return full[:budget]
3 changes: 2 additions & 1 deletion agent/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from datetime import datetime, timezone

from agent.context import context_for_agent
from agent.philosophy import serialize_philosophy_for_prompt

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -1098,7 +1099,7 @@ def plan_next_actions(context: dict, philosophy: dict, n: int, llm) -> list:
if not isinstance(context, dict):
return _offline_plan_stub({}, n)
user = (
f"Repository philosophy:\n{json.dumps(philosophy, indent=1)[:4000]}\n\n"
f"Repository philosophy:\n{serialize_philosophy_for_prompt(philosophy, 4000)}\n\n"
f"Repository state:\n{_render(context)}\n"
f"{_repo_layout_note(context)}"
f"{_recent_kinds_note(context)}"
Expand Down
62 changes: 62 additions & 0 deletions tests/test_philosophy.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
_normalize_string_list,
_normalize_text,
infer_philosophy,
serialize_philosophy_for_prompt,
)

EXPECTED_KEYS = {"summary", "values", "merge_bar", "direction", "evidence"}
Expand Down Expand Up @@ -137,3 +138,64 @@ def test_infer_philosophy_non_dict_context_returns_fresh_copy():
b = infer_philosophy(None, llm)
a["summary"] = "mutated"
assert b["summary"] != "mutated"


def _oversized_philosophy(num_evidence: int = 13) -> dict:
"""Shape sized like the #1706 instrumented run (4,237 chars full serialization)."""
return {
"summary": "A mature library prioritizing stability and a small dependency surface.",
"values": ["conservative", "stability-over-features", "docs-first"],
"merge_bar": (
"Merges well-justified fixes and incremental hardening; rejects churn, new "
"dependencies without payoff, and breaking changes outside a deprecation window."
),
"direction": "Incremental hardening on the current major line with careful API evolution.",
"evidence": [f"signal {i}: " + ("x" * 300) for i in range(num_evidence)],
}


def test_serialize_philosophy_for_prompt_is_byte_identical_when_it_fits():
philosophy = {"summary": "ship fast", "values": ["feature-first"], "merge_bar": "low",
"direction": "expand", "evidence": ["recent feature flags"]}
rendered = serialize_philosophy_for_prompt(philosophy, 4000)
assert rendered == json.dumps(philosophy, indent=1)


def test_serialize_philosophy_for_prompt_drops_trailing_evidence_to_stay_valid_json():
philosophy = _oversized_philosophy()
full = json.dumps(philosophy, indent=1)
assert len(full) > 4000

for budget in (4000, 3000):
rendered = serialize_philosophy_for_prompt(philosophy, budget)
assert len(rendered) <= budget
decoded = json.loads(rendered)
assert decoded["summary"] == philosophy["summary"]
assert decoded["values"] == philosophy["values"]
assert decoded["merge_bar"] == philosophy["merge_bar"]
assert decoded["direction"] == philosophy["direction"]
assert isinstance(decoded["evidence"], list)
assert decoded["evidence"] == philosophy["evidence"][: len(decoded["evidence"])]
if len(full) > budget:
assert len(decoded["evidence"]) < len(philosophy["evidence"])


def test_serialize_philosophy_for_prompt_hard_slices_when_core_fields_exceed_budget():
philosophy = {
"summary": "p" * 5000,
"values": [],
"merge_bar": "high",
"direction": "forward",
"evidence": [],
}
full = json.dumps(philosophy, indent=1)
rendered = serialize_philosophy_for_prompt(philosophy, 3000)
assert rendered == full[:3000]
assert len(rendered) == 3000


def test_serialize_philosophy_for_prompt_does_not_mutate_input():
philosophy = _oversized_philosophy()
original_evidence = list(philosophy["evidence"])
serialize_philosophy_for_prompt(philosophy, 3000)
assert philosophy["evidence"] == original_evidence
Loading