Skip to content

Commit 22832db

Browse files
committed
docs: update
1 parent acd0654 commit 22832db

1 file changed

Lines changed: 1 addition & 356 deletions

File tree

Multi_Agent_Future_Roadmap.md

Lines changed: 1 addition & 356 deletions
Original file line numberDiff line numberDiff line change
@@ -392,353 +392,6 @@ log, result = agent.run_sync("Build an Alzheimer's disease knowledge graph")
392392

393393
---
394394

395-
### Feature 10: Skills System Overhaul (Spec-Driven Loading + Progressive Disclosure)
396-
397-
**Priority:** HIGH -- the current implementation defeats its own purpose and will not scale for multi-agent workflows.
398-
399-
**Depends on:** None (can be built in parallel with other features).
400-
401-
**Current state and problems:**
402-
403-
The skills system exists (Feature "Agent Skills" in Existing Features) but has several design flaws that contradict the intended purpose of skills and will become blockers as multi-agent workflows add more domain-specific skills:
404-
405-
1. **Full skill body always injected into context** (`base_agent.py:774-784`). Every selected skill has its entire `instructions` field dumped into the system prompt via `_SKILL_ENTRY_TEMPLATE`. There is no progressive disclosure -- if 5 skills are selected, all 5 bodies go in at once. The YAML frontmatter (name, description) was designed for lightweight selection, but selection immediately triggers full injection. This directly contradicts the reference pattern from [anthropics/skills](https://github.com/anthropics/skills/blob/main/skills/skill-creator/SKILL.md), which specifies three loading levels:
406-
- Level 1: **Metadata** (name + description) -- always in context (~100 words)
407-
- Level 2: **SKILL.md body** -- loaded when skill triggers (<500 lines ideal)
408-
- Level 3: **Bundled resources** -- loaded as needed (unlimited, scripts execute without loading)
409-
410-
2. **No bundled resources support**. `_parse_skill_file` (`base_agent.py:264-308`) only reads the single SKILL.md file. The reference pattern describes `scripts/`, `references/`, `assets/` directories alongside SKILL.md. A skill like `ontology-mapping` may need reference docs (OWL specs, mapping examples) and template scripts (Cypher export patterns) that should load on demand -- not bloat the system prompt or be absent entirely.
411-
412-
3. **No context budget awareness**. Skills are injected unconditionally into the system prompt. In multi-agent workflows where each specialist agent may need 2-3 domain skills, unbounded injection will collide with Feature 5 (Context Window Management).
413-
414-
4. **Retriever makes all-or-nothing decisions**. At `base_agent.py:1034-1038`, the retriever selects skills based on `{name, description}` -- appropriate for lightweight triage. But selection immediately injects the full body. There is no intermediate step where the agent could inspect a skill summary before committing to loading the full instructions.
415-
416-
5. **`tools` field is purely decorative** (`resources.py:347`). `Skill.tools` is shown in the prompt but never used functionally -- it doesn't filter available tools, validate that referenced tools exist, or influence retriever decisions.
417-
418-
6. **No skill directory tracking**. `source_path` stores the SKILL.md file path, but nothing records the parent directory. Even if bundled resources existed, the system wouldn't know where to find `references/` or `scripts/` relative to the skill.
419-
420-
7. **Loading is wasteful for multi-agent**. `configure()` loads ALL skills from `skills_directory` via glob, then `spec.skill_names` filters via `select_skills_by_names()`. In BaseAgent multi-agent workflows, each agent needs 1-3 skills from a shared pool. Loading all skills just to discard most of them is unnecessary overhead and risks cross-agent state confusion.
421-
422-
**Skill directory convention:**
423-
424-
Skills reside in a conventional directory structure under the project root:
425-
426-
```
427-
skills/ # skills_directory points here
428-
ontology-design/
429-
SKILL.md # required: YAML frontmatter + markdown body
430-
references/ # optional: bundled reference docs
431-
owl_spec.md
432-
scripts/ # optional: bundled template scripts
433-
template.py
434-
database-evaluation/
435-
SKILL.md
436-
parser-development/
437-
SKILL.md
438-
scripts/
439-
csv_template.py
440-
ontology-mapping/
441-
SKILL.md
442-
references/
443-
owl_mapping_guide.md
444-
memgraph-export/
445-
SKILL.md
446-
scripts/
447-
cypher_template.py
448-
```
449-
450-
`AgentSpec.skill_names=["ontology-design"]` resolves to `{skills_directory}/ontology-design/SKILL.md`. Each agent loads **only** the skills specified in its spec -- no glob, no load-all-then-filter.
451-
452-
**Phase 1 -- Model cleanup: remove `trigger`, add `source_dir`**
453-
454-
Remove `trigger` from the `Skill` model and all code that branches on it:
455-
456-
- `resources.py:344` -- remove `trigger` field from `Skill`
457-
- `resource_manager.py:526-532` -- remove `trigger="manual"` guard; all skills follow the same selection logic
458-
- `base_agent.py:1033-1037` -- retriever passes all skills as candidates (no trigger filter)
459-
- `agent_spec.py:42` -- update docstring
460-
- `tests/test_skills.py` -- remove `test_manual_trigger`, update `test_select_skills_by_names_manual_not_deselected`
461-
462-
Add `source_dir: str | None` to the `Skill` model. When loading from a file, set `source_dir` to the SKILL.md's parent directory. Add a `has_bundled_resources` computed property that checks for `references/`, `scripts/`, or `assets/` subdirectories.
463-
464-
```python
465-
# In resources.py, add to Skill:
466-
source_dir: Optional[str] = Field(None, description="Directory containing the SKILL.md and optional bundled resources")
467-
468-
@property
469-
def has_bundled_resources(self) -> bool:
470-
if not self.source_dir:
471-
return False
472-
d = Path(self.source_dir)
473-
return any((d / sub).is_dir() for sub in ("references", "scripts", "assets"))
474-
475-
@property
476-
def bundled_resource_manifest(self) -> dict[str, list[str]]:
477-
"""List files in each bundled resource subdirectory."""
478-
if not self.source_dir:
479-
return {}
480-
manifest = {}
481-
for sub in ("references", "scripts", "assets"):
482-
sub_dir = Path(self.source_dir) / sub
483-
if sub_dir.is_dir():
484-
manifest[sub] = [f.name for f in sorted(sub_dir.iterdir()) if f.is_file()]
485-
return manifest
486-
```
487-
488-
In `_parse_skill_file`, set `source_dir=str(path.parent)`.
489-
490-
**Files to modify (Phase 1):**
491-
- `BaseAgent/resources.py` -- remove `trigger`, add `source_dir` + computed properties
492-
- `BaseAgent/resource_manager.py` -- remove trigger guard in `select_skills_by_names`
493-
- `BaseAgent/base_agent.py` -- `_parse_skill_file` (set `source_dir`), `_select_resources_for_prompt` (remove trigger filter)
494-
- `BaseAgent/agent_spec.py` -- update docstring
495-
- `BaseAgent/tests/test_skills.py` -- remove trigger tests, add `source_dir` / bundled resource tests
496-
497-
**Phase 2 -- Spec-driven targeted skill loading**
498-
499-
Replace the current "load all → filter" loading model with targeted loading. Each agent loads only the skills specified in its `AgentSpec.skill_names` directly from their conventional directory paths.
500-
501-
Add `_resolve_skill_path(skill_name) -> Path | None` to `BaseAgent`:
502-
503-
```python
504-
def _resolve_skill_path(self, skill_name: str) -> Path | None:
505-
"""Resolve a skill name to its SKILL.md path using directory conventions.
506-
507-
Looks up {skills_directory}/{skill_name}/SKILL.md.
508-
Returns None if the file does not exist or skills_directory is not set.
509-
"""
510-
if not self.skills_directory:
511-
return None
512-
candidate = Path(self.skills_directory) / skill_name / "SKILL.md"
513-
return candidate if candidate.is_file() else None
514-
```
515-
516-
Rewrite the skill loading block in `configure()` (`base_agent.py:867-876`):
517-
518-
```python
519-
# Skill loading: targeted when spec provides skill_names, legacy glob otherwise
520-
if self.spec is not None and self.spec.skill_names is not None:
521-
# Targeted: load only the skills named in the agent spec
522-
for skill_name in self.spec.skill_names:
523-
path = self._resolve_skill_path(skill_name)
524-
if path:
525-
skill = self._parse_skill_file(path)
526-
self.resource_manager.add_skill(skill)
527-
else:
528-
print(f"Warning: skill '{skill_name}' not found in '{self.skills_directory}'")
529-
elif self.skills_directory:
530-
# Legacy fallback: load all skills from directory via glob
531-
self.load_skills(self.skills_directory)
532-
533-
# Apply AgentSpec tool filters (skill filtering no longer needed -- we only loaded what we need)
534-
if self.spec is not None:
535-
if self.spec.tool_names is not None:
536-
self.resource_manager.select_tools_by_names(self.spec.tool_names)
537-
```
538-
539-
The `select_skills_by_names()` call is removed from `configure()` -- since we only loaded the skills we need, there is nothing to filter. `load_skills()` and `add_skill()` remain as public API for ad-hoc single-agent usage.
540-
541-
**Files to modify (Phase 2):**
542-
- `BaseAgent/base_agent.py` -- `_resolve_skill_path()` (new), `configure()` (rewrite skill loading block)
543-
544-
**Phase 3 -- Progressive disclosure: metadata-only prompt + retriever-driven loading**
545-
546-
This is the core behavioral change. The system prompt shows only skill metadata at configuration time; the `retrieve` node loads full bodies on demand when the task arrives.
547-
548-
**New config field:**
549-
- `BaseAgentConfig.skill_retrieval: bool = True` (default `True` -- always retrieve skills on demand)
550-
- Env var override: `BASE_AGENT_SKILL_RETRIEVAL`
551-
552-
When `skill_retrieval=True` (default):
553-
- `configure()` / `_generate_system_prompt(is_retrieval=False)`: injects only the **skill catalog** -- name, description, and bundled resource manifest per skill (~2-3 lines each)
554-
- `retrieve` node: always runs skill selection (even if `use_tool_retriever=False`), then regenerates system prompt with full instructions for selected skills
555-
- The agent sees full instructions only for task-relevant skills
556-
557-
When `skill_retrieval=False`:
558-
- Falls back to current behavior: all loaded skills' full instructions injected into the system prompt at configure time
559-
- Provided as an escape hatch for simple single-skill setups
560-
561-
**New prompt templates** in `prompts.py`:
562-
563-
```python
564-
_SKILL_CATALOG_SECTION = \
565-
"""
566-
567-
AVAILABLE SKILLS
568-
===============================
569-
The following skills provide specialized domain knowledge and workflows.
570-
Relevant skill instructions will be loaded based on the current task.
571-
{skill_catalog}
572-
"""
573-
574-
_SKILL_CATALOG_ENTRY = \
575-
"""- {skill_name}: {skill_description}{bundled_note}"""
576-
577-
_SKILL_SELECTION_PROMPT = \
578-
"""Select which skills are relevant for this task.
579-
580-
TASK: {query}
581-
582-
AVAILABLE SKILLS:
583-
{skills}
584-
585-
Respond with: SKILLS: [list of indices]
586-
If none are relevant: SKILLS: []
587-
"""
588-
```
589-
590-
**System prompt changes** in `_generate_system_prompt()` (`base_agent.py:774-784`):
591-
592-
```python
593-
selected_skills = self.resource_manager.get_selected_skills()
594-
if selected_skills:
595-
if self.skill_retrieval and not is_retrieval:
596-
# Catalog mode: metadata only (initial prompt)
597-
catalog_entries = []
598-
for skill in selected_skills:
599-
bundled = ""
600-
if skill.has_bundled_resources:
601-
bundled = " [has bundled resources]"
602-
catalog_entries.append(_SKILL_CATALOG_ENTRY.format(
603-
skill_name=skill.name,
604-
skill_description=skill.description,
605-
bundled_note=bundled,
606-
))
607-
prompt_modifier += _SKILL_CATALOG_SECTION.format(
608-
skill_catalog="\n".join(catalog_entries)
609-
)
610-
else:
611-
# Full injection: either retrieval pass or opt-out
612-
skill_parts = []
613-
for skill in selected_skills:
614-
skill_parts.append(_SKILL_ENTRY_TEMPLATE.format(
615-
skill_name=skill.name,
616-
skill_description=skill.description,
617-
skill_instructions=skill.instructions,
618-
))
619-
prompt_modifier += _SKILLS_SECTION.format(skills_content="\n".join(skill_parts))
620-
```
621-
622-
**New method** `_select_skills_for_prompt(prompt)` in `BaseAgent`:
623-
624-
```python
625-
def _select_skills_for_prompt(self, prompt: str) -> None:
626-
"""Select relevant skills based on task prompt via lightweight LLM call."""
627-
all_skills = [
628-
{"name": s.name, "description": s.description}
629-
for s in self.resource_manager.get_all_skills()
630-
]
631-
if not all_skills:
632-
return
633-
634-
skills_text = "\n".join(
635-
f"{i}. {s['name']}: {s['description']}" for i, s in enumerate(all_skills)
636-
)
637-
selection_prompt = _SKILL_SELECTION_PROMPT.format(query=prompt, skills=skills_text)
638-
response = self.llm.invoke([HumanMessage(content=selection_prompt)])
639-
640-
# Parse SKILLS: [indices] response
641-
match = re.search(r"SKILLS:\s*\[(.*?)\]", response.content, re.IGNORECASE)
642-
if match and match.group(1).strip():
643-
indices = [int(idx.strip()) for idx in match.group(1).split(",") if idx.strip()]
644-
selected_names = [all_skills[i]["name"] for i in indices if i < len(all_skills)]
645-
else:
646-
selected_names = []
647-
648-
self.resource_manager.select_skills_by_names(selected_names)
649-
```
650-
651-
**Retrieve node changes** (`nodes.py:310-332`):
652-
653-
```python
654-
def retrieve(self, state: "AgentState") -> "AgentState":
655-
agent = self.agent
656-
prompt = state["input"][-1].content
657-
658-
# Skill retrieval: always runs when skill_retrieval=True and skills exist
659-
if agent.skill_retrieval and agent.resource_manager.get_all_skills():
660-
agent._select_skills_for_prompt(prompt)
661-
agent.system_prompt = agent._generate_system_prompt(
662-
self_critic=agent.self_critic,
663-
is_retrieval=True,
664-
)
665-
666-
# Tool/data/library retrieval: only when use_tool_retriever=True
667-
if agent.use_tool_retriever:
668-
agent._select_resources_for_prompt(prompt)
669-
agent.system_prompt = agent._generate_system_prompt(
670-
self_critic=agent.self_critic,
671-
is_retrieval=True,
672-
)
673-
674-
return state
675-
```
676-
677-
The retrieve node is no longer a no-op when `use_tool_retriever=False` -- it always processes skill retrieval when `skill_retrieval=True` and there are registered skills.
678-
679-
**Files to modify (Phase 3):**
680-
- `BaseAgent/config.py` -- `skill_retrieval: bool = True` field + env var override
681-
- `BaseAgent/prompts.py` -- `_SKILL_CATALOG_SECTION`, `_SKILL_CATALOG_ENTRY`, `_SKILL_SELECTION_PROMPT` (new templates)
682-
- `BaseAgent/base_agent.py` -- `_generate_system_prompt()` (catalog vs. full injection), `_select_skills_for_prompt()` (new method), `configure()` (store `self.skill_retrieval`)
683-
- `BaseAgent/nodes.py` -- `retrieve` node: skill retrieval always runs independently of `use_tool_retriever`
684-
- `BaseAgent/tests/test_skills.py` -- progressive disclosure tests (catalog in initial prompt, full body after retrieval)
685-
686-
**Phase 4 -- Bundled resource access via REPL**
687-
688-
Register a built-in helper function `read_skill_resource(skill_name, path)` that reads files from a skill's bundled resource directory. This lets the agent load reference docs and templates on demand from within `<execute>` blocks:
689-
690-
```python
691-
# Injected into REPL namespace when skills with bundled resources are present
692-
def read_skill_resource(skill_name: str, path: str, _agent=None) -> str:
693-
"""Read a bundled resource file from a skill directory.
694-
695-
Args:
696-
skill_name: Name of the skill (e.g. "ontology-mapping")
697-
path: Relative path within the skill directory (e.g. "references/owl_spec.md")
698-
"""
699-
if _agent is None:
700-
raise RuntimeError("read_skill_resource requires an agent context")
701-
skill = _agent.resource_manager.get_skill_by_name(skill_name)
702-
if not skill or not skill.source_dir:
703-
raise FileNotFoundError(f"Skill '{skill_name}' not found or has no source directory")
704-
full_path = Path(skill.source_dir) / path
705-
if not full_path.is_file():
706-
raise FileNotFoundError(f"Resource '{path}' not found in skill '{skill_name}'")
707-
return full_path.read_text(encoding="utf-8")
708-
```
709-
710-
Injected into the per-instance REPL namespace via `functools.partial(_agent=self)` in `_inject_custom_functions_to_repl()` when any loaded skill has bundled resources.
711-
712-
The skill instructions would then say things like:
713-
```markdown
714-
When you need the OWL mapping reference, load it:
715-
read_skill_resource("ontology-mapping", "references/owl_mapping_guide.md")
716-
```
717-
718-
**Files to modify (Phase 4):**
719-
- `BaseAgent/tools/support_tools.py` -- `read_skill_resource` helper function
720-
- `BaseAgent/tools/tool_description/support_tools.py` -- tool description for `read_skill_resource`
721-
- `BaseAgent/base_agent.py` -- inject into REPL namespace in `_inject_custom_functions_to_repl()`
722-
- `BaseAgent/tests/test_skills.py` -- bundled resource access tests
723-
724-
**Phase 5 -- Functional `tools` field**
725-
726-
Make the `Skill.tools` field actionable:
727-
- At `configure()` time, validate that all tools listed in `skill.tools` actually exist in `ResourceManager`. Emit a warning for missing tools.
728-
- When a skill is selected by the retriever in `_select_skills_for_prompt()`, auto-select its referenced tools (even if the retriever didn't explicitly pick them). This ensures skills always have their required tools available.
729-
730-
**Files to modify (Phase 5):**
731-
- `BaseAgent/base_agent.py` -- `configure()` (tool validation), `_select_skills_for_prompt()` (auto-select skill tools)
732-
- `BaseAgent/tests/test_skills.py` -- tool validation and auto-selection tests
733-
734-
**Backwards compatibility:**
735-
- `spec=None` + `skills_directory` set: legacy `load_skills()` glob behavior preserved
736-
- `skill_retrieval=False`: restores current all-in body injection behavior
737-
- `add_skill()` and `load_skills()` remain as public API for ad-hoc usage
738-
- Default (`skill_retrieval=True`) changes the prompt behavior but is strictly better -- agents see the same skill instructions, just loaded on demand rather than always present
739-
740-
---
741-
742395
## Post-Prototype Feature Specifications
743396

744397
Deferred until the prototype is validated. Listed here to inform design decisions -- do not build hooks or abstractions for these unless they are zero-cost.
@@ -817,14 +470,6 @@ New files to be created by future features:
817470

818471
```
819472
Depends On Effort
820-
== GROUP A (parallel, no dependencies) =============================================
821-
Feature 10 Skills system overhaul -- ~3 days
822-
Phase 1 Model cleanup (remove trigger, add source_dir) ~0.5 day
823-
Phase 2 Spec-driven targeted skill loading ~0.5 day
824-
Phase 3 Progressive disclosure in prompt injection ~1 day
825-
Phase 4 Bundled resource access via REPL ~0.5 day
826-
Phase 5 Functional tools field ~0.5 day
827-
828473
== GROUP B (after Group A completes) ===============================================
829474
Feature 5 Context window management -- ~2 days
830475
Phase 1 Sliding window for single-agent
@@ -854,7 +499,7 @@ Feature 9 Workflow orchestration 8 ~3 days
854499
Phase 3 BaseAgent multi-agent demo script
855500
```
856501

857-
**Critical path:** `[1, 2, 3, 4, 10 parallel] -> [5, 6 parallel] -> 7 -> 8 -> 9`
502+
**Critical path:** `[1, 2, 3, 4, 10 ] -> [5, 6 parallel] -> 7 -> 8 -> 9`
858503

859504
**Minimum viable prototype:** Features 1-6 + 8 + 10 Phase 1-3 (supervisor orchestrator, spec-driven skills with progressive disclosure)
860505

0 commit comments

Comments
 (0)