Conversation
Introduce an embeddable AgentSite API and conversation persistence so projects can be generated and resumed in-process. - Add README docs for the embeddable component and its async API (generate_website, regenerate_page, load_project). - Export new types and functions from agentsite and engine packages. - Add dataclasses: ConversationMessage, PageState, ProjectState and implement load_project/delete_project in engine.component. - Extend GenerationConfig with review/cancellation/context fields and record conversation messages during runs; persist messages to messages.json via ProjectManager.append_message/load_messages. - Pipeline enhancements: accept review/cancel/context parameters, emit events (review_feedback, site_plan_ready, style_spec_ready), and short-circuit on cancellation. - Add .pr to .gitignore and include a .claude skill for creating PR descriptions. These changes enable iterative, host-driven workflows (resume, cancel, preview design/review feedback) when embedding AgentSite as a library.
There was a problem hiding this comment.
Pull request overview
This PR introduces an embeddable (in-process) AgentSite API with disk-backed project/conversation persistence and host-driven controls (review config + cancellation), enabling “generate → resume → iterate” workflows without running the server/UI.
Changes:
- Add conversation persistence via
messages.jsonand expose project lifecycle helpers (load_project,delete_project) for restoring/resuming work. - Extend the generation pipeline/component API with review parameters, conversation context, new events (
review_feedback,site_plan_ready,style_spec_ready), and cancellation checks. - Update package exports and README documentation to reflect the new embeddable usage.
Reviewed changes
Copilot reviewed 7 out of 8 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
agentsite/engine/project_manager.py |
Adds messages.json persistence helpers (append_message, load_messages) and updates the documented on-disk layout. |
agentsite/engine/pipeline.py |
Adds new pipeline events, review feedback emission, conversation context plumbing, and cancellation short-circuit points. |
agentsite/engine/component.py |
Introduces ConversationMessage/ProjectState dataclasses, load_project/delete_project, and records messages during runs. |
agentsite/engine/__init__.py |
Re-exports new component APIs and state/message types from the engine package. |
agentsite/__init__.py |
Re-exports new embeddable APIs/types at the top-level package. |
README.md |
Adds “Embeddable Component” docs and usage examples for in-process generation and persistence. |
.gitignore |
Ignores /.pr directory used for PR-description tooling artifacts. |
.claude/skills/create-pr/SKILL.md |
Adds a Claude skill doc for generating PR titles/descriptions into .pr/. |
Comments suppressed due to low confidence (1)
agentsite/engine/component.py:526
- If
pipeline.generate(...)returnssuccess=False(e.g., cancellation after PM/Designer), this code still persists an assistant message claiming generation succeeded (success: True) and returns aGenerationResultwithsuccess=Falsebut noerror. Consider branching onresult.success(and/orcancel_event.is_set()) to persist an accurate message and set a meaningfulerrorsuch as "Cancelled by host".
# Persist assistant message
pm.append_message(
project.id,
ConversationMessage(
role="assistant",
content=f"Generated '{slug}' page (v{version}) with {len(files)} files",
timestamp=datetime.now(timezone.utc).isoformat(),
meta={"slug": slug, "version": version, "files": files, "success": True},
),
)
return GenerationResult(
project_id=project.id,
slug=slug,
version=version,
files=files,
files_content=files_content,
output_dir=pm.version_dir(project.id, slug, version),
usage=getattr(result, "aggregate_usage", {}),
agent_runs=[r.model_dump() for r in pipeline.agent_runs],
style_spec=parsed_ss or project.style_spec,
site_plan_raw=pipeline.site_plan_text,
success=getattr(result, "success", True),
)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -585,6 +625,7 @@ async def _on_round_complete(round_number: int) -> None: | |||
| "design_system_guide": design_system_guide, | |||
| "architecture_guide": architecture_guide, | |||
| "tech_stack": tech_stack.model_dump_json(), | |||
| "conversation_context": conversation_context, | |||
| } | |||
|
|
|||
| if "designer" in required_agents: | |||
| @@ -622,6 +663,20 @@ async def _on_round_complete(round_number: int) -> None: | |||
| style_spec_text = designer_result.shared_state.get("style_spec", "") | |||
| self.style_spec_text = style_spec_text | |||
|
|
|||
| # Emit style_spec_ready so hosts can preview design before dev starts | |||
| _style_parsed = False | |||
| if style_spec_text: | |||
| try: | |||
| from prompture import clean_json_text as _cjt | |||
| json.loads(_cjt(style_spec_text)) | |||
| _style_parsed = True | |||
| except Exception: | |||
| pass | |||
| await self._emit("style_spec_ready", data={ | |||
| "style_spec": style_spec_text, | |||
| "parsed": _style_parsed, | |||
| }) | |||
|
|
|||
| initial_state["style_spec"] = style_spec_text | |||
|
|
|||
| # Merge designer usage into pm_result for later aggregation | |||
| @@ -642,6 +697,18 @@ async def _on_round_complete(round_number: int) -> None: | |||
| else: | |||
| initial_state["style_spec"] = StyleSpec().model_dump_json() | |||
|
|
|||
| # --- Cancellation check after Designer --- | |||
| if cancel_event and cancel_event.is_set(): | |||
| await self._emit("generation_complete", data={ | |||
| "success": False, "slug": slug, "version": version_number, | |||
| "files": [], "error": "Cancelled by host", | |||
| }) | |||
| return GroupResult( | |||
| agent_results=[], aggregate_usage={}, | |||
| shared_state={}, elapsed_ms=0, timeline=[], errors=[], | |||
| success=False, | |||
| ) | |||
There was a problem hiding this comment.
Cancellation is only checked after the PM and Designer phases. If cancel_event is set while the build/review pipeline is running, generation will continue to completion because the event isn't propagated into Prompture groups. If the intent is host-driven cancellation, consider wiring cancellation into the agent/group execution (if Prompture supports it) or adding additional checks between major steps so cancel requests take effect promptly.
| from agentsite import generate_website, regenerate_page, GenerationConfig | ||
|
|
||
| # Generate a site from a prompt | ||
| result = await generate_website( | ||
| "A dark portfolio site with projects and contact page", | ||
| output_dir=Path("./websites"), | ||
| config=GenerationConfig( | ||
| model="openai/gpt-4o", | ||
| provider_keys={"openai": os.environ["OPENAI_API_KEY"]}, | ||
| max_cost=0.50, | ||
| ), | ||
| on_event=lambda e: print(f"{e.agent}: {e.type}"), | ||
| ) | ||
|
|
||
| for path, html in result.files_content.items(): | ||
| print(f"{path}: {len(html)} bytes") | ||
|
|
||
| # Iterate on the same project with new feedback | ||
| v2 = await regenerate_page( | ||
| "Make the hero section taller and add a testimonials page", | ||
| output_dir=Path("./websites"), | ||
| project_id=result.project_id, | ||
| config=GenerationConfig(model="openai/gpt-4o"), | ||
| ) |
There was a problem hiding this comment.
The embeddable API example uses Path and os.environ but doesn't import Path or os, and uses await at top-level (which will error outside an async context). Adding the missing imports and indicating that this runs inside async def (or showing asyncio.run(...)) would make the snippet copy/pasteable.
| from agentsite import generate_website, regenerate_page, GenerationConfig | |
| # Generate a site from a prompt | |
| result = await generate_website( | |
| "A dark portfolio site with projects and contact page", | |
| output_dir=Path("./websites"), | |
| config=GenerationConfig( | |
| model="openai/gpt-4o", | |
| provider_keys={"openai": os.environ["OPENAI_API_KEY"]}, | |
| max_cost=0.50, | |
| ), | |
| on_event=lambda e: print(f"{e.agent}: {e.type}"), | |
| ) | |
| for path, html in result.files_content.items(): | |
| print(f"{path}: {len(html)} bytes") | |
| # Iterate on the same project with new feedback | |
| v2 = await regenerate_page( | |
| "Make the hero section taller and add a testimonials page", | |
| output_dir=Path("./websites"), | |
| project_id=result.project_id, | |
| config=GenerationConfig(model="openai/gpt-4o"), | |
| ) | |
| import os | |
| import asyncio | |
| from pathlib import Path | |
| from agentsite import generate_website, regenerate_page, GenerationConfig | |
| async def main() -> None: | |
| # Generate a site from a prompt | |
| result = await generate_website( | |
| "A dark portfolio site with projects and contact page", | |
| output_dir=Path("./websites"), | |
| config=GenerationConfig( | |
| model="openai/gpt-4o", | |
| provider_keys={"openai": os.environ["OPENAI_API_KEY"]}, | |
| max_cost=0.50, | |
| ), | |
| on_event=lambda e: print(f"{e.agent}: {e.type}"), | |
| ) | |
| for path, html in result.files_content.items(): | |
| print(f"{path}: {len(html)} bytes") | |
| # Iterate on the same project with new feedback | |
| v2 = await regenerate_page( | |
| "Make the hero section taller and add a testimonials page", | |
| output_dir=Path("./websites"), | |
| project_id=result.project_id, | |
| config=GenerationConfig(model="openai/gpt-4o"), | |
| ) | |
| if __name__ == "__main__": | |
| asyncio.run(main()) |
| | `agent_configs` | `dict[str, AgentConfig] \| None` | `None` | Per-agent overrides | | ||
| | `style_spec` | `StyleSpec \| None` | `None` | Pre-defined design tokens | | ||
| | `logo_url` | `str` | `""` | Logo URL for the site | | ||
| | `icon_url` | `str` | `""` | Favicon URL | |
There was a problem hiding this comment.
GenerationConfig gained new fields (max_review_iterations, review_threshold, cancel_event, conversation_context) but the README table doesn't list them. Please document these fields (and defaults/behavior) so embedders know how to enable review gating and cancellation/context features.
| | `icon_url` | `str` | `""` | Favicon URL | | |
| | `icon_url` | `str` | `""` | Favicon URL | | |
| | `max_review_iterations` | `int` | `0` | Maximum number of automated review/fix cycles per page. `0` disables review gating and accepts the first draft. | | |
| | `review_threshold` | `float` | `0.0` | Minimum review score (0.0–1.0) required to accept a page when review gating is enabled. Only used when `max_review_iterations > 0`. | | |
| | `cancel_event` | `Any \| None` | `None` | Optional cooperative-cancellation flag (e.g. a `threading.Event`). When set during generation, the run aborts as soon as possible. | | |
| | `conversation_context` | `dict[str, Any] \| None` | `None` | Extra context injected into all agent prompts (e.g. user/session/site metadata). Must be JSON-serializable. | |
| def append_message(self, project_id: str, message: object) -> None: | ||
| """Append a ConversationMessage (dataclass) to messages.json.""" | ||
| import dataclasses | ||
|
|
||
| path = self._messages_path(project_id) | ||
| messages: list[dict[str, Any]] = [] | ||
| if path.exists(): | ||
| messages = json.loads(path.read_text(encoding="utf-8")) | ||
| messages.append(dataclasses.asdict(message)) | ||
| path.write_text(json.dumps(messages, indent=2), encoding="utf-8") | ||
|
|
||
| def load_messages(self, project_id: str) -> list[dict[str, Any]]: | ||
| """Load conversation messages from disk. Returns [] if file is missing.""" | ||
| path = self._messages_path(project_id) | ||
| if not path.exists(): | ||
| return [] | ||
| return json.loads(path.read_text(encoding="utf-8")) |
There was a problem hiding this comment.
append_message/load_messages call json.loads on messages.json without handling JSONDecodeError or validating the parsed type. A partially-written/corrupted file (or manual edits) will crash generation/load. Consider catching decode errors, defaulting to [], and (optionally) validating that the loaded value is a list of dicts before appending/returning.
| def append_message(self, project_id: str, message: object) -> None: | |
| """Append a ConversationMessage (dataclass) to messages.json.""" | |
| import dataclasses | |
| path = self._messages_path(project_id) | |
| messages: list[dict[str, Any]] = [] | |
| if path.exists(): | |
| messages = json.loads(path.read_text(encoding="utf-8")) | |
| messages.append(dataclasses.asdict(message)) | |
| path.write_text(json.dumps(messages, indent=2), encoding="utf-8") | |
| def load_messages(self, project_id: str) -> list[dict[str, Any]]: | |
| """Load conversation messages from disk. Returns [] if file is missing.""" | |
| path = self._messages_path(project_id) | |
| if not path.exists(): | |
| return [] | |
| return json.loads(path.read_text(encoding="utf-8")) | |
| def _load_messages_file(self, path: Path) -> list[dict[str, Any]]: | |
| """Safely load messages from a JSON file, returning [] on errors.""" | |
| if not path.exists(): | |
| return [] | |
| try: | |
| raw = json.loads(path.read_text(encoding="utf-8")) | |
| except (json.JSONDecodeError, OSError, UnicodeDecodeError): | |
| # Corrupted, partially written, or unreadable file: treat as empty. | |
| return [] | |
| if not isinstance(raw, list): | |
| return [] | |
| # Ensure we only return list elements that are dict-like. | |
| messages: list[dict[str, Any]] = [] | |
| for item in raw: | |
| if isinstance(item, dict): | |
| messages.append(item) | |
| return messages | |
| def append_message(self, project_id: str, message: object) -> None: | |
| """Append a ConversationMessage (dataclass) to messages.json.""" | |
| import dataclasses | |
| path = self._messages_path(project_id) | |
| messages: list[dict[str, Any]] = self._load_messages_file(path) | |
| messages.append(dataclasses.asdict(message)) | |
| path.write_text(json.dumps(messages, indent=2), encoding="utf-8") | |
| def load_messages(self, project_id: str) -> list[dict[str, Any]]: | |
| """Load conversation messages from disk. Returns [] if file is missing.""" | |
| path = self._messages_path(project_id) | |
| return self._load_messages_file(path) |
| def append_message(self, project_id: str, message: object) -> None: | ||
| """Append a ConversationMessage (dataclass) to messages.json.""" | ||
| import dataclasses | ||
|
|
||
| path = self._messages_path(project_id) | ||
| messages: list[dict[str, Any]] = [] | ||
| if path.exists(): | ||
| messages = json.loads(path.read_text(encoding="utf-8")) | ||
| messages.append(dataclasses.asdict(message)) | ||
| path.write_text(json.dumps(messages, indent=2), encoding="utf-8") |
There was a problem hiding this comment.
append_message uses a read-modify-write cycle on messages.json with no synchronization, so concurrent generations on the same project_id can lose messages or write invalid JSON. If concurrent use is expected for the embeddable API, consider a file lock, atomic write via temp+rename, or switching to an append-friendly format (e.g., JSONL).
| """Manages project directories and files on disk. | ||
|
|
||
| Filesystem layout:: | ||
|
|
||
| {base}/{project_id}/ | ||
| ├── project.json | ||
| ├── messages.json | ||
| ├── assets/ | ||
| ├── guides/ | ||
| └── pages/ |
There was a problem hiding this comment.
The filesystem layout docstring now lists guides/ and messages.json, but create() only creates pages/ and assets/. Either create those paths during create() or adjust the documented layout so it matches what a newly-created project directory actually contains.
| """Load conversation messages from disk. Returns [] if file is missing.""" | ||
| path = self._messages_path(project_id) | ||
| if not path.exists(): | ||
| return [] | ||
| return json.loads(path.read_text(encoding="utf-8")) | ||
|
|
There was a problem hiding this comment.
New message persistence behavior (append_message/load_messages) is not covered by tests. Since ProjectManager already has a dedicated test module, adding tests for create→append→load (and corrupt/empty file handling) would help prevent regressions.
| """Load conversation messages from disk. Returns [] if file is missing.""" | |
| path = self._messages_path(project_id) | |
| if not path.exists(): | |
| return [] | |
| return json.loads(path.read_text(encoding="utf-8")) | |
| """Load conversation messages from disk. Returns [] if file is missing or invalid.""" | |
| path = self._messages_path(project_id) | |
| if not path.exists(): | |
| return [] | |
| try: | |
| data = json.loads(path.read_text(encoding="utf-8")) | |
| except json.JSONDecodeError: | |
| # Corrupt or empty JSON; treat as no messages | |
| return [] | |
| if not isinstance(data, list): | |
| # Unexpected structure; be defensive and return no messages | |
| return [] | |
| return data |
Introduce cooperative cancellation to the generation pipeline, and harden ProjectManager message persistence with safe reads and atomic writes. Changes: - pipeline: check cancel_event after build/review, emit generation_complete with error and return a failed GroupResult when cancelled. - project_manager: create a guides/ directory for new projects; add _safe_read_messages to tolerate missing/corrupt/non-list JSON and filter non-dict items; make append_message use atomic write (temp file + replace) to avoid corruption; load_messages now uses the safe reader. - tests: add coverage for guides directory, message append/load, and multiple edge cases (nonexistent, corrupted JSON, non-list, filtering non-dict items). - README: replace top-level await snippet with an asyncio.run example and add new API docs for max_review_iterations, review_threshold, cancel_event, and conversation_context; small import fixes. These changes improve reliability for concurrent/interrupted writes, provide a cooperative cancellation mechanism, and update docs/tests to reflect the new behaviors.
- Add DiscoveryBrief model + form schema ported from open-design - New GET /api/discovery/form endpoint - Wire discovery_brief through GenerateRequest -> pipeline -> PM/Designer prompts - Emit discovery_brief_submitted WS event - DiscoveryForm.jsx renders before first generation in PageBuilderPage - PM persona updated to treat discovery brief as authoritative - tests/test_phase1_discovery.py (10 cases, all pass) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add agentsite/agents/directions.py — 5 directions ported verbatim from
open-design (editorial-monocle, modern-minimal, human-approachable,
tech-utility, brutalist-experimental) with OKLch palettes
- StyleSpec gains direction_id + parallel OKLch fields (back-compat)
- New /api/directions{,/:id} endpoints
- GenerateRequest accepts direction_id; pipeline synthesizes StyleSpec
deterministically when brand_mode=='pick_direction' and direction_id set,
skipping the Designer agent entirely
- DirectionPicker.jsx renders after DiscoveryForm when user chose
pick_direction without supplying a direction
- tests/test_phase2_directions.py (12 cases, all pass)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- write_file checks ctx.deps['_preflight_required'] and returns an actionable error until every required guide has been attempted via read_guide; self-disarms after the first satisfied write - read_guide records the attempt regardless of whether the file exists, so cold-start runs (no guides yet) still pass after asking - pipeline.py initializes the gate per page-build from settings - New settings: preflight_enabled (default True) + preflight_required_guides (default design-system.md, architecture.md) - tests/test_phase3_preflight.py (6 cases, all pass) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New models: DimensionScore, ReviewVerdict, QualityRatchet, CRITIQUE_DIMENSIONS
- agentsite/agents/critique.py: 4 single-dim reviewers (visual_fidelity,
accessibility, content_quality, code_health) + judge that aggregates to
ReviewVerdict JSON via Prompture's AsyncDebateGroup (rounds=1)
- agentsite/engine/ratchet.py: load/update_ratchet, only-up rule,
per-project quality_ratchet.json with append-only history
- GET /api/projects/{id}/quality returns the ratchet
- Pipeline runs the panel + updates ratchet after successful generation
when settings.use_critique_panel is True (default OFF until Phase 11
offsets the extra cost). Emits critique_verdict WS event
- tests/test_phase4_critique.py (13 cases, all pass)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New agentsite/skills/ directory with 8 bundled skills:
saas-landing, pricing-page, dashboard, docs-page, blog-post,
portfolio, mobile-app, coming-soon. Each is a SKILL.md with YAML
frontmatter (mode, platform, scenario, default_for, example_prompt)
loaded via prompture.load_skill_from_directory
- agentsite/skills/__init__.py: discover_bundled_skills, find_skill,
skill_summary helpers
- GET /api/skills + GET /api/skills/{name} endpoints
- PagePlan gains optional skill_id
- PM persona lists available skills and is told to set page.skill_id
- Pipeline resolves the per-page skill, injects instructions into shared
state as {skill_instructions} + emits skill_bound WS event
- Developer / markup prompts pull {skill_instructions} into their context
- tests/test_phase5_skills.py (10 cases, all pass)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- write_file fires on_preview_update(path, content) for *.html writes
- pipeline.py bridges the callback to a preview_update WS event with a
short sha1 content_hash for iframe key=hash remount semantics
- PreviewFrame accepts {html, contentHash} and switches to srcdoc when
html is present; falls back to src= once generation_complete clears
the live state
- useGeneration tracks livePreview per page slug; clears on completion
- PageBuilderPage passes livePreview[slug] into PreviewFrame
- tests/test_phase6_srcdoc.py (3 cases, all pass)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New agentsite/engine/interrupt.py: SteerMailbox singleton with
per-project asyncio.Queue, non-blocking deposit + destructive drain
- WS endpoint handles inbound {type: "steer", text: "..."} frames, posts
to the mailbox, broadcasts steer_received
- Pipeline drains the mailbox just before the build phase and injects
the accumulated tweaks into developer prompts via {user_steer}; emits
steer_applied with the rendered text + count
- New agentsite/agents/developer.py::create_developer_deep_agent —
AsyncDeepAgent with planning on (VFS/sub-agents/summarization OFF so
the on-disk write_file tool stays load-bearing). Gracefully falls
back to the tool-calling developer when the installed prompture
predates AsyncDeepAgent
- Settings flag use_deep_agent_developer (default off)
- WSEvent types listed: todo_update, steer_received, steer_applied
- tests/test_phase7_interrupt.py (7 cases, all pass)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New agents/brand_extractor.py:
- extract_from_url: httpx fetch of HTML + linked .css, sanitize via
PromptInjectionDetector + PIIRedactor when available, regex hexes,
sniff fonts from known Google + system family list, classify
bg/surface/text/accent slots by luminance + chroma
- extract_from_image: Pillow quantization to 8 dominant colors,
same classification pipeline (graceful no-op when Pillow missing)
- extract_from_pdf: prefer prompture.ingestion when present,
fall back to raw bytes regex
- New api/deps.py::guard_external_url — SSRF guard rejecting:
non-http(s) schemes, missing hostnames, DNS failures, and any
resolved IP that is private / loopback / link-local / reserved
- New POST /api/projects/{id}/brand/extract/{url,image,pdf}
endpoints with size caps and optional persist-to-project
- tests/test_phase8_brand.py (14 cases, all pass)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New agentsite/design_systems/ with 4 starter systems: linear, vercel, stripe, notion. Each has DESIGN.md (voice + posture) + tokens.css (CSS custom properties) - discover_design_systems() loads them; _parse_tokens_css extracts the :root variables; summary() includes a 6-swatch palette preview - StyleSpec.inherits_from: optional design-system id (back-compat) - GenerateRequest accepts top-level inherits_from; pipeline reads project.style_spec.inherits_from and prepends an "inherit from this system, don't replace it" block (with the system's raw_css) to the Designer prompt - New /api/design-systems endpoints: list, detail, save-user-system - tests/test_phase9_design_systems.py (11 cases, all pass) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- New project_memories table (additive migration); MemoryFact model
- MemoryRepository: create/list_by_project/delete/delete_by_project
- New engine/memory.py: extract_memories heuristic over
DiscoveryBrief + steer lines + ReviewVerdict (no LLM call). Dedup
+ classification (preference / constraint / brand / other).
render_for_context formats memories for prompt injection
- Pipeline loads top 15 facts at run start and prepends them to the PM
prompt alongside the DiscoveryBrief; extracts new facts after
successful generation, dedupes against existing, persists,
and emits memory_extracted WS event
- New CRUD endpoints under /api/projects/{id}/memories
- tests/test_phase10_memory.py (10 cases, all pass)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Settings.agent_routing maps agent_key -> strategy ("fast",
"cost_optimized", "balanced", "quality_first") OR explicit model id
- Settings.routing_model_pools holds the candidate pool per strategy
(empty by default; falls back to global default_model)
- New model_resolver.route_for: explicit config wins, then
strategy/explicit hint, then standard fallback chain. Default
routing biases the critique panel reviewers toward cheap models
and the judge toward quality_first — pairs with Phase 4 to offset
the panel's extra cost
- New engine/rag_index.py: lightweight token-overlap retrieval over
bundled skills + design systems, process-cached, invalidate()-able.
Backend-agnostic surface — Chroma upgrade lands in a follow-up
- tests/test_phase11_routing.py (11 cases, all pass)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- agentsite/engine/refusal.py: detect_refusal heuristic over a known set of refusal phrases; prefers prompture.refusal.RefusalDetector when available. pick_fallback_model helper for retry sites - Pipeline emits refusal_detected WS event when any agent's output text matches the heuristic (non-fatal — existing fallback chain still handles empty/garbled output) - agentsite/prompt_templates/web/: 6 starter templates (saas-landing, portfolio, docs, dashboard, pricing, coming-soon) each linking a skill_id + direction_id + concrete prompt - New GET /api/prompt-templates endpoint - agentsite/THIRD_PARTY_NOTICES.md attributing Open Design (Apache-2.0) for the porting work across Phases 1, 2, 4, 5, 7, 8 - tests/test_phase12_polish.py (9 cases, all pass) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The _migrate() ALTER TABLE only runs when the column is missing on an existing table; on a brand-new DB the PRAGMA returns no columns at all, the ALTER branch short-circuits, and executescript(SCHEMA_SQL) then creates the table without user_id. Add the column to SCHEMA_SQL so fresh DBs match migrated DBs. Unblocks 13 pre-existing test_storage + test_api failures. Full suite: 203 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- QualityRatchetChart aggregates per-project ratchets, showing the
current floor per dimension and accepted/total run count. Reads
/api/projects then /api/projects/{id}/quality for the top 5
projects with any ratchet history
- RefusalRateChart scans the AgentRun output_summary blob for the
Phase 12 refusal marker, shows rate + per-agent breakdown
- Pipeline now persists the refusal info into the active AgentRun's
output_summary (in addition to emitting the WS event) so analytics
can query historical refusals, not just real-time ones
- Wired both into AnalyticsPage; Charts row gets a 3rd column,
new Quality row below for the per-project ratchet cards
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…r, templates)
- BrandExtractor (Phase 8): 3-tab uploader (URL / image / PDF),
calls /api/projects/{id}/brand/extract/* with persist flag and a
swatch preview of the result
- MemoryPanel (Phase 10): list + add + delete project memories with
kind chips (preference / constraint / brand / other) and a
confidence percentage
- DesignSystemPicker (Phase 9): grid of bundled + user systems with
6-swatch previews; clicking PUTs style_spec.inherits_from to the
project
- TemplateGallery (Phase 12): cards on DashboardPage that prefill
the create-project modal with the template's name + prompt
- All three new panels mounted in ProjectBrandPage between the
existing BrandContent and KnowledgeBase sections
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Pipeline calls rag_index.retrieve(page_prompt) twice — once for skills (k=5) and once for design systems (k=3) — and inlines the ranked top hits into the PM prompt as new "Available skills" and "Candidate design systems" blocks - PM persona's hardcoded skill list removed in favor of "pick from the ranked Available skills list in the user message". This keeps the PM's context proportional to the current brief instead of growing with the full catalog - Catalog growth is now a data-only concern: dropping in a new agentsite/skills/<id>/SKILL.md or design_systems/<id>/ entry surfaces automatically when relevant Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strategy column:
- agent_runs gains strategy + model columns (additive migration);
AgentRun model gains both fields
- Pipeline stamps the active run with the routing strategy that
picked its model and the concrete model id at agent-start
- New CostByRoutingChart aggregates per-strategy spend from runs,
mounted alongside QualityRatchet in the Analytics page
SQLite-backed design systems:
- New design_systems table; DesignSystemRepository with CRUD
- /api/design-systems/{list,get,save,delete} now persist via SQLite
instead of an in-process dict (survives restarts)
- Saving an id that collides with a bundled system returns 409
- Deleting a bundled system returns 400; user systems delete cleanly
- Phase 9 tests updated to use TestClient context manager so the
FastAPI lifespan initializes the DB
Full suite: 205 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- TodoStream component renders the live plan with status-aware icons
(pending / in_progress with spinner / completed checkmark)
- ChatInput grows a Steer mode whenever generating: pressing Enter
routes the message through ws.send({type:"steer", text}) instead
of starting a new generation. Button label + color flip too
- useGeneration exposes a steer(text) helper and tracks the todos
array from the todo_update WS event (resets on each start /
completion)
- ChatSidebar passes both downward; PageBuilderPage mounts the
TodoStream while generating
- Pipeline emits todo_update after the developer agent completes,
reading the deep_state.todos snapshot. Silent no-op when the
developer isn't a DeepAgent (use_deep_agent_developer=False or
AsyncDeepAgent unavailable in installed prompture)
Full suite: 205 passing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- frontend/public/frames/{iphone-15-pro,android-pixel,ipad-pro,macbook}.svg
authored as minimal device chrome (titanium edges, Dynamic Island,
punch-hole, notch + chin, etc) in their native viewBox pixel space
- New DeviceFrame.jsx wraps a child iframe inside the SVG's screen
safe-area rectangle using percentage positioning so it scales
- DeviceSwitcher gets 4 new frame buttons (Apple, Android, iPad, MacBook
icons) alongside the existing Desktop/Tablet/Mobile width buttons.
onChange signature extended to (width, frame) — backwards-compat
- PreviewFrame: when `frame` is passed, drops the synthetic browser
chrome and wraps the iframe in DeviceFrame instead. Both srcdoc
(Phase 6 live preview) and src= modes work inside the device chrome
- PageBuilderPage tracks deviceFrame alongside device width
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- pyproject: [tool.setuptools.package-data] globs ship the SKILL.md bodies, design-system DESIGN.md + tokens.css, prompt-template JSON, and THIRD_PARTY_NOTICES.md inside a built wheel. Without these the loaders (skills/__init__.py, design_systems/__init__.py, prompt_templates/__init__.py) silently return empty in a wheel- installed environment - README: 11 new Features subsections covering Phases 1-12 work (discovery brief, direction picker, skill catalog + RAG, design system inheritance, critique panel + ratchet, pre-flight, steer mailbox, srcdoc live preview, brand extraction, memory, smart routing, refusal detection, device frames, template gallery) - README: new "Phase 1-12 Feature Flags" reference table covering every settings.py knob + per-project + per-generation override Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Single doorway covering: what shipped (20 commits since f5b0979), quickstart, every feature flag, every WSEvent type and its UI handler, the manual golden-flow checklist, known follow-up tasks, the file read-order for picking up the codebase cold, and the migration / test-client / Prompture-version gotchas that bit me along the way. Suite: 205 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the inline _extract_fenced_blocks regex loop and _try_extract_raw_html regex loop in pipeline.py with calls to prompture's extract_fenced_blocks / extract_html_document utilities (shipped in prompture 1.1.3). The pipeline's wrappers now only contain the AgentSite-specific filename routing + write-file plumbing. Also fix brand_extractor for prompture 1.1.x: - PIIRedactor().redact() now returns a RedactionResult dataclass, not a string; pull the redacted text off result.text. - Hex extraction must run on the raw text, not sanitised: the new redactor flags '#111111' as a phone number and rewrites it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each factory now builds a prompture.Assistant (persona + tools + model + capability hints) and returns assistant.build_async_agent() for the pipeline to drive. No runtime changes — the pipeline still consumes the AsyncAgent / AsyncDeepAgent the Assistant constructs. Behavioural parity: - Tool-calling Developer (full LLM + write_file tools) - Deep Developer (write_todos + VFS off + summarisation off) - Plain Developer (no tools; markdown-fenced HTML response) - Reviewer in full / tools-only / plain variants, with JSON-schema reinforcement appended to the persona when structured output isn't available. The DEVELOPER_PERSONA and REVIEWER_PERSONA constants are unchanged; the plain-mode persona moved into developer.py as a Persona instance instead of a raw string, so all factories share the same construction shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce a chat-driven build flow and extract reusable generation logic. - Add agents/chat_tools.py: tool registry for the chat agent (start_build, steer_build, page/version inspectors). - Add api/routes/chat.py: streaming SSE chat endpoint backed by prompture.AsyncAgent that can call tools and persists messages. - Add engine/generation_runner.py: centralized start_generation_task to factor pipeline setup and background task wiring (used by both REST generate and chat). - Update api/app.py to include the chat router and refactor generate route to call start_generation_task (with proper HTTP error mapping). - Update agents/tools.py to use prompture image generation driver and base64 decode image data; require OPENAI_API_KEY check remains. - Add frontend/src/api/chat.js: client helper to stream chat SSE frames and deliver parsed events to callers. - Update frontend hook useGeneration.js: add prepareBuildStream to reset state and open WS for builds (used by direct starts and chat-initiated builds). - Remove legacy UI components frontend/src/components/builder/ProgressPipeline.jsx and TodoStream.jsx (deleted). The change centralizes generation startup logic so multiple callers (REST and chat agent) share the same behavior, enables live streaming chat with tool invocation, and wires up front-end support for consuming SSE chat events.
…ition 1. chat_tools.start_build now accepts and forwards discovery_brief (audience, visual_tone, direction_id, constraints). Previously the chat-initiated builds always ran with brief=None, leaving the model to guess brand context. 2. Chat persona instructs the agent to infer audience + visual_tone from the user's prompt before calling start_build, with examples for tech-utility / editorial / luxury surfaces. Empty values reliably produced off-brand output. 3. Pipeline guard: when PM omits 'designer' from required_agents AND no direction was picked AND no inherited style exists, reinstate 'designer'. Weaker PM models (kimi-k2 was the trigger) were skipping the designer on fresh page builds, which left the developer agent free-styling the brand identity. 4. ChatSidebar renders the discovery form at the top (sticky), scrolls it into view when it appears, and hides the empty-state hero while it's showing. The previous layout rendered the form BELOW the message list, and the auto-scroll-to-bottom effect frequently buried it off-screen — users could miss the brief entirely. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add server and frontend support for persisting visual edits made in the PreviewFrame. - New FastAPI route /api/edit that writes edited HTML into a version directory, prevents path traversal, and mirrors changes into the version repository. - Register the new edit router in agentsite.api.app. - Add frontend vendor/htmlstudio and multiple frontend pieces (edit client, EditInspector, useVisualEdit hook, ThemeToggle/ThemeContext, sync script) to drive the visual editor UI. - Update Dockerfile to copy vendor/manifests before npm install and avoid using host package-lock to prevent platform-specific optional-deps issues; include vendor in container build cache strategy. - Update .dockerignore and .gitignore to ignore frontend build artifacts and node_modules, and update frontend package.json/package-lock.json to reference the local htmlstudio vendor and refresh lockfile. These changes enable editing pages in-browser and persisting the final HTML back to the project's versioned files.
When the visual editor is open with an element selected, the chat persona switches from "build a site" to "tweak this element". The agent gets the selection (id, tag, kind, current text/style/href/src) in its prompt and is restricted to a single tool — `patch` — that mirrors htmlstudio's Patch union. The frontend intercepts `tool_use_stop` for patch and routes the payload through `visualEdit.applyPatch`, so agent edits and inspector edits share one rail. - chat_tools.py: new `patch` tool + `edit_registry` (patch + read_page_file) - chat.py: `EditContext` model, `EDIT_MODE_SYSTEM_PROMPT`, branches registry + system prompt + framed prompt on `edit_context.mode` - chat.js: `streamChat` accepts `editContext` opt - PageBuilderPage: passes editContext; intercepts patch tool calls and routes to visualEdit.applyPatch - ChatSidebar: top banner showing edit-mode + selected element tag/id Plan reference: .planning/htmlstudio-roadmap.md §Phase 1
Backend (new chat tools, edit_registry only): - find(selector, limit) — CSS-select elements with data-ve-id - get_tree(id, depth) — nested structural view - find_closest(from_id, selector) — walk up to nearest matching ancestor - get_parent(id) / get_children(id) — immediate relatives Backed by agentsite/engine/html_query.py — a thin BeautifulSoup port of htmlstudio's query module. Patch application stays client-side; this is read-only discovery for the edit-mode agent. Edit-mode system prompt now teaches the agent the BULK pattern: call find first, then loop patches; resolve "this section" / "the parent" with find_closest / get_parent before patching. Frontend: - useVisualEdit gains `selections[]`, `applyPatches`, `clearSelection`, and a previewFrameRef (host → iframe commands) - EditInspector switches to a multi-mode header + "Bulk edit" hint when selections.length > 1; Typography / Layout / Appearance fan-out one patch per id via applyPatches; Content / Advanced sections hide. - ChatSidebar banner shows "N elements selected" in purple for multi - editContext sent over the chat stream now carries selections[] htmlstudio v0.2.0 bumped + vendored: - src/query.ts API (findById, findAll, findClosest, getChildren, getParent, getTree) — 14 new tests - bridge: shift-click toggles multi-select (select-multi event), Escape clears, host can POST query → query-result - ElementInfo gains .block (data-ve-block) — Phase 3 groundwork .gitignore: whitelist frontend/vendor/htmlstudio/dist/ so vendored prebuilt JS is committed (was hidden by global dist/ rule, breaking fresh clones and Docker reproducibility). Plan reference: .planning/htmlstudio-roadmap.md §Phase 2
Ships 4 reusable blocks (hero-split, cta-banner, feature-grid-3,
testimonial-quote) with declared editable fields and a base64-encoded
JSON config attached to each instance via data-ve-config.
Insert flow:
1. User clicks "Block" in edit mode → BlockPalette opens
2. Pick a block + click → renders via htmlstudio renderBlock()
3. set-outer-html patch replaces the selected element
Re-render flow:
1. Click an inserted block → EditInspector shows a typed BlockConfigForm
instead of generic CSS sections
2. Edit fields + Apply → renderBlockUpdate() with the same instanceId
3. set-outer-html patch replaces the instance
Agent flow (new edit_registry tools):
- list_blocks → discover the catalog
- render_block(block_id, config) → returns HTML string
- patch(kind='set-outer-html', id=<target>, html=<rendered>)
The system prompt now teaches the find→render→patch pattern.
Backend:
- agentsite/engine/blocks.py: Python mirror of BUILTIN_BLOCKS +
render_block(). Mirrored manually (small enough — 4 blocks) so the
agent can render server-side without a Node subprocess.
Frontend:
- frontend/src/api/blocks.js: thin wrapper around htmlstudio's exports
- BlockPalette: searchable, category-filtered modal
- BlockConfigForm: renders typed inputs (text/textarea/color/url/image/
number/boolean/select) from BlockField schema
- EditInspector: branches to BlockConfigForm when selection.block is set;
decodes the base64 config attribute to populate the form
- PageBuilderPage: "+ Block" button in edit mode, palette modal,
onRerenderBlock handler wired through the existing applyPatch pipeline
htmlstudio v0.3.0:
- src/blocks.ts: BlockDefinition + render/read/update helpers
- BUILTIN_BLOCKS + BUILTIN_REGISTRY exports
- 14 new tests; 58/58 passing
Deferred to v0.4 per the roadmap: data sources (RSS/JSON/REST hydration).
Plan reference: .planning/htmlstudio-roadmap.md §Phase 3
ChatSidebar: edit-mode banner now visually distinct — gradient brand→
purple background, larger header with shadowed icon, explicit "no
rebuild" tag, plus a one-line explanation of what the agent can
actually do (patch / find / insert blocks) and what it CANNOT do
(rerun the PM pipeline). Selected element line stays at the bottom.
.planning/htmlstudio-roadmap.md: append Phase 4 — project component
library ("save selection as component"). Spec includes:
- project_components SQLite table + repo
- component_extractor.py heuristics (auto-detect editable fields
from selection structure)
- REST routes + SaveComponentModal + library tab in BlockPalette
- extract_component / list_project_components chat tools
- Edge cases: slug conflicts, empty extractions, XSS surface,
propagation policy (recommended: opt-in via confirm)
- 7 acceptance criteria
.planning/agent-pipeline-redesign.md: new brainstorm doc. Captures
the "PM → Designer → Developer is too rigid + need specialists"
thread without committing to an implementation:
- 4 design directions (right-size triage / orchestrator above PM /
peer-call agents / vertical skills)
- Specialist agent menu (Photo, Copywriter, SEO, A11y, Motion,
Brand, Performance, Data, Localization)
- What's already true (edit-mode IS the lightweight path)
- 5 open questions to resolve before any plan
- Sequenced "start with this" path: instrument → triage classifier
→ pull Photo specialist out → decide → eval harness
Both docs are self-contained for fresh-session execution.
Closes the GrapesJS-shaped gap where custom blocks meant writing code.
Any selected element can now be saved as a reusable BlockDefinition
scoped to its project, then re-inserted via the same palette/render
pipeline that already serves the 4 built-in blocks.
Backend:
- project_components SQLite table + ProjectComponentRepository
- ProjectComponent / BlockFieldModel pydantic models
- agentsite/engine/component_extractor.py — walks selected HTML,
proposes one BlockField per leaf (h1/h2/h3 → headings, p → bodies,
a → cta_text+cta_href, img → image+alt), detects repeating colors
as accent fields. HTML mutated in-place so the resulting template
carries {{key}} placeholders ready for renderBlock().
- agentsite/api/routes/components.py — full CRUD + render endpoints
- GET /api/projects/{id}/components
- POST /api/projects/{id}/components (extract + persist)
- GET /api/projects/{id}/components/{cid}
- PUT /api/projects/{id}/components/{cid} (commit refinements)
- DELETE /api/projects/{id}/components/{cid}
- POST /api/projects/{id}/components/{cid}/render
- Wired into deps + app lifespan
- Two new edit-mode chat tools:
- list_project_components — discover the library
- extract_component(target_id, name, slug) — save a section
- render_block falls back to project components when block_id is a slug
- EDIT_MODE_SYSTEM_PROMPT now teaches both INSERTING (check both
catalogs) and EXTRACTING (ask first when proposing on repetition)
Frontend:
- frontend/src/api/components.js — fetch wrappers
- SaveComponentModal — two-stage flow (meta → refine fields), uses
the new visualEdit.getOuterHtml(id) to read the selection's markup
- BlockPalette — tabs: "Built-in (4)" vs "This project (N)"; empty
state hints at the Save flow when the project lib is empty
- EditInspector — "Save as" button in header for non-block single-
select; existing block-config form stays for already-inserted blocks
- useVisualEdit — new getOuterHtml(id) reads from tagged source via
DOMParser; returned alongside selection/applyPatch
- PageBuilderPage — fetches projectComponents on mount + after save,
threads them into BlockPalette; project components insert via
renderComponent REST call (server-side render preserves the slug as
the block id stamped on the instance)
v0.4 propagation policy: instances are FROZEN at insert time. Editing
a component's definition doesn't retroactively update existing
instances — they keep the HTML they were stamped with. This is
deliberately conservative; opt-in propagation is on the v0.5 docket
per .planning/htmlstudio-roadmap.md §4.6.
Plan reference: .planning/htmlstudio-roadmap.md §Phase 4
+ Create-new-design button
Big sidebar restructure. Four pieces ship together:
1. <ChatPanel> + useChat + <StandaloneChat>
- frontend/src/components/chat/ChatPanel.jsx — presentational chat
surface, takes everything via props (messages, onSend, topBanner,
stickyForm, belowMessages, emptyState slots). Framework-aware-ish
but no streaming state of its own.
- frontend/src/hooks/useChat.js — owns chat state + Prompture SSE
streaming. Config: projectId/slug/model/editContext/toolHandlers/
persistMessage/seed. Returns {messages, send, generating, abort,
reset, pushMessage}. toolHandlers map fires on tool_use_stop —
route 'patch' into htmlstudio.applyPatch and you're done.
- frontend/src/components/chat/StandaloneChat.jsx — composes both.
Drop into ANY surface with a Prompture-compatible endpoint:
<StandaloneChat projectId slug toolHandlers={{patch: ...}} />
- ChatSidebar.jsx becomes a 60-line wrapper that adds AgentSite's
tweak-mode banner + Create-new-design button, then defers to
<ChatPanel>. PageBuilderPage keeps its complex state setup.
2. Right-rail [Inspector | Blocks] tabs
- frontend/src/components/builder/RightRail.jsx — tabbed wrapper.
Auto-flips to Inspector when something gets selected; user can
manually flip back to Blocks. Inspector tab disabled when nothing
selected (defaults to Blocks).
- frontend/src/components/builder/BlocksPanel.jsx — inline (non-
modal) version of the deleted BlockPalette. Same Built-in vs
Project tabs, 1-up grid sized for the narrow rail.
- EditInspector loses its own <aside> wrapper (RightRail provides
the chrome) — outer becomes a <div className="h-full ...">
- BlockPalette.jsx DELETED — modal was the wrong shape.
- "+ Block" toolbar button DELETED — Blocks tab replaces it.
3. SVG wireframes (htmlstudio v0.4.0)
- BlockDefinition gains optional `wireframe` field (inline SVG).
- 4 hand-drawn wireframes for builtins (hero-split / cta-banner /
feature-grid-3 / testimonial-quote). 160×90, currentColor strokes
so the palette themes them.
- generateWireframe(template) auto-generates from tag structure
(image presence, heading count, paragraph count, CTA presence).
- wireframeFor(def) — convenience: authored or auto.
- BlocksPanel cards render wireframes via dangerouslySetInnerHTML
in a 16:9 aspect container. Emoji thumbnail field kept for back-
compat / fallback.
- 4 new tests; 62/62 passing.
4. "Create new design" button
- Replaces the implicit isFirstBrief guess. Always opens the
DiscoveryForm on a fresh slate via a new forceDiscovery state.
- Button is a brand→purple gradient at the top of the chat sidebar,
hidden in edit mode (banner takes priority) and while a form is
already showing.
- handleDiscoverySubmit/Skip now gracefully handle the "no pending
message — brief IS the input" case.
Plan reference: previous turn's plan-then-execute pass.
Same root cause as the Dockerfile fix in 087fc4f / phase 2: package-lock.json is generated on the Windows host, so it never records the Linux rollup native binary (@rollup/rollup-linux-x64-gnu). `npm ci` is strict and refuses to install anything not in the lockfile → build dies with "Cannot find module @rollup/rollup-linux-x64-gnu". Fix: in both GHA workflows (dev.yml + publish.yml), rm the lockfile + node_modules before installing, and use `npm install` instead of `npm ci`. Lets npm pick the right platform binaries. Tradeoff: lockfile is no longer authoritative for CI builds. Acceptable for now since the only consumer is the frontend bundle and the bundle is determined by package.json semver constraints. If we ever want a fully-pinned CI install, the right answer is to ALSO generate the lockfile on Linux (e.g. via a devcontainer or a one-off `docker run`) and commit that. Refs: npm/cli#4828
Introduce an embeddable AgentSite API and conversation persistence so projects can be generated and resumed in-process.
These changes enable iterative, host-driven workflows (resume, cancel, preview design/review feedback) when embedding AgentSite as a library.