Spaces:
Sleeping
Skillbook v2 β Design & Execution Plan
Status: approved. This document is the frozen design for the skillbook refactor.
Vision
- Issues are the primary object. A skill's core content is a prose description of the problem it addresses, with scope expressed inline.
- Insights are mandatory for context skills, optional for harness. Context skills must carry the imperative action the agent should follow β that's the whole point. Harness skills may be pure issue catalogs (a problem in the runtime environment, no agent-side workaround available yet); if a harness workaround does exist, it goes in
insight. - Fine-grained categories stay structured. The old specific category / topic labels are preserved as
keywords; they do not get collapsed into free text and they are not replaced by the binarysection. - Scope is emergent. New issues start narrow; the SkillManager widens the scope text recursively as it sees the same issue recur across domains/traces. No separate scope field β widening = rewriting the
issueprose. - Skillbook search can go hybrid. BM25 + dense, fused via RRF. Flat structure retained.
- Dashboard-first mindset. Schema + provenance must support issue dashboards, occurrence heatmaps, effectiveness KPIs.
Final Skill schema
@dataclass
class Skill:
id: str
section: Literal["context", "harness"] # pipeline-facing split only
keywords: List[str] # fine-grained category/topic labels (required, normalized)
issue: str # prose problem + scope inline β required
insight: Optional[str] # imperative action β required for context, optional for harness
occurrences: List[InsightSource] # append-only audit chain; auto-appended on every mutation
active: bool = True
used_count: int = 0
helpful_count: int = 0
harmful_count: int = 0
neutral_count: int = 0
embedding: Optional[List[float]] = None # stored in sidecar .npz, not JSON
created_at: str
updated_at: str
Dropped fields: content, justification, evidence.
Rename: sources β occurrences.
Section semantics: section is no longer the old free-form category field. It is only the binary split context|harness. Fine-grained categorization now lives in keywords.
Invariants enforced in Skillbook.add_skill / update_skill:
section β {"context", "harness"}β reject otherwise.keywordsrequired, non-empty, normalized by stripping / lowercasing / de-duping while preserving order.issuealways required, non-empty.insightrequired + non-empty whensection="context"; may beNoneor empty whensection="harness".- Any mutation invalidates
embedding(set toNoneso it recomputes on next retrieval).
Storage β split embeddings
skillbook.jsonβ diffable.Skillentries never carryembedding.skillbook.embeddings.npzβnumpy.savez_compressed, keyed byskill_id, float32. Cache only. Can be deleted and recomputed lazily.save_to_file(path)writes both.load_from_file(path)loads JSON; loads.npzif present (silent no-op otherwise).- Schema version check on load: JSON must contain
"schema_version": "2". Missing/mismatched βraise ValueError("Skillbook format v2 required β regenerate"). Hard break confirmed.
Tool surface β atomic, no micro-tools
Full signatures. issue required on every mutation; keywords required on add and optional on update (omit to keep current); insight required when section="context", optional when section="harness":
add_skill(section, issue, keywords, insight=None) -> {ok, skill_id} # insight required iff section="context"
update_skill(skill_id, issue, keywords=None, insight=None) -> {ok} # omit keywords / insight to keep current values
tag_skill(skill_id, delta) -> {ok} # delta β {-1, 0, 1}
remove_skill(skill_id, reason) -> {ok} # SOFT default β sets active=False, keeps history
search_skills(query, top_k=5, section=None, keywords=None) -> [...]
read_skill(skill_id) -> {id, section, keywords, issue, insight, counters, active, occurrences}
add_skill / update_skill auto-append an InsightSource from the current trace. Every mutation is recorded in occurrences. tag_skill auto-appends an observation entry too.
No widen_scope / codify_insight / add_occurrence micro-tools β all mutations go through the atomic update_skill to prevent partial/stale states.
Provenance wiring (closes current gap)
Problem today: Skill.sources is always [] because SM tools never thread insight_source=. Grepped to confirm.
Fix:
UpdateStep.__call__builds anInsightSourcefromctx.trace(trace_uid,source_system,trace_id,sample_question) +ctx.epoch+reflections[0].error_identification+reflections[0].key_insight.SkillManager.update_skills(..., source: InsightSource)β new required kwarg.SMDeps.current_source: InsightSourceβ available to every tool.add_skill/update_skill/tag_skilltools derive per-opInsightSource(copying identity, settingoperation_type, appending op-specificerror_identification/learning_text), and passinsight_source=through to the underlyingSkillbookmethod.
Result: skill.occurrences populates naturally. Dashboard has data.
Embedding input formula
parts = [issue]
if insight is not None:
parts.append(insight)
if keywords:
parts.append(f"Keywords: {', '.join(keywords)}")
embedding_input = "\n\n".join(parts)
So search / dedup consumers can match on problem text, action text, and structured category labels. Invalidate on any mutation of issue, insight, or keywords.
Prompt rendering
Skillbook.as_prompt() remains a compatibility / helper surface. It should render only skills where active=True, grouped by section, using the new issue / insight fields:
## context
- [context-00007]
Keywords: airline, booking_api, cabin_class
Issue: In tau-airline's update_reservation_flights API, cabin class is a single param applied to all legs/passengers β no per-leg or per-passenger differentiation.
Insight: Before offering per-passenger or per-leg upgrades, immediately tell the user that cabin class is all-or-nothing, then present only all-or-nothing options.
## harness
- [harness-00003]
Keywords: tau2, rate_limit, retries
Issue: tau2 runner retries Bedrock 429 with 60s exponential backoff, blocking the whole pipeline. Observed in airline + retail runs.
This phase does not decide rollout retrieval or prompt-injection policy. as_prompt() is kept as a generic rendering helper for debugging, exports, and backward-compatible callers.
SM prompt rewrite (ace/implementations/prompts.py)
- Declare the two-section taxonomy and the insight-required-iff-context invariant.
- Declare the distinction between binary
section(context|harness) and fine-grainedkeywords. - Require
issueon every ADD/UPDATE; requireinsightonly whensection="context". - Require non-empty
keywordson ADD. Guide: 1-5 short stable labels such as domain, subsystem, API family, or behavior category. - Guide: write
issueas problem + applicability inline (start narrow β single domain / single API / single endpoint). - Recursive widening rule: if
search_skillsreturns a semantically overlapping issue from another domain, callupdate_skillwith a broaderissuethat covers both contexts, rather than creating a new skill. - When broadening or merging a skill, update
keywordstoo: keep useful existing labels, add genuinely new ones, and drop stale labels that no longer fit. - Duplicate-avoidance: always
search_skillsbeforeadd_skill. - Soft-delete semantics:
remove_skillfor skills that are harmful or outdated; audit chain is preserved andactive=Falseskills are excluded from normal active-skill views.
Skillbook search / retrieval (tooling only)
File: ace/implementations/skill_rendering.py.
retrieve_top_k(skillbook, query, *, top_k=5, section=None, keywords=None):
- Optional
sectionpre-filter (skillbook._sectionsalready indexed). - Optional
keywordsfilter / boost againstskill.keywords. - BM25 rank over
issue + insight + keywordstext (lexical). - Dense cosine rank over embeddings. Query embedding failure β
raise(already done). - Reciprocal Rank Fusion (k=60). Return top-k.
Add dep: rank-bm25 (MIT, ~50 LOC wrapping). No infra.
This section applies to search_skills / inspection flows only. Agent-side retrieval and prompt-injection policy are explicitly deferred.
Files to touch
Core (CLAUDE.md-gated β user pre-approved):
ace/core/skillbook.pyβSkillrewrite,UpdateOperationrewrite (drop content/justification/evidence fields, add keywords/issue/insight),add_skill/update_skill/remove_skill(defaultsoft=True),to_dict/from_dictwithschema_version="2"check, sidecar save/load,as_prompt()field rendering update,to_llm_dict,_apply_operation.ace/core/insight_source.pyβ no change (fields already sufficient).
Integration:
ace/deduplication/detector.pyβ embedding input changed toissue + insight + keywords. Line 178 needs update (s.contentβ new formula). Invalidation hook on update (skill.embedding = None).ace/deduplication/prompts.pyβ referencingskill_a.content(lines 54, 56, 113, 114) β update toissue+keywordscontext.ace/deduplication/operations.pyβ referencing.contentwrites (lines 113, 153) β update to.issue/.insight.ace/implementations/sm_tools.pyβ rewrite all tool signatures (add_skill,update_skill,tag_skill,remove_skill,search_skills,read_skill). Threadctx.deps.current_sourceinto every mutation.ace/implementations/skill_manager.pyβ acceptsource: InsightSourceonupdate_skills, store onSMDeps.current_source.ace/implementations/prompts.pyβ full SM prompt rewrite.ace/implementations/rr/tools.pyβread_skillreturn dict (line 89) β new fields.search_skillbookreturn (line 122) β new fields.ace/implementations/skill_rendering.pyβrender_skills_xml(line 47) β new fields + hybrid BM25+RRF +section/keywordssupport.ace/implementations/helpers.pyβ line 59 rendersskill.contentβ swap to issue/insight.ace/steps/update.pyβ buildInsightSourcefromctx.traceand pass toSM.update_skills(source=...).ace/steps/export_markdown.pyβ lines 44, 46-47, 49-50 reference old fields β update.
Tests / examples:
tests/β fixtures will break on load (different field names); update.examples/β skim for any.content/.justification/.evidencereads.
New dep: rank-bm25.
Smoke test
Run after changes land:
uv run ace-eval e2e \
--benchmark tau-bench-airline \
--traces results/e2e/run_784b73163157/collection \
--agent-model bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0 \
--reflector-model bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0 \
--skill-manager-model bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0 \
--user-model bedrock/openai.gpt-oss-120b-1:0 \
--reflector-type rr \
--num-trials 1 --max-num-steps 50 --max-workers 1 \
--no-benchmark --logfire --verbose
Verify:
- No crashes.
skillbook.jsonconforms to v2 shape.skillbook.embeddings.npzcreated.- Every skill has
len(occurrences) >= 1. - Every skill has
len(keywords) >= 1. - Context skills have non-null
insight; harness skills may or may not. - Logfire shows
sm.sessionβadd_skill/update_skillwith atomic signatures.
Out of scope
- Dashboard UI β data shape is sufficient; build later.
- Agent-side retrieval / prompt injection policy β defer to a later plan; this document does not decide whether skills are fetched by a pre-step, tool calls, or some other rollout path.
- Cross-encoder reranker β not worth it below ~2000 skills.
- Multi-vector embeddings (content + use-case split) β not needed; single concat works.
- Hierarchical taxonomy β explicitly rejected. Structured flat
keywordsare sufficient. - Query expansion / HyDE β defer until retrieval misses observed in production.
- SQLite migration β JSON+sidecar is right for <5K skills.
Open decisions
UpdateOperationaudit-log fields: dropcontent/justification/evidence, addissue/insight. Keep structure identical otherwise.- Section validation: add a module-level constant
VALID_SECTIONS = frozenset({"context", "harness"})and validate inadd_skill+update_skill(viasection=lookup from existing skill). - Keyword normalization: store
keywordsas short lowercase identifiers; de-dupe while preserving order. _generate_idprefix: stays assection.split()[0].lower()β yieldscontext-00001/harness-00001naturally.- Hard purge: expose
Skillbook.purge(skill_id)as a module-level method NOT wired to any SM tool. Human-operator / CLI only.
Already done in this branch
-
ace/implementations/skill_rendering.py:96-101βretrieve_top_kraises on embedding failure (no silent fallback). -
ace/core/recursive_agent.pyβspan_labelthreaded throughrun_agent_with_compactionandRecursiveAgent; SkillManager emitssm.sessionspans distinct from RR'srr.session. -
ace/implementations/rr/config.pyβcache_prompts/cache_ttladded toRecursiveConfig(was previously anAttributeError). -
ace-eval/src/ace_eval/e2e/training.pyβ_train_sequentialsurfacesSampleResult.errorinstead of silently swallowing. -
ace/implementations/skill_manager.pyβ passesspan_label="sm"to superclass.