soothe-plugins 0.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,177 @@
1
+ """AgentGenerator -- manifest and system prompt generation (RFC-0005) -- community edition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from datetime import UTC, datetime
7
+ from typing import TYPE_CHECKING
8
+
9
+ if TYPE_CHECKING:
10
+ from pathlib import Path
11
+
12
+ from langchain_core.language_models import BaseChatModel
13
+
14
+ import anyio
15
+
16
+ from .models import AgentBlueprint, AgentManifest
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ _SYSTEM_PROMPT_GENERATION = """\
21
+ You are generating a system prompt for a specialist AI agent.
22
+
23
+ Agent name: {agent_name}
24
+ Agent description: {description}
25
+
26
+ The agent must fulfil this objective:
27
+ {objective}
28
+
29
+ Expected input: {expected_input}
30
+ Expected output: {expected_output}
31
+ Constraints: {constraints}
32
+
33
+ The agent has access to these skills (integrated instructions):
34
+ {skills_text}
35
+
36
+ {bridge_instructions}
37
+
38
+ The agent has these tools available: {tools}
39
+
40
+ Generate a comprehensive system prompt for this agent. The prompt should:
41
+ 1. Define the agent's role and responsibilities clearly
42
+ 2. Incorporate the skill instructions naturally (do not reference them as "skills")
43
+ 3. Include the bridge instructions as part of the workflow
44
+ 4. Specify the expected input/output format
45
+ 5. Include any constraints as operational rules
46
+
47
+ Output ONLY the system prompt text (markdown format). Do not include meta-commentary."""
48
+
49
+
50
+ class AgentGenerator:
51
+ """Generates agent packages (manifest + system prompt) from blueprints.
52
+
53
+ Args:
54
+ model: Chat model for system prompt generation.
55
+ """
56
+
57
+ def __init__(self, model: BaseChatModel) -> None:
58
+ """Initialize the agent generator.
59
+
60
+ Args:
61
+ model: Chat model for system prompt generation.
62
+ """
63
+ self._model = model
64
+
65
+ async def generate(
66
+ self,
67
+ blueprint: AgentBlueprint,
68
+ output_dir: Path,
69
+ ) -> AgentManifest:
70
+ """Generate an agent package from a blueprint.
71
+
72
+ Creates the output directory with ``manifest.yml``, ``system_prompt.md``,
73
+ and a ``skills/`` subdirectory with copied skill files.
74
+
75
+ Args:
76
+ blueprint: Complete agent specification.
77
+ output_dir: Directory to write the agent package into.
78
+
79
+ Returns:
80
+ The generated ``AgentManifest``.
81
+ """
82
+ adir = anyio.Path(output_dir)
83
+ await adir.mkdir(parents=True, exist_ok=True)
84
+
85
+ skills_dir = output_dir / "skills"
86
+ await anyio.Path(skills_dir).mkdir(exist_ok=True)
87
+ copied_skills = self._copy_skills(blueprint, skills_dir)
88
+
89
+ system_prompt = await self._generate_system_prompt(blueprint)
90
+
91
+ prompt_path = output_dir / "system_prompt.md"
92
+ await anyio.Path(prompt_path).write_text(system_prompt, encoding="utf-8")
93
+
94
+ manifest = AgentManifest(
95
+ name=blueprint.agent_name,
96
+ description=blueprint.capability.description,
97
+ skills=copied_skills,
98
+ tools=blueprint.tools,
99
+ capabilities=blueprint.capability.required_capabilities,
100
+ created_at=datetime.now(UTC),
101
+ )
102
+
103
+ manifest_path = output_dir / "manifest.yml"
104
+ self._write_manifest(manifest, manifest_path)
105
+
106
+ logger.info("Generated agent '%s' at %s", blueprint.agent_name, output_dir)
107
+ return manifest
108
+
109
+ async def _generate_system_prompt(self, blueprint: AgentBlueprint) -> str:
110
+ """Use LLM to craft a system prompt from the blueprint."""
111
+ skills_text = (
112
+ "\n\n".join(f"### {sid}\n{content[:2000]}" for sid, content in blueprint.harmonized.skill_contents.items())
113
+ or "(no specific skills)"
114
+ )
115
+
116
+ bridge = ""
117
+ if blueprint.harmonized.bridge_instructions:
118
+ bridge = f"Additional integration instructions:\n{blueprint.harmonized.bridge_instructions}"
119
+
120
+ constraints = ", ".join(blueprint.capability.constraints) or "none"
121
+ tools = ", ".join(blueprint.tools) or "standard file and shell tools"
122
+
123
+ prompt = _SYSTEM_PROMPT_GENERATION.format(
124
+ agent_name=blueprint.agent_name,
125
+ description=blueprint.capability.description,
126
+ objective=blueprint.capability.description,
127
+ expected_input=blueprint.capability.expected_input or "user request",
128
+ expected_output=blueprint.capability.expected_output or "task result",
129
+ constraints=constraints,
130
+ skills_text=skills_text,
131
+ bridge_instructions=bridge,
132
+ tools=tools,
133
+ )
134
+
135
+ try:
136
+ resp = await self._model.ainvoke([{"role": "user", "content": prompt}])
137
+ return str(resp.content).strip()
138
+ except Exception:
139
+ logger.exception("System prompt generation failed")
140
+ return self._fallback_prompt(blueprint)
141
+
142
+ @staticmethod
143
+ def _fallback_prompt(blueprint: AgentBlueprint) -> str:
144
+ """Generate a minimal system prompt when LLM call fails."""
145
+ return (
146
+ f"You are {blueprint.agent_name}, a specialist agent.\n\n"
147
+ f"## Objective\n{blueprint.capability.description}\n\n"
148
+ f"## Expected Input\n{blueprint.capability.expected_input}\n\n"
149
+ f"## Expected Output\n{blueprint.capability.expected_output}\n"
150
+ )
151
+
152
+ @staticmethod
153
+ def _copy_skills(blueprint: AgentBlueprint, skills_dir: Path) -> list[str]:
154
+ """Copy skill files into the agent's skills directory."""
155
+ copied: list[str] = []
156
+ for sid, content in blueprint.harmonized.skill_contents.items():
157
+ skill_subdir = skills_dir / sid
158
+ skill_subdir.mkdir(exist_ok=True)
159
+ (skill_subdir / "SKILL.md").write_text(content, encoding="utf-8")
160
+ copied.append(str(skill_subdir))
161
+ return copied
162
+
163
+ @staticmethod
164
+ def _write_manifest(manifest: AgentManifest, path: Path) -> None:
165
+ """Write the manifest as YAML."""
166
+ try:
167
+ import yaml
168
+
169
+ data = manifest.model_dump(mode="json")
170
+ if "created_at" in data and hasattr(data["created_at"], "isoformat"):
171
+ data["created_at"] = data["created_at"].isoformat()
172
+ path.write_text(yaml.dump(data, default_flow_style=False, sort_keys=False), encoding="utf-8")
173
+ except ImportError:
174
+ import json
175
+
176
+ data = manifest.model_dump(mode="json")
177
+ path.with_suffix(".json").write_text(json.dumps(data, indent=2, default=str), encoding="utf-8")
@@ -0,0 +1,136 @@
1
+ """Weaver data models (RFC-0005) -- community edition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import UTC, datetime
6
+ from typing import Literal
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class CapabilitySignature(BaseModel):
12
+ """Structured analysis of what the user request requires.
13
+
14
+ Args:
15
+ description: One-paragraph summary of what the agent should do.
16
+ required_capabilities: Capability keywords (e.g. ``arxiv_search``).
17
+ constraints: Operational limits or requirements.
18
+ expected_input: What the agent receives from the user.
19
+ expected_output: What the agent should produce.
20
+ """
21
+
22
+ description: str
23
+ required_capabilities: list[str] = Field(default_factory=list)
24
+ constraints: list[str] = Field(default_factory=list)
25
+ expected_input: str = ""
26
+ expected_output: str = ""
27
+
28
+
29
+ class AgentManifest(BaseModel):
30
+ """Metadata file for a generated agent package.
31
+
32
+ Args:
33
+ name: Agent identifier (lowercase, hyphenated).
34
+ description: Human-readable description for the ``task`` tool.
35
+ type: Agent type (currently always ``subagent``).
36
+ system_prompt_file: Filename of the system prompt markdown.
37
+ skills: Skill paths copied into the package.
38
+ tools: Langchain tool group names enabled for this agent.
39
+ capabilities: Capability keywords this agent provides.
40
+ created_at: Creation timestamp.
41
+ version: Monotonically increasing version number.
42
+ """
43
+
44
+ name: str
45
+ description: str
46
+ type: Literal["subagent"] = "subagent"
47
+ system_prompt_file: str = "system_prompt.md"
48
+ skills: list[str] = Field(default_factory=list)
49
+ tools: list[str] = Field(default_factory=list)
50
+ capabilities: list[str] = Field(default_factory=list)
51
+ created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
52
+ version: int = 1
53
+
54
+
55
+ class ReuseCandidate(BaseModel):
56
+ """A previously generated agent that may fulfill the current request.
57
+
58
+ Args:
59
+ manifest: The agent's manifest metadata.
60
+ confidence: Semantic similarity score in [0, 1].
61
+ path: Absolute path to the agent directory.
62
+ """
63
+
64
+ manifest: AgentManifest
65
+ confidence: float
66
+ path: str
67
+
68
+
69
+ class SkillConflict(BaseModel):
70
+ """A detected conflict between two skills.
71
+
72
+ Args:
73
+ skill_a_id: First skill identifier.
74
+ skill_b_id: Second skill identifier.
75
+ conflict_type: Nature of the conflict.
76
+ description: Human-readable conflict description.
77
+ severity: Impact level.
78
+ resolution: LLM-proposed resolution strategy.
79
+ """
80
+
81
+ skill_a_id: str
82
+ skill_b_id: str
83
+ conflict_type: Literal["contradictory", "ambiguous", "version_mismatch"]
84
+ description: str
85
+ severity: Literal["low", "medium", "high"]
86
+ resolution: str
87
+
88
+
89
+ class SkillConflictReport(BaseModel):
90
+ """Full analysis of conflicts, overlaps, and gaps in a candidate skill set.
91
+
92
+ Args:
93
+ conflicts: Typed conflict entries between skill pairs.
94
+ overlaps: Pairs of skill IDs with redundant coverage.
95
+ gaps: Missing capabilities identified by analysis.
96
+ harmonization_summary: Human-readable summary of the analysis.
97
+ """
98
+
99
+ conflicts: list[SkillConflict] = Field(default_factory=list)
100
+ overlaps: list[list[str]] = Field(default_factory=list)
101
+ gaps: list[str] = Field(default_factory=list)
102
+ harmonization_summary: str = ""
103
+
104
+
105
+ class HarmonizedSkillSet(BaseModel):
106
+ """Output of skill harmonization -- a clean, unified instruction corpus.
107
+
108
+ Args:
109
+ skills: Final skill IDs retained after harmonization.
110
+ skill_contents: Mapping of skill_id to merged/cleaned content.
111
+ bridge_instructions: LLM-generated glue logic connecting skills.
112
+ dropped_skills: Skill IDs removed during deduplication.
113
+ merge_log: Human-readable log of merge decisions.
114
+ """
115
+
116
+ skills: list[str] = Field(default_factory=list)
117
+ skill_contents: dict[str, str] = Field(default_factory=dict)
118
+ bridge_instructions: str = ""
119
+ dropped_skills: list[str] = Field(default_factory=list)
120
+ merge_log: list[str] = Field(default_factory=list)
121
+
122
+
123
+ class AgentBlueprint(BaseModel):
124
+ """Complete specification for agent generation.
125
+
126
+ Args:
127
+ capability: The analysed capability signature.
128
+ harmonized: The harmonized skill set.
129
+ tools: Resolved tool group names.
130
+ agent_name: Generated agent name (lowercase, hyphenated).
131
+ """
132
+
133
+ capability: CapabilitySignature
134
+ harmonized: HarmonizedSkillSet
135
+ tools: list[str] = Field(default_factory=list)
136
+ agent_name: str = ""
@@ -0,0 +1,214 @@
1
+ """GeneratedAgentRegistry -- filesystem-based agent registry (RFC-0005) -- community edition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from pathlib import Path
7
+ from typing import TYPE_CHECKING
8
+
9
+ from .models import AgentManifest
10
+
11
+ if TYPE_CHECKING:
12
+ from deepagents.middleware.subagents import SubAgent
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class GeneratedAgentRegistry:
18
+ """Manages generated agent packages under a base directory.
19
+
20
+ Each agent is a subdirectory containing ``manifest.yml`` (or
21
+ ``manifest.json``) and ``system_prompt.md``.
22
+
23
+ Args:
24
+ base_dir: Root directory for generated agents
25
+ (e.g. ``~/.soothe/generated_agents``).
26
+ """
27
+
28
+ def __init__(self, base_dir: Path) -> None:
29
+ """Initialize the generated agent registry.
30
+
31
+ Args:
32
+ base_dir: Root directory for generated agents
33
+ (e.g. ``~/.soothe/generated_agents``).
34
+ """
35
+ self._base_dir = Path(base_dir).expanduser().resolve()
36
+
37
+ @property
38
+ def base_dir(self) -> Path:
39
+ """The base directory for generated agents."""
40
+ return self._base_dir
41
+
42
+ def list_agents(self) -> list[AgentManifest]:
43
+ """Scan base_dir for ``*/manifest.yml`` and return parsed manifests.
44
+
45
+ Returns:
46
+ List of valid ``AgentManifest`` objects found.
47
+ """
48
+ manifests: list[AgentManifest] = []
49
+ if not self._base_dir.is_dir():
50
+ return manifests
51
+
52
+ for manifest_path in self._base_dir.glob("*/manifest.yml"):
53
+ try:
54
+ manifest = self._load_manifest(manifest_path)
55
+ if manifest:
56
+ manifests.append(manifest)
57
+ except Exception:
58
+ logger.warning("Failed to load manifest %s", manifest_path, exc_info=True)
59
+
60
+ for manifest_path in self._base_dir.glob("*/manifest.json"):
61
+ try:
62
+ manifest = self._load_manifest_json(manifest_path)
63
+ if manifest:
64
+ manifests.append(manifest)
65
+ except Exception:
66
+ logger.warning("Failed to load manifest %s", manifest_path, exc_info=True)
67
+
68
+ return manifests
69
+
70
+ def get_agent(self, name: str) -> tuple[AgentManifest, Path] | None:
71
+ """Get a specific agent by name.
72
+
73
+ Args:
74
+ name: Agent name to look up.
75
+
76
+ Returns:
77
+ Tuple of (manifest, agent_dir) or ``None``.
78
+ """
79
+ agent_dir = self._base_dir / name
80
+ if not agent_dir.is_dir():
81
+ return None
82
+
83
+ for ext in ("yml", "json"):
84
+ manifest_path = agent_dir / f"manifest.{ext}"
85
+ if manifest_path.is_file():
86
+ loader = self._load_manifest if ext == "yml" else self._load_manifest_json
87
+ manifest = loader(manifest_path)
88
+ if manifest:
89
+ return manifest, agent_dir
90
+ return None
91
+
92
+ def register(self, manifest: AgentManifest, path: Path) -> None:
93
+ """Record a new agent entry (idempotent).
94
+
95
+ Args:
96
+ manifest: The agent manifest.
97
+ path: Absolute path to the agent directory.
98
+ """
99
+ logger.info("Registered generated agent '%s' at %s", manifest.name, path)
100
+
101
+ def load_as_subagent(self, name: str) -> "SubAgent" | None:
102
+ """Load a generated agent as a deepagents SubAgent dict.
103
+
104
+ Args:
105
+ name: Agent name to load.
106
+
107
+ Returns:
108
+ A ``SubAgent`` dict or ``None`` if not found.
109
+ """
110
+ result = self.get_agent(name)
111
+ if result is None:
112
+ return None
113
+
114
+ manifest, agent_dir = result
115
+ prompt_path = agent_dir / manifest.system_prompt_file
116
+ system_prompt = ""
117
+ if prompt_path.is_file():
118
+ try:
119
+ system_prompt = prompt_path.read_text(encoding="utf-8")
120
+ except Exception:
121
+ logger.warning("Failed to read system prompt for '%s'", name, exc_info=True)
122
+
123
+ return {
124
+ "name": manifest.name,
125
+ "description": manifest.description,
126
+ "system_prompt": system_prompt,
127
+ }
128
+
129
+ def cleanup_old_agents(self, max_age_days: int = 30, max_agents: int = 100) -> int:
130
+ """Remove old/unused generated agents.
131
+
132
+ Args:
133
+ max_age_days: Delete agents not accessed for N days.
134
+ max_agents: Keep at most N agents (delete oldest first).
135
+
136
+ Returns:
137
+ Number of agents deleted.
138
+ """
139
+ import shutil
140
+ from datetime import UTC, datetime, timedelta
141
+
142
+ deleted = 0
143
+
144
+ if not self._base_dir.is_dir():
145
+ return deleted
146
+
147
+ # Get all agents with their access times
148
+ agents: list[tuple[Path, datetime]] = []
149
+ try:
150
+ for manifest_file in self._base_dir.glob("*/manifest.yml"):
151
+ try:
152
+ stat = manifest_file.stat()
153
+ atime = datetime.fromtimestamp(stat.st_atime, tz=UTC)
154
+ agents.append((manifest_file.parent, atime))
155
+ except Exception:
156
+ logger.debug("Failed to stat %s", manifest_file, exc_info=True)
157
+ continue
158
+
159
+ # Sort by access time (oldest first)
160
+ agents.sort(key=lambda x: x[1])
161
+
162
+ # Delete by age
163
+ cutoff = datetime.now(UTC) - timedelta(days=max_age_days)
164
+ for agent_dir, atime in agents:
165
+ if atime < cutoff:
166
+ try:
167
+ shutil.rmtree(agent_dir)
168
+ deleted += 1
169
+ logger.info("Deleted old generated agent: %s", agent_dir.name)
170
+ except Exception:
171
+ logger.warning("Failed to delete agent %s", agent_dir, exc_info=True)
172
+
173
+ # Delete by count (if still over limit)
174
+ remaining = [(d, t) for d, t in agents if t >= cutoff]
175
+ if len(remaining) > max_agents:
176
+ for agent_dir, _ in remaining[:-max_agents]:
177
+ try:
178
+ shutil.rmtree(agent_dir)
179
+ deleted += 1
180
+ logger.info("Deleted excess generated agent: %s", agent_dir.name)
181
+ except Exception:
182
+ logger.warning("Failed to delete agent %s", agent_dir, exc_info=True)
183
+ except Exception:
184
+ logger.warning("Agent cleanup failed", exc_info=True)
185
+
186
+ return deleted
187
+
188
+ @staticmethod
189
+ def _load_manifest(path: Path) -> AgentManifest | None:
190
+ """Load a YAML manifest file."""
191
+ try:
192
+ import yaml
193
+
194
+ data = yaml.safe_load(path.read_text(encoding="utf-8"))
195
+ if not isinstance(data, dict):
196
+ return None
197
+ return AgentManifest(**data)
198
+ except Exception:
199
+ logger.debug("Failed to parse YAML manifest %s", path, exc_info=True)
200
+ return None
201
+
202
+ @staticmethod
203
+ def _load_manifest_json(path: Path) -> AgentManifest | None:
204
+ """Load a JSON manifest file."""
205
+ import json
206
+
207
+ try:
208
+ data = json.loads(path.read_text(encoding="utf-8"))
209
+ if not isinstance(data, dict):
210
+ return None
211
+ return AgentManifest(**data)
212
+ except Exception:
213
+ logger.debug("Failed to parse JSON manifest %s", path, exc_info=True)
214
+ return None
@@ -0,0 +1,151 @@
1
+ """ReuseIndex -- vector search over previously generated agents (RFC-0005) -- community edition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from .models import AgentManifest, CapabilitySignature, ReuseCandidate
9
+
10
+ if TYPE_CHECKING:
11
+ from langchain_core.embeddings import Embeddings
12
+
13
+ from soothe_sdk.protocols import VectorStoreProtocol
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class ReuseIndex:
19
+ """Semantic index of previously generated agents for reuse-first strategy.
20
+
21
+ Args:
22
+ vector_store: Vector store for agent description embeddings.
23
+ embeddings: Embedding model for vectorization.
24
+ threshold: Minimum confidence score to consider a reuse hit.
25
+ collection: Vector store collection name.
26
+ embedding_dims: Embedding vector dimensionality.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ vector_store: "VectorStoreProtocol",
32
+ embeddings: Embeddings,
33
+ threshold: float = 0.85,
34
+ collection: str = "soothe_weaver_reuse",
35
+ embedding_dims: int = 1536,
36
+ ) -> None:
37
+ """Initialize the reuse index.
38
+
39
+ Args:
40
+ vector_store: Vector store for agent description embeddings.
41
+ embeddings: Embedding model for vectorization.
42
+ threshold: Minimum confidence score to consider a reuse hit.
43
+ collection: Vector store collection name.
44
+ embedding_dims: Embedding vector dimensionality.
45
+ """
46
+ self._vector_store = vector_store
47
+ self._embeddings = embeddings
48
+ self._threshold = threshold
49
+ self._collection = collection
50
+ self._embedding_dims = embedding_dims
51
+ self._initialized = False
52
+
53
+ async def _ensure_collection(self) -> None:
54
+ if self._initialized:
55
+ return
56
+ try:
57
+ await self._vector_store.create_collection(
58
+ vector_size=self._embedding_dims,
59
+ distance="cosine",
60
+ )
61
+ except Exception:
62
+ logger.debug("Reuse collection creation failed (may already exist)", exc_info=True)
63
+ self._initialized = True
64
+
65
+ async def find_reusable(self, capability: CapabilitySignature) -> ReuseCandidate | None:
66
+ """Search for an existing generated agent matching the capability.
67
+
68
+ Args:
69
+ capability: The analysed capability signature.
70
+
71
+ Returns:
72
+ A ``ReuseCandidate`` if confidence >= threshold, else ``None``.
73
+ """
74
+ await self._ensure_collection()
75
+
76
+ try:
77
+ vector = await self._embeddings.aembed_query(capability.description)
78
+ except Exception:
79
+ logger.exception("Failed to embed capability description")
80
+ return None
81
+
82
+ try:
83
+ results = await self._vector_store.search(
84
+ query=capability.description,
85
+ vector=vector,
86
+ limit=5,
87
+ )
88
+ except Exception:
89
+ logger.exception("Reuse index search failed")
90
+ return None
91
+
92
+ if not results:
93
+ return None
94
+
95
+ best = results[0]
96
+ confidence = best.score or 0.0
97
+ if confidence < self._threshold:
98
+ return None
99
+
100
+ payload = best.payload
101
+ try:
102
+ manifest = AgentManifest(**payload.get("manifest", {}))
103
+ except Exception:
104
+ logger.warning("Failed to parse reuse candidate manifest", exc_info=True)
105
+ return None
106
+
107
+ return ReuseCandidate(
108
+ manifest=manifest,
109
+ confidence=confidence,
110
+ path=payload.get("path", ""),
111
+ )
112
+
113
+ async def index_agent(self, manifest: AgentManifest, path: str) -> None:
114
+ """Add a newly generated agent to the reuse index.
115
+
116
+ Args:
117
+ manifest: The agent's manifest.
118
+ path: Absolute path to the agent directory.
119
+ """
120
+ await self._ensure_collection()
121
+
122
+ text = f"{manifest.name}: {manifest.description}"
123
+ if manifest.capabilities:
124
+ text += f"\nCapabilities: {', '.join(manifest.capabilities)}"
125
+
126
+ try:
127
+ vector = await self._embeddings.aembed_query(text)
128
+ except Exception:
129
+ logger.exception("Failed to embed agent description for reuse index")
130
+ return
131
+
132
+ payload: dict[str, Any] = {
133
+ "manifest": manifest.model_dump(mode="json"),
134
+ "path": path,
135
+ "agent_name": manifest.name,
136
+ }
137
+
138
+ try:
139
+ await self._vector_store.insert(
140
+ vectors=[vector],
141
+ payloads=[payload],
142
+ ids=[manifest.name],
143
+ )
144
+ except Exception:
145
+ logger.exception("Failed to index agent in reuse store")
146
+
147
+ async def close(self) -> None:
148
+ """Close the underlying vector store if supported."""
149
+ close_method = getattr(self._vector_store, "close", None)
150
+ if callable(close_method):
151
+ await close_method()