Skip to content

feat(prompts): move backend + HTML-dashboard contracts into built-in skills#244

Open
SailingSF wants to merge 3 commits into
stagingfrom
feat/prompt-skills-slim
Open

feat(prompts): move backend + HTML-dashboard contracts into built-in skills#244
SailingSF wants to merge 3 commits into
stagingfrom
feat/prompt-skills-slim

Conversation

@SailingSF

Copy link
Copy Markdown
Contributor

The 15.3k-char BACKEND_GENERATION_PROMPT and 10.7k-char VISUALIZATIONS_HTML_OUTPUT_FORMAT_PROMPT were re-sent in the system prompt on every LLM call. They now ship as read-only built-in skills (build-fullstack-backend, build-html-dashboard) served by SkillStore and are recalled on demand; the always-sent prompt carries short mandatory recall hints instead. create_artifact/launch_backend descriptions reinforce the recall. System prompt drops from ~43.5k to ~22.6k chars.

…skills

The 15.3k-char BACKEND_GENERATION_PROMPT and 10.7k-char
VISUALIZATIONS_HTML_OUTPUT_FORMAT_PROMPT were re-sent in the system prompt on
every LLM call. They now ship as read-only built-in skills
(build-fullstack-backend, build-html-dashboard) served by SkillStore and are
recalled on demand; the always-sent prompt carries short mandatory recall
hints instead. create_artifact/launch_backend descriptions reinforce the
recall. System prompt drops from ~43.5k to ~22.6k chars.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@SailingSF
SailingSF requested a review from alecantu7 July 10, 2026 00:06

@alecantu7 alecantu7 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — REQUEST CHANGES (adversarially verified, cross-checked with cowork-server#185)

The direction is right and most of the engineering is careful — verified before writing anything else: the prompt content moved into the SKILL.md files word-for-word verbatim; built-ins ship in the wheel and load live from the installed package (auto-refresh on upgrade, no copy step); cowork-server sessions get them (its harness sets skills_root and anton's ChatSession builds the SkillStore, which post-PR always merges the builtin root); recall labels are drift-tested; and there's no merge-order coupling with cowork-server#185. The system-prompt savings are real today (anton sends the system prompt uncached, full price, on every drip-send call).

The blocking findings are all about what happens when the recall bet loses — details inline. Summary:

  1. The now-mandatory contract can be silently shadowed or become permanently unrecallable (skills.py — no builtin fallback behind a broken same-label user dir; pre-existing same-label skills win silently; in cowork the per-project skills dir is agent-writable).
  2. build-fullstack-backend step 5 references a prompt section this same PR deleted, and inlines a drift-prone summary instead of recalling the dashboard skill.
  3. The contract moved from compaction-immune to compactable with no recall idempotence — mid-build compaction can evict it; duplicate recalls append duplicate ~2.8k-token bodies re-sent full-price every subsequent call.
  4. The simple-chart path regressed: ~80 tokens of inline guidance became a mandatory ~2.8k-token recall + one extra full-context round trip (break-even ≈ 29 calls).

One design suggestion that resolves 1, 3, and 4 together: make delivery deterministic instead of prose-gated — handle_create_artifact receives the artifact type from a closed enum, so on the first html-app/fullstack-* create of a session it can append the matching builtin skill body to the tool result itself (guaranteed, once, ~5KB; unaffected by shadowing, compaction, or the model skipping the hint). Same enforce-in-code-not-prompt principle as the ENG-350 fix. launch_backend linting the cloud-only contract items (Mangum handler, /api/*, requirements.txt) would back the skill's "WILL fail deployment" claim in code too.

Hygiene (non-blocking): uv.lock carries an unrelated rich <15 resync (repo practice puts lock resyncs in their own commits); skills.py:98's provenance comment still says "manual" | "consolidator" while this PR adds "builtin"; the module docstring + docs/docs/developer/skills-internals.md still claim skills live only under ~/.anton/skills/; list_summaries()/list_all() grew duplicate builtin-merge blocks a shared helper would collapse.

🤖 Generated with Claude Code

d = check_migrate(d, self.root)
if d is not None:
return self._skill_from_dir(d)
return self._load_builtin(label)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: the mandatory contract can be shadowed or become unrecallable, with no fallback.

Two failure modes, both new because the contract no longer exists anywhere else:

  1. Broken shadow dead-ends recall. If a user dir with the same label exists but its SKILL.md is unparseable (_skill_from_dir → None), load() returns None here without falling back to the builtin — while list_summaries() still advertises the label from the builtin root, so closest_match resolves to the same label and fails again. Result: "closest candidate 'build-html-dashboard' could not be loaded", permanently, and the prompt contains none of the contract anymore.
  2. Silent replacement. A pre-existing user/consolidator skill named build-html-dashboard (plausible auto-generated slug from before this PR — make_unique_label only guards new creations) shadows the builtin with zero signal, and the prompt still asserts the label carries the mandatory contract. In cowork-server the per-project skills root is agent-writable (snapshot_stray_skills exists because agents create folders there), so a stray dir can swap out the backend contract in the flagship product.

Suggested: in load(), fall back to _load_builtin(label) when the user-dir parse fails; and when a user skill shadows a builtin label, either log it or annotate the recall result ("note: this overrides a built-in skill") so the swap is at least visible.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0af6687: load() now falls back to the built-in when a same-label user dir exists but is unreadable (broken shadow can no longer dead-end the label — closest_match/list_summaries and load agree again), and a user skill shadowing a built-in label is logged. Covered by TestBrokenShadowFallback in tests/test_builtin_skills.py.

5. BUILD FRONTEND (if needed): In a separate scratchpad:
- Build a single-file HTML dashboard or web interface
- Include all CSS and JS inlined (no external file references)
- Apply the HTML build guidance from the `VISUALIZATIONS` section above (single self-contained HTML file; Apache ECharts via CDN for charts; dark theme #0d1117; responsive layout with a viewport meta tag). If that section is not present in this prompt, follow these same defaults regardless.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this references a prompt section this same PR deleted. "the VISUALIZATIONS section above" no longer exists — the PR reduced it to a recall pointer — so this instruction dead-ends, and the parenthetical 3-item summary becomes the only HTML guidance the fullstack path gets: the full contract (hidden-tab chart init renders blank, Python→JS string escaping, resize/viewport handling, large-dataset rules) is lost for fullstack frontends, and the two skills now drift independently.

Fix: replace with MANDATORY: call recall_skill("build-html-dashboard") for the frontend and follow its contract — one source of truth, same recall mechanism this PR establishes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0af6687 — step 5 now says: MANDATORY recall_skill("build-html-dashboard") for the frontend, single source of truth; the 3-item inline summary remains only as an explicit fallback for when recall is unavailable. No more reference to the deleted VISUALIZATIONS section.

Comment thread anton/core/llm/prompts.py
Never split CSS or chart logic into separate files — only large data payloads.\
MANDATORY: call `recall_skill("build-html-dashboard")` BEFORE writing the HTML \
and follow the loaded output contract (charting library, theme, file layout, \
large-dataset handling). Recalling it too often is fine; skipping it is not.\

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two related economics problems with the mandatory recall, worth a deliberate decision:

  1. This path regressed. The markdown-output section used to carry ~80 tokens of inline chart guidance; it now mandates recalling the full ~2.8k-token skill plus one extra full-context round trip before the first HTML write (anton drip-sends the whole history, uncached — no cache_control exists in the repo). For a one-chart ask late in a 50k-token conversation, that's ~50k input tokens of pure overhead; break-even needs ~29 subsequent calls. Net across all traffic the PR still wins, but simple chart asks now cost more than before the PR.
  2. "Recalling it too often is fine" — but handle_recall_skill has no per-session idempotence, so each repeat appends another full ~2.8k-token body to history, re-sent full-price on every later call. A one-line "already recalled this conversation" stub response makes the encouraged behavior cheap.

Also worth noting: the contract now lives in compactable conversation history (the old system-prompt block was compaction-immune) — a long build that crosses the context-pressure threshold can summarize the recalled contract away mid-task. The create_artifact auto-inject suggested in the review body addresses all three at once by making delivery deterministic and re-injectable.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially taken in 0af6687. Implemented: recall idempotence — every recall payload now carries a stable marker ('# Skill recalled: <label>'); a repeat recall returns a ~40-token stub while that marker is still visible in history, and re-sends the full body if compaction evicted it (so the compaction concern is self-healing rather than silent: the launch_backend/create_artifact descriptions prompt a re-recall, and the guard won't stub it). Tests in TestRecallIdempotence.

On the economics framing: we A/B'd exactly this on real tasks before the PR went up (eval run r-2026-07-09-6421fa, 5 tasks x 2 variants, same model/gateway): this branch came out 22% cheaper overall ($5.42 vs $6.92), the recall fired only on the task that built a dashboard, and the pairwise judge scored the dashboard task a tie. The simple-chart-late-in-a-long-conversation case is real but is the tail, not the body, of the distribution — and maintainer decision (Max) is that prompt-gated recall is acceptable; we're not adding deterministic injection in this PR.

"with a `metadata.json` + `README.md` written automatically.\n\n"
"SKILL PREREQUISITE: `html-app` and the two fullstack types have "
"detailed output contracts stored as skills. If you haven't already "
"done so this conversation, call `recall_skill(\"build-html-dashboard\")` "

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Altitude suggestion (non-blocking but high-leverage): everything in this PR rides on the model choosing to obey these hints — there's no code enforcement anywhere (handle_create_artifact validates type against a closed enum but never checks/injects the contract; handle_launch_backend does no linting). Since create_artifact deterministically knows the artifact type, the first html-app/fullstack-* create of a session could append the matching builtin skill body to the tool result itself — guaranteed delivery, once per session, immune to shadowing/compaction/hint-skipping, and it makes the mandatory-recall prose advisory instead of load-bearing. Same enforce-in-code-not-prompt principle as the ENG-350 fix.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged but deliberately not taken: maintainer decision (Max) is that non-deterministic, prompt-gated delivery is acceptable here, and the A/B eval (r-2026-07-09-6421fa) showed the recall firing exactly where needed with no quality regression (judge tie on the dashboard task, artifact produced on both variants). The downside cases are now bounded by 0af6687 (broken-shadow fallback + shadow logging + idempotent recall). If real traffic shows missed recalls breaking launches, create_artifact auto-inject is the agreed follow-up — filing it as future work rather than expanding this PR.

…e source of truth for HTML contract

- load() falls back to the built-in when a same-label user dir exists but
  is unreadable (broken shadow no longer dead-ends a mandatory contract),
  and logs when a user skill shadows a built-in label.
- recall_skill embeds a stable marker in its payload; repeat recalls
  return a short stub while the body is still visible in history, and
  re-send the full procedure if compaction evicted it.
- build-fullstack-backend step 5 no longer references the deleted
  VISUALIZATIONS prompt section — it recalls build-html-dashboard, the
  single source of truth for dashboard HTML (inline defaults only as
  fallback).
- Hygiene: revert unrelated uv.lock resync; provenance comment/docstring/
  developer docs mention built-ins; list_all/list_summaries share one
  _iter_skill_dirs walk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@alecantu7 alecantu7 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up review — CHANGES REQUESTED (one new finding; the other three are resolved)

Verified the addressed commit 0af66879 against my prior review:

  • Shadow/fallback (skills.py): resolved — load() now falls back to _load_builtin() on an unparseable same-label user dir and logs a shadow; _iter_skill_dirs collapses the duplicated merge. ✅
  • build-fullstack-backend step 5: resolved — MANDATORY recall_skill("build-html-dashboard") as single source of truth, inline defaults only as fallback, no more reference to the deleted VISUALIZATIONS section. ✅
  • Economics tail + create_artifact auto-inject: reasonably declined with the A/B data (r-2026-07-09-6421fa) and filed as follow-up. Fine as a maintainer call. ✅

One new blocking finding, introduced by the idempotence fix itself — see the inline comment on recall_skill.py. In short: the stub reproduces the exact detection marker, so after compaction evicts the full body but keeps a surviving stub, recall returns stubs forever and never re-sends the contract — re-creating the compaction failure this fix was meant to heal.

🤖 Generated with Claude Code

Comment thread anton/core/tools/recall_skill.py Outdated
import json as _json

return any(
marker in (m if isinstance(m, str) else _json.dumps(m, default=str))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking (new, introduced by this fix): the stub embeds the same marker used for detection, so the compaction self-healing doesn't hold.

_recall_marker(label) returns `# Skill recalled: <label>`. The full body embeds it (line 72), and _already_in_history detects a prior recall by matching that string (line 94). But the stub below (lines 147–149) reproduces the exact same marker string in its own text ("look for '# Skill recalled: ...'"). That makes the stub a self-sustaining signal:

  1. Recall Handle malformed user skills and avoid persisting failed builds #1 → full body (contains marker).
  2. Recall anton #2 → stub (stub also contains the marker).
  3. Compaction evicts the large, old full body but keeps the smaller, newer stub.
  4. Recall Added the Python Client for Anton #3_already_in_history matches the stub's marker → returns another stub. The full contract is never re-sent.

That's exactly the compaction failure the idempotence work was meant to heal, re-emerging through the fix. test_compacted_history_resends_full_body doesn't catch it because it simulates a summary that lacks the marker; it never exercises a surviving stub that carries it.

Fix: detect on a substring only the full body has — e.g. the ## Procedure (Stage 1 — declarative) header (line 78) — instead of a marker the stub reproduces. (Or have the stub describe the banner without emitting the literal marker string.) Add a test where the full body in history is replaced by a stub and assert the next recall re-sends the full procedure.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c5db078 — exactly as suggested: detection now requires the marker AND the '## Procedure (Stage 1 — declarative)' header in the same message (only the full payload carries both; ensure_ascii=False so the em-dash header matches through json serialization), and the stub deliberately contains neither string. Added both regression tests: a surviving stub triggers a full re-send, and a compaction summary quoting the marker doesn't count as the contract being present.

The idempotence stub embedded the same marker _already_in_history
matches on, so a stub surviving compaction (while the full body was
evicted) suppressed re-sends forever. Detection now requires the marker
AND the procedure header in the same message (only the full payload has
both, ensure_ascii=False so the em-dash header actually matches), and
the stub carries neither. Regression tests: surviving stub and
marker-quoting summary both trigger a full re-send.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants