Skip to content
Merged
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
118 changes: 118 additions & 0 deletions pilot_agent/agent/acceptance_blueprints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True)
class AcceptanceBlueprint:
id: str
title: str
cadence: str
purpose: str
commands: tuple[str, ...]
required_credentials: tuple[str, ...] = ()
artifacts: tuple[str, ...] = ()
notes: tuple[str, ...] = ()


BLUEPRINTS: tuple[AcceptanceBlueprint, ...] = (
AcceptanceBlueprint(
id="nightly-checks",
title="Nightly local acceptance",
cadence="nightly after main changes",
purpose="Re-run the repository quality bar and surface regressions before users hit them.",
commands=(
"UV_CACHE_DIR=.uv-cache uv sync --all-groups --frozen",
"scripts/run_tests.sh",
"pilot-agent doctor --json",
),
artifacts=(
"test transcript",
"doctor JSON",
),
notes=(
"This blueprint validates the existing local MVP pipeline; it does not deploy.",
"Treat a red doctor check as an acceptance failure unless explicitly waived.",
),
),
AcceptanceBlueprint(
id="dependency-audit",
title="Dependency drift audit",
cadence="weekly or before a release tag",
purpose="Detect lockfile drift and dependency metadata issues without adding new services.",
commands=(
"uv lock --check",
"UV_CACHE_DIR=.uv-cache uv sync --all-groups --frozen",
"UV_CACHE_DIR=.uv-cache uv run python -m pip check",
),
artifacts=(
"uv lock check output",
"pip check output",
),
notes=(
"Security scanners can be wired later; this v1 blueprint sticks to installed tooling.",
),
),
AcceptanceBlueprint(
id="deploy-verification",
title="Deploy readiness verification",
cadence="before release or deploy phase",
purpose="Confirm local package and container build paths still work before handoff.",
commands=(
"UV_CACHE_DIR=.uv-cache uv build",
"docker compose build",
"pilot-agent doctor --json",
),
required_credentials=(
"VERCEL_TOKEN when deploy phase is enabled",
),
artifacts=(
"dist build output",
"docker build output",
"doctor JSON",
),
notes=(
"This is a readiness blueprint; publishing remains intentionally separate.",
),
),
)

_BY_ID = {blueprint.id: blueprint for blueprint in BLUEPRINTS}


def list_blueprints() -> tuple[AcceptanceBlueprint, ...]:
return BLUEPRINTS


def get_blueprint(blueprint_id: str) -> AcceptanceBlueprint:
try:
return _BY_ID[blueprint_id]
except KeyError as exc:
known = ", ".join(sorted(_BY_ID))
raise ValueError(f"unknown acceptance blueprint {blueprint_id!r}; known: {known}") from exc


def render_blueprint(blueprint: AcceptanceBlueprint) -> str:
lines = [
f"# {blueprint.title}",
f"id: {blueprint.id}",
f"cadence: {blueprint.cadence}",
"",
blueprint.purpose,
"",
"Commands:",
*[f"- {command}" for command in blueprint.commands],
]
if blueprint.required_credentials:
lines.extend(
[
"",
"Required credentials:",
*[f"- {item}" for item in blueprint.required_credentials],
]
)
if blueprint.artifacts:
lines.extend(["", "Artifacts:", *[f"- {item}" for item in blueprint.artifacts]])
if blueprint.notes:
lines.extend(["", "Notes:", *[f"- {item}" for item in blueprint.notes]])
return "\n".join(lines)
51 changes: 37 additions & 14 deletions pilot_agent/agent/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,15 @@
from pathlib import Path

from pilot_agent.agent.safety import redact_sensitive_text
from pilot_agent.agent.types import CompletionResponse, Message, Role, ToolResult, ToolSpec
from pilot_agent.agent.types import (
CompletionResponse,
Message,
Role,
SessionEvent,
ToolResult,
ToolSpec,
to_json,
)
from pilot_agent.providers.base import Provider

SUMMARY_PROMPT = """Compress the agent work history into a state document.
Expand All @@ -32,6 +40,10 @@ def __init__(
self.summarizer = summarizer or provider
self.session_log = session_log
self._ineffective_compactions = 0
session_anchor = str(session_log.resolve()) if session_log is not None else "memory"
self._root_session_id = session_anchor
self._current_session_id = f"{session_anchor}#0"
self._compaction_depth = 0

def prepare(self, system: str, history: list[Message]) -> list[Message]:
prepared = copy.deepcopy(history)
Expand Down Expand Up @@ -67,7 +79,7 @@ def replace_provider(self, provider: Provider) -> None:
def _truncate_tool_results(self, system: str, history: list[Message]) -> list[Message]:
cutoff = self._last_turn_start(history, turns=5)
seen_tool_outputs: dict[str, str] = {}
for message in reversed(history[:cutoff]):
for message in history[:cutoff]:
if message.role is not Role.TOOL:
continue
for result in message.tool_results:
Expand Down Expand Up @@ -181,20 +193,31 @@ def _last_turn_start(history: list[Message], turns: int) -> int:
def _write_compaction_event(self, before: int, after: int) -> None:
if self.session_log is None:
return
next_depth = self._compaction_depth + 1
parent_session_id = self._current_session_id
current_session_id = f"{self._root_session_id}#{next_depth}"
event = SessionEvent(
event_type="compaction",
payload={
"before_tokens": before,
"after_tokens": after,
"saved_tokens": max(0, before - after),
"ineffective_count": self._ineffective_compactions,
"provenance": {
"root_session_id": self._root_session_id,
"parent_session_id": parent_session_id,
"current_session_id": current_session_id,
"session_kind": "continuation",
"creator_kind": "compaction",
"compaction_depth": next_depth,
},
},
)
self.session_log.parent.mkdir(parents=True, exist_ok=True)
with self.session_log.open("a", encoding="utf-8") as handle:
handle.write(
json.dumps(
{
"_type": "compaction",
"before_tokens": before,
"after_tokens": after,
"saved_tokens": max(0, before - after),
"ineffective_count": self._ineffective_compactions,
}
)
+ "\n"
)
handle.write(to_json(event) + "\n")
self._current_session_id = current_session_id
self._compaction_depth = next_depth


def build_system_prompt(
Expand Down
Loading
Loading