Skip to content

Commit ca51743

Browse files
committed
Sync version and context output previews
1 parent babb244 commit ca51743

20 files changed

Lines changed: 433 additions & 141 deletions

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "setup-agent"
3-
version = "0.1.2"
3+
version = "0.3.0"
44
description = "LLM Powered open source project setup agent"
55
authors = [
66
{name = "Setup-Agent Contributors"}

src/sag/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
"""Setup-Agent runtime package."""
2+
3+
__version__ = "0.3.0"

src/sag/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from rich.table import Table
1414
from rich.text import Text
1515

16+
from sag import __version__
1617
from sag.agent.agent import SetupAgent
1718
from sag.config import (
1819
Config,
@@ -627,7 +628,9 @@ def ui(host, port, demo):
627628
@cli.command()
628629
def version():
629630
"""Show SAG version information."""
630-
console.print("[bold blue]SAG[/bold blue] (Setup-Agent) version [green]0.2.0[/green]")
631+
console.print(
632+
f"[bold blue]SAG[/bold blue] (Setup-Agent) version [green]{__version__}[/green]"
633+
)
631634
console.print("[dim]LLM-powered project setup automation[/dim]")
632635

633636

src/sag/tools/report_tool.py

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from loguru import logger
88

9+
from sag import __version__
910
from sag.agent.context_manager import TaskStatus
1011
from sag.reporting import format_percentage, render_condensed_summary, truncate_list
1112
from sag.runtime.env_overlay import EnvOverlayStore
@@ -3979,28 +3980,8 @@ def _save_markdown_report_fallback(self, markdown_content: str, filepath: str):
39793980
# ==================== NEW IMPROVED REPORT RENDERING METHODS ====================
39803981

39813982
def _get_setup_agent_version(self) -> str:
3982-
"""Get the Setup-Agent version from pyproject.toml."""
3983-
try:
3984-
import re
3985-
from pathlib import Path
3986-
3987-
# Look for pyproject.toml in the parent directories
3988-
current = Path(__file__).resolve()
3989-
for _ in range(5): # Look up to 5 levels
3990-
current = current.parent
3991-
pyproject_path = current / "pyproject.toml"
3992-
if pyproject_path.exists():
3993-
# Simple parsing without requiring tomllib
3994-
with open(pyproject_path, "r", encoding="utf-8") as f:
3995-
content = f.read()
3996-
# Look for version = "x.x.x" pattern
3997-
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
3998-
if match:
3999-
return match.group(1)
4000-
return "0.1.2" # Default version if not found
4001-
except Exception as e:
4002-
logger.debug(f"Could not read version from pyproject.toml: {e}")
4003-
return "0.1.2" # Default version on error
3983+
"""Get the Setup-Agent version used in generated reports."""
3984+
return __version__
40043985

40053986
def _render_enhanced_header(
40063987
self, timestamp: str, status: str, project_info: dict, snapshot: dict = None

src/sag/web/context_map.py

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,19 @@
77
from pathlib import Path
88
from typing import Any
99

10-
from sag.web.models import ActiveBranchSummary, ContextMap, ContextTask, TrunkSummary
10+
from sag.web.models import (
11+
ActiveBranchSummary,
12+
ContextMap,
13+
ContextReference,
14+
ContextTask,
15+
TrunkSummary,
16+
)
1117

1218

1319
class ContextMapBuilder:
1420
def __init__(self, contexts_dir: Path):
1521
self.contexts_dir = contexts_dir
22+
self._output_records: dict[str, dict[str, Any]] | None = None
1623

1724
def build(self) -> ContextMap | None:
1825
trunk_path = self._find_trunk()
@@ -88,10 +95,12 @@ def _task(self, item: dict[str, Any], index: int) -> ContextTask:
8895
),
8996
status=str(item.get("status") or "pending"),
9097
summary=str(item.get("summary") or self._branch_summary(branch_data) or ""),
91-
refs=[
92-
*[str(ref) for ref in item.get("refs", [])],
93-
*self._branch_refs(branch_data),
94-
],
98+
refs=self._dedupe_refs(
99+
[
100+
*[self._context_ref(ref) for ref in item.get("refs", [])],
101+
*self._branch_refs(branch_data),
102+
]
103+
),
95104
recovered=bool(item.get("recovered", False)),
96105
)
97106

@@ -112,9 +121,10 @@ def _branch_summary(self, data: dict[str, Any]) -> str:
112121
if action is not None:
113122
tool_name = str(action.get("tool_name") or "action")
114123
outcome = "succeeded" if action.get("success") is True else "failed"
115-
output = self._compact_text(str(action.get("output") or ""))
124+
output = self._full_output_for_history_item(action) or str(action.get("output") or "")
125+
output = self._compact_text(output)
116126
parts.append(
117-
f"{tool_name} {outcome}: {output}" if output else f"{tool_name} {outcome}."
127+
f"{tool_name} {outcome}:\n{output}" if output else f"{tool_name} {outcome}."
118128
)
119129

120130
if parts:
@@ -156,25 +166,105 @@ def _latest_history_entry(
156166
None,
157167
)
158168

159-
def _branch_refs(self, data: dict[str, Any]) -> list[str]:
169+
def _branch_refs(self, data: dict[str, Any]) -> list[ContextReference]:
160170
history = data.get("history")
161171
if not isinstance(history, list):
162172
return []
163173

164-
refs: list[str] = []
174+
refs: list[ContextReference] = []
165175
for item in history:
166176
if not isinstance(item, dict):
167177
continue
168-
for match in re.findall(
169-
r"Full output ref:\s*([A-Za-z0-9_-]+)", str(item.get("output") or "")
170-
):
171-
if match not in refs:
172-
refs.append(match)
178+
refs.extend(
179+
self._context_ref(match)
180+
for match in self._output_refs_from_text(str(item.get("output") or ""))
181+
)
173182
return refs
174183

175184
def _compact_text(self, value: str) -> str:
176-
text = " ".join(value.split())
177-
return text
185+
return "\n".join(" ".join(line.split()) for line in value.splitlines()).strip()
186+
187+
def _output_refs_from_text(self, value: str) -> list[str]:
188+
return re.findall(r"Full output ref:\s*([A-Za-z0-9_-]+)", value)
189+
190+
def _full_output_for_history_item(self, item: dict[str, Any]) -> str | None:
191+
for ref in self._output_refs_from_text(str(item.get("output") or "")):
192+
record = self._output_record(ref)
193+
content = record.get("output")
194+
if isinstance(content, str) and content:
195+
return content
196+
return None
197+
198+
def _context_ref(self, value: Any) -> ContextReference:
199+
if isinstance(value, dict):
200+
ref = str(value.get("ref") or value.get("id") or value.get("path") or "")
201+
label = str(value.get("label") or ref)
202+
return ContextReference(
203+
ref=ref,
204+
label=label,
205+
kind=str(value.get("kind") or "reference"),
206+
tool=str(value.get("tool")) if value.get("tool") is not None else None,
207+
task_id=str(value.get("task_id") or value.get("taskId"))
208+
if value.get("task_id") or value.get("taskId")
209+
else None,
210+
timestamp=str(value.get("timestamp")) if value.get("timestamp") is not None else None,
211+
content=str(value.get("content")) if value.get("content") is not None else None,
212+
content_length=self._int_or_none(value.get("content_length") or value.get("contentLength")),
213+
)
214+
215+
ref = str(value)
216+
record = self._output_record(ref)
217+
content = record.get("output") if record else None
218+
return ContextReference(
219+
ref=ref,
220+
label=ref,
221+
kind="output" if ref.startswith("output_") else "reference",
222+
tool=str(record.get("tool_name")) if record.get("tool_name") is not None else None,
223+
task_id=str(record.get("task_id")) if record.get("task_id") is not None else None,
224+
timestamp=str(record.get("timestamp")) if record.get("timestamp") is not None else None,
225+
content=content if isinstance(content, str) else None,
226+
content_length=self._int_or_none(record.get("output_length"))
227+
or (len(content) if isinstance(content, str) else None),
228+
)
229+
230+
def _dedupe_refs(self, refs: list[ContextReference]) -> list[ContextReference]:
231+
deduped: list[ContextReference] = []
232+
seen: set[str] = set()
233+
for ref in refs:
234+
key = ref.ref
235+
if not key or key in seen:
236+
continue
237+
seen.add(key)
238+
deduped.append(ref)
239+
return deduped
240+
241+
def _output_record(self, ref: str) -> dict[str, Any]:
242+
return self._output_records_by_ref().get(ref, {})
243+
244+
def _output_records_by_ref(self) -> dict[str, dict[str, Any]]:
245+
if self._output_records is not None:
246+
return self._output_records
247+
248+
records: dict[str, dict[str, Any]] = {}
249+
path = self.contexts_dir / "full_outputs.jsonl"
250+
try:
251+
for line in path.read_text(encoding="utf-8").splitlines():
252+
if not line.strip():
253+
continue
254+
record = json.loads(line)
255+
if isinstance(record, dict) and record.get("ref_id"):
256+
records[str(record["ref_id"])] = record
257+
except (OSError, json.JSONDecodeError):
258+
pass
259+
260+
self._output_records = records
261+
return records
262+
263+
def _int_or_none(self, value: Any) -> int | None:
264+
try:
265+
return int(value)
266+
except (TypeError, ValueError):
267+
return None
178268

179269
def _is_active_status(self, status: str) -> bool:
180270
return status.strip().lower() in {"active", "running", "in_progress"}

src/sag/web/models.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from typing import Any, ClassVar, Literal
66

7-
from pydantic import BaseModel, ConfigDict, Field
7+
from pydantic import BaseModel, ConfigDict, Field, field_validator
88

99

1010
class WebModel(BaseModel):
@@ -82,14 +82,37 @@ class FileChangeDigest(WebModel):
8282
items: list[FileChangeItem] = Field(default_factory=list)
8383

8484

85+
class ContextReference(WebModel):
86+
ref: str
87+
label: str
88+
kind: str = "output"
89+
tool: str | None = None
90+
task_id: str | None = Field(default=None, serialization_alias="taskId")
91+
timestamp: str | None = None
92+
content: str | None = None
93+
content_length: int | None = Field(default=None, serialization_alias="contentLength")
94+
95+
8596
class ContextTask(WebModel):
8697
id: str
8798
title: str
8899
status: str
89100
summary: str = ""
90-
refs: list[str] = Field(default_factory=list)
101+
refs: list[ContextReference] = Field(default_factory=list)
91102
recovered: bool = False
92103

104+
@field_validator("refs", mode="before")
105+
@classmethod
106+
def _coerce_refs(cls, value: Any) -> Any:
107+
if not isinstance(value, list):
108+
return value
109+
return [
110+
{"ref": str(ref), "label": str(ref), "kind": "reference"}
111+
if isinstance(ref, str)
112+
else ref
113+
for ref in value
114+
]
115+
93116

94117
class TrunkSummary(WebModel):
95118
goal: str

src/sag/web/session_registry.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from sag.web.models import (
1818
BuildSummary,
1919
ContextMap,
20+
ContextReference,
2021
EvidenceGroup,
2122
EvidenceRecord,
2223
ExecutionSessionDetail,
@@ -301,8 +302,14 @@ def _backfill_completed_report_task(
301302
for task in context.tasks:
302303
if _is_incomplete_final_report_context_task(task):
303304
refs = [*task.refs]
304-
if report_ref and report_ref not in refs:
305-
refs.append(report_ref)
305+
if report_ref and report_ref not in {ref.ref for ref in refs}:
306+
refs.append(
307+
ContextReference(
308+
ref=report_ref,
309+
label=report_ref,
310+
kind="report",
311+
)
312+
)
306313
tasks.append(
307314
task.model_copy(
308315
update={

src/sag/web/static/assets/index-ByYg71fl.css

Lines changed: 32 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)