semora-coding 0.2.0__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,424 @@
1
+ """Source-neutral skill discovery and progressive disclosure.
2
+
3
+ Sources list lightweight metadata and load full instructions only when the ``skill`` tool is
4
+ invoked. Filesystem discovery is one adapter, not a registry responsibility.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import builtins
11
+ import os
12
+ import re
13
+ from collections.abc import Mapping, Sequence
14
+ from dataclasses import dataclass
15
+ from html import escape
16
+ from inspect import isawaitable
17
+ from pathlib import Path
18
+ from typing import Any, Protocol, runtime_checkable
19
+
20
+ from semora.contracts import BaseMessage, DynamicTools, Tools
21
+
22
+ __all__ = [
23
+ "DirectorySkillSource",
24
+ "Skill",
25
+ "SkillMetadata",
26
+ "SkillRegistry",
27
+ "SkillSource",
28
+ "SkillTools",
29
+ ]
30
+
31
+ _NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:-]*$")
32
+ _DEFAULT_CATALOG_BUDGET = 8_000
33
+ _DESCRIPTION_LIMIT = 250
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class SkillMetadata:
38
+ """The model-visible part of a skill returned by source discovery."""
39
+
40
+ name: str
41
+ description: str
42
+ revision: str | None = None
43
+
44
+
45
+ @dataclass(frozen=True, slots=True)
46
+ class Skill:
47
+ """Full instructions loaded on demand from an arbitrary source."""
48
+
49
+ name: str
50
+ description: str
51
+ body: str
52
+ origin: str | None = None
53
+ resource_base: str | None = None
54
+ allowed_tools: tuple[str, ...] = ()
55
+ paths: tuple[str, ...] = ()
56
+
57
+ def context(self, arguments: str = "") -> str:
58
+ """Render the full on-demand context injected after the tool result."""
59
+ body = self.body
60
+ prefix = ""
61
+ if self.resource_base is not None:
62
+ body = body.replace("${SEMORA_SKILL_ROOT}", self.resource_base)
63
+ body = body.replace("${SEMORA_SKILL_DIR}", self.resource_base)
64
+ prefix = f"Resource base for this skill: {self.resource_base}\n\n"
65
+ body = body.replace("${ARGUMENTS}", arguments).replace("$ARGUMENTS", arguments)
66
+ argument_block = f"\n\nArguments: {arguments}" if arguments else ""
67
+ return f"{prefix}{body}{argument_block}"
68
+
69
+
70
+ @runtime_checkable
71
+ class SkillSource(Protocol):
72
+ """Metadata-first store for directory, database, API, or package-backed skills."""
73
+
74
+ async def list(self) -> Sequence[SkillMetadata]:
75
+ """List discovery metadata without loading complete skill bodies."""
76
+ ...
77
+
78
+ async def load(self, name: str) -> Skill | None:
79
+ """Load one exact skill body, or return ``None`` if it disappeared."""
80
+ ...
81
+
82
+
83
+ class DirectorySkillSource:
84
+ """Read directory-form ``SKILL.md`` files through the generic source contract."""
85
+
86
+ def __init__(self, root: str | Path) -> None:
87
+ """Resolve the root once; source callers remain responsible for user expansion."""
88
+ self.root = Path(root).resolve()
89
+ self._locations: dict[str, Path] | None = None
90
+
91
+ async def list(self) -> Sequence[SkillMetadata]:
92
+ """Read frontmatter only; instruction bodies remain unread until ``load``."""
93
+ metadata, locations = await asyncio.to_thread(self._discover)
94
+ self._locations = locations
95
+ return metadata
96
+
97
+ async def load(self, name: str) -> Skill | None:
98
+ """Read and parse one previously discovered skill file."""
99
+ if self._locations is None:
100
+ await self.list()
101
+ assert self._locations is not None
102
+ path = self._locations.get(name)
103
+ if path is None:
104
+ return None
105
+ try:
106
+ return await asyncio.to_thread(_parse_skill, path)
107
+ except (OSError, ValueError):
108
+ return None
109
+
110
+ def _discover(self) -> tuple[tuple[SkillMetadata, ...], dict[str, Path]]:
111
+ found: dict[str, SkillMetadata] = {}
112
+ locations: dict[str, Path] = {}
113
+ if not self.root.is_dir():
114
+ return (), locations
115
+ for root, directories, files in os.walk(self.root, followlinks=False):
116
+ directories[:] = sorted(
117
+ directory for directory in directories if not (Path(root) / directory).is_symlink()
118
+ )
119
+ if "SKILL.md" not in files:
120
+ continue
121
+ path = Path(root) / "SKILL.md"
122
+ if path.is_symlink():
123
+ continue
124
+ try:
125
+ metadata = _read_metadata(path)
126
+ except (OSError, ValueError):
127
+ continue
128
+ found[metadata.name] = metadata
129
+ locations[metadata.name] = path.resolve()
130
+ return tuple(found[name] for name in sorted(found)), locations
131
+
132
+
133
+ class SkillRegistry:
134
+ """Merge ordered metadata sources; a later source overrides an earlier one by name."""
135
+
136
+ def __init__(
137
+ self,
138
+ sources: Sequence[SkillSource | str | Path],
139
+ *,
140
+ catalog_char_budget: int = _DEFAULT_CATALOG_BUDGET,
141
+ ) -> None:
142
+ """Configure source precedence and the maximum model-visible catalog size."""
143
+ if catalog_char_budget < 256:
144
+ raise ValueError("skill catalog budget must be at least 256 characters")
145
+ self._sources = tuple(_source(source) for source in sources)
146
+ self._catalog_char_budget = catalog_char_budget
147
+ self._metadata: dict[str, SkillMetadata] | None = None
148
+ self._owners: dict[str, SkillSource] = {}
149
+
150
+ async def refresh(self) -> tuple[SkillMetadata, ...]:
151
+ """Refresh metadata from every source without loading any instruction body."""
152
+ found: dict[str, SkillMetadata] = {}
153
+ owners: dict[str, SkillSource] = {}
154
+ for source in self._sources:
155
+ for metadata in await source.list():
156
+ _validate_metadata(metadata)
157
+ found[metadata.name] = metadata
158
+ owners[metadata.name] = source
159
+ self._metadata = found
160
+ self._owners = owners
161
+ return self.snapshot()
162
+
163
+ def clear(self) -> None:
164
+ """Forget discovery metadata so the next access refreshes every source."""
165
+ self._metadata = None
166
+ self._owners = {}
167
+
168
+ async def list(self) -> tuple[SkillMetadata, ...]:
169
+ """Return deterministic discovery metadata, refreshing once when needed."""
170
+ if self._metadata is None:
171
+ await self.refresh()
172
+ return self.snapshot()
173
+
174
+ def snapshot(self) -> tuple[SkillMetadata, ...]:
175
+ """Return already-fetched metadata without performing source I/O."""
176
+ if self._metadata is None:
177
+ return ()
178
+ return tuple(self._metadata[name] for name in sorted(self._metadata))
179
+
180
+ async def load(self, name: str) -> Skill | None:
181
+ """Load a full skill from the source that won metadata precedence."""
182
+ await self.list()
183
+ owner = self._owners.get(name)
184
+ if owner is None:
185
+ return None
186
+ skill = await owner.load(name)
187
+ if skill is None:
188
+ return None
189
+ if skill.name != name:
190
+ raise ValueError(f"skill source returned {skill.name!r} for {name!r}")
191
+ return skill
192
+
193
+ async def catalog(self) -> str:
194
+ """Fetch metadata and build a bounded discovery-only catalog."""
195
+ await self.list()
196
+ return self.catalog_snapshot()
197
+
198
+ def catalog_snapshot(self) -> str:
199
+ """Build a catalog from fetched metadata without source I/O."""
200
+ skills = self.snapshot()
201
+ if not skills:
202
+ return ""
203
+ head = "<available_skills>\n"
204
+ tail = "\n</available_skills>"
205
+ entries = [
206
+ f" - {escape(skill.name)}: {escape(skill.description[:_DESCRIPTION_LIMIT])}"
207
+ for skill in skills
208
+ ]
209
+ full = head + "\n".join(entries) + tail
210
+ if len(full) <= self._catalog_char_budget:
211
+ return full
212
+
213
+ names = [f" - {escape(skill.name)}" for skill in skills]
214
+ kept: list[str] = []
215
+ for entry in names:
216
+ remaining = len(skills) - len(kept) - 1
217
+ suffix = f"\n ... {remaining} more" if remaining > 0 else ""
218
+ candidate = head + "\n".join([*kept, entry]) + suffix + tail
219
+ if len(candidate) > self._catalog_char_budget:
220
+ break
221
+ kept.append(entry)
222
+ omitted = len(skills) - len(kept)
223
+ suffix = f"\n ... {omitted} more" if omitted else ""
224
+ return head + "\n".join(kept) + suffix + tail
225
+
226
+ class SkillTools:
227
+ """Add an on-demand ``skill`` tool to another tool collection."""
228
+
229
+ def __init__(self, inner: Tools, registry: SkillRegistry) -> None:
230
+ """Compose the registry over an existing tool collection."""
231
+ if any(definition.get("name") == "skill" for definition in inner.list()):
232
+ raise ValueError("the wrapped tool collection already defines 'skill'")
233
+ self._inner = inner
234
+ self._registry = registry
235
+
236
+ async def execute(self, name: str, call_id: str, arguments: Any) -> dict[str, Any]:
237
+ """Load a skill or delegate an ordinary tool call."""
238
+ if name != "skill":
239
+ return await self._inner.execute(name, call_id, arguments)
240
+ if not isinstance(arguments, Mapping) or not isinstance(arguments.get("skill"), str):
241
+ return {"type": "error", "message": "skill requires a string 'skill' argument"}
242
+ skill_name = str(arguments["skill"]).removeprefix("/").strip()
243
+ skill = await self._registry.load(skill_name)
244
+ if skill is None:
245
+ return {"type": "error", "message": f"unknown skill: {skill_name}"}
246
+ raw_args = arguments.get("args", "")
247
+ if not isinstance(raw_args, str):
248
+ return {"type": "error", "message": "skill 'args' must be a string"}
249
+ metadata: dict[str, Any] = {
250
+ "kind": "skill",
251
+ "name": skill.name,
252
+ "allowed_tools": list(skill.allowed_tools),
253
+ }
254
+ if skill.origin is not None:
255
+ metadata["origin"] = skill.origin
256
+ return {
257
+ "type": "text",
258
+ "text": f"Loaded skill {skill.name}.",
259
+ "context_messages": [
260
+ {
261
+ "content": skill.context(raw_args),
262
+ "metadata": metadata,
263
+ }
264
+ ],
265
+ }
266
+
267
+ def get(self, name: str) -> dict[str, Any] | None:
268
+ """Return one currently available tool definition."""
269
+ if name == "skill":
270
+ return self._definition()
271
+ return self._inner.get(name)
272
+
273
+ def list(self) -> list[dict[str, Any]]:
274
+ """Expose ordinary tools plus the fetched skill discovery schema."""
275
+ return [*self._inner.list(), self._definition()]
276
+
277
+ async def prepare(self, messages: builtins.list[BaseMessage]) -> None:
278
+ """Fetch skill metadata and forward dynamic exposure reconstruction."""
279
+ await self._registry.list()
280
+ if isinstance(self._inner, DynamicTools):
281
+ prepared = self._inner.prepare(messages)
282
+ if isawaitable(prepared):
283
+ await prepared
284
+
285
+ def get_context(self) -> Any:
286
+ """Forward the workspace context when the wrapped collection supports it."""
287
+ get_context = getattr(self._inner, "get_context", None)
288
+ if get_context is None:
289
+ raise TypeError("wrapped tools do not expose a workspace context")
290
+ return get_context()
291
+
292
+ def with_context(self, context: Any) -> SkillTools:
293
+ """Rebind the wrapped collection without rebuilding the skill registry."""
294
+ with_context = getattr(self._inner, "with_context", None)
295
+ if with_context is None:
296
+ raise TypeError("wrapped tools cannot be rebound to a workspace context")
297
+ return SkillTools(with_context(context), self._registry)
298
+
299
+ def _definition(self) -> dict[str, Any]:
300
+ catalog = self._registry.catalog_snapshot()
301
+ return {
302
+ "name": "skill",
303
+ "description": (
304
+ "Load a matching skill before doing the task. The tool injects the full "
305
+ "instructions into the next model round; do not guess unlisted names.\n\n"
306
+ f"{catalog}"
307
+ ),
308
+ "parameters": {
309
+ "type": "object",
310
+ "properties": {
311
+ "skill": {"type": "string", "description": "Exact skill name"},
312
+ "args": {"type": "string", "description": "Optional skill arguments"},
313
+ },
314
+ "required": ["skill"],
315
+ "additionalProperties": False,
316
+ },
317
+ "is_exclusive": True,
318
+ }
319
+
320
+
321
+ def _source(source: SkillSource | str | Path) -> SkillSource:
322
+ if isinstance(source, str | Path):
323
+ return DirectorySkillSource(source)
324
+ return source
325
+
326
+
327
+ def _validate_metadata(metadata: SkillMetadata) -> None:
328
+ if not _NAME.fullmatch(metadata.name):
329
+ raise ValueError(f"invalid skill name: {metadata.name!r}")
330
+
331
+
332
+ def _read_metadata(path: Path) -> SkillMetadata:
333
+ lines: list[str] = []
334
+ with path.open(encoding="utf-8") as stream:
335
+ first = stream.readline()
336
+ if first.strip() != "---":
337
+ raise ValueError("SKILL.md has no YAML frontmatter")
338
+ for line in stream:
339
+ if line.strip() == "---":
340
+ break
341
+ lines.append(line.rstrip("\n"))
342
+ else:
343
+ raise ValueError("SKILL.md frontmatter is not closed")
344
+ frontmatter = _frontmatter_values(lines)
345
+ name = str(frontmatter.get("name") or path.parent.name).strip()
346
+ if not _NAME.fullmatch(name):
347
+ raise ValueError(f"invalid skill name: {name!r}")
348
+ return SkillMetadata(
349
+ name=name,
350
+ description=str(frontmatter.get("description") or "").strip(),
351
+ revision=str(path.stat().st_mtime_ns),
352
+ )
353
+
354
+
355
+ def _parse_skill(path: Path) -> Skill:
356
+ content = path.read_text(encoding="utf-8")
357
+ frontmatter, body = _frontmatter(content)
358
+ name = str(frontmatter.get("name") or path.parent.name).strip()
359
+ if not _NAME.fullmatch(name):
360
+ raise ValueError(f"invalid skill name: {name!r}")
361
+ description = str(frontmatter.get("description") or "").strip()
362
+ return Skill(
363
+ name=name,
364
+ description=description,
365
+ body=body.strip(),
366
+ origin=path.resolve().as_uri(),
367
+ resource_base=str(path.resolve().parent),
368
+ allowed_tools=_string_tuple(frontmatter.get("allowed-tools")),
369
+ paths=_string_tuple(frontmatter.get("paths")),
370
+ )
371
+
372
+
373
+ def _frontmatter(content: str) -> tuple[dict[str, Any], str]:
374
+ lines = content.splitlines()
375
+ if not lines or lines[0].strip() != "---":
376
+ raise ValueError("SKILL.md has no YAML frontmatter")
377
+ try:
378
+ end = next(index for index, line in enumerate(lines[1:], 1) if line.strip() == "---")
379
+ except StopIteration as error:
380
+ raise ValueError("SKILL.md frontmatter is not closed") from error
381
+ return _frontmatter_values(lines[1:end]), "\n".join(lines[end + 1 :])
382
+
383
+
384
+ def _frontmatter_values(lines: Sequence[str]) -> dict[str, Any]:
385
+ values: dict[str, Any] = {}
386
+ current_list: str | None = None
387
+ for line in lines:
388
+ stripped = line.strip()
389
+ if current_list is not None and stripped.startswith("- "):
390
+ value = values[current_list]
391
+ assert isinstance(value, list)
392
+ value.append(_scalar(stripped[2:]))
393
+ continue
394
+ if ":" not in line:
395
+ current_list = None
396
+ continue
397
+ key, raw = line.split(":", 1)
398
+ key = key.strip()
399
+ raw = raw.strip()
400
+ if not raw:
401
+ values[key] = []
402
+ current_list = key
403
+ continue
404
+ current_list = None
405
+ values[key] = _scalar(raw)
406
+ return values
407
+
408
+
409
+ def _scalar(value: str) -> Any:
410
+ value = value.strip().strip("\"'")
411
+ if value.startswith("[") and value.endswith("]"):
412
+ inner = value[1:-1].strip()
413
+ return [] if not inner else [_scalar(item) for item in inner.split(",")]
414
+ if value == "true":
415
+ return True
416
+ if value == "false":
417
+ return False
418
+ return value
419
+
420
+
421
+ def _string_tuple(value: Any) -> tuple[str, ...]:
422
+ if not isinstance(value, list):
423
+ return ()
424
+ return tuple(str(item) for item in value)
@@ -0,0 +1,224 @@
1
+ """Provider-neutral deferred tool exposure.
2
+
3
+ The model initially receives complete schemas only for ordinary tools and ``tool_search``. Deferred
4
+ tools are announced by name in that tool's description; selecting/searching one records a compact
5
+ activation marker in the transcript, from which a fresh wrapper can reconstruct the active set.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import builtins
11
+ import json
12
+ import re
13
+ from collections.abc import Mapping
14
+ from inspect import isawaitable
15
+ from typing import Any
16
+
17
+ from langchain_core.messages import BaseMessage, ToolMessage
18
+ from semora.contracts import DynamicTools, Tools
19
+
20
+ __all__ = ["DeferredTools"]
21
+
22
+ _TOOL_NAME = "tool_search"
23
+ _ACTIVATED = re.compile(r"<activated_tools>(.*?)</activated_tools>", re.DOTALL)
24
+
25
+
26
+ class DeferredTools:
27
+ """Hide selected tool schemas until ``tool_search`` activates them."""
28
+
29
+ def __init__(
30
+ self,
31
+ inner: Tools,
32
+ *,
33
+ deferred: set[str] | None = None,
34
+ initially_active: set[str] | None = None,
35
+ _discovered: set[str] | None = None,
36
+ ) -> None:
37
+ """Configure explicit deferrals and session-local activation state."""
38
+ if any(definition.get("name") == _TOOL_NAME for definition in inner.list()):
39
+ raise ValueError(f"the wrapped tool collection already defines {_TOOL_NAME!r}")
40
+ self._inner = inner
41
+ self._explicit = frozenset(deferred or ())
42
+ self._initially_active = frozenset(initially_active or ())
43
+ self._discovered = _discovered if _discovered is not None else set()
44
+
45
+ async def execute(self, name: str, call_id: str, arguments: Any) -> dict[str, Any]:
46
+ """Search for a schema or execute an already exposed tool."""
47
+ if name == _TOOL_NAME:
48
+ return self._search(arguments)
49
+ definition = self._definition(name)
50
+ if definition is None:
51
+ return {"type": "error", "message": f"tool is not available: {name}"}
52
+ if self._is_deferred(definition) and name not in self._active_names():
53
+ return {
54
+ "type": "error",
55
+ "message": f"tool {name!r} is deferred; load it with {_TOOL_NAME} first",
56
+ }
57
+ return await self._inner.execute(name, call_id, arguments)
58
+
59
+ def get(self, name: str) -> dict[str, Any] | None:
60
+ """Return a definition only when the model is allowed to call it."""
61
+ if name == _TOOL_NAME:
62
+ return self._search_definition() if self._deferred_definitions() else None
63
+ definition = self._definition(name)
64
+ if definition is None:
65
+ return None
66
+ if self._is_deferred(definition) and name not in self._active_names():
67
+ return None
68
+ return definition
69
+
70
+ def list(self) -> builtins.list[dict[str, Any]]:
71
+ """List non-deferred and previously discovered schemas."""
72
+ active = self._active_names()
73
+ definitions = [
74
+ definition
75
+ for definition in self._inner.list()
76
+ if not self._is_deferred(definition) or str(definition.get("name")) in active
77
+ ]
78
+ if self._deferred_definitions():
79
+ definitions.append(self._search_definition())
80
+ return definitions
81
+
82
+ async def prepare(self, messages: builtins.list[BaseMessage]) -> None:
83
+ """Recover activated names from durable tool results in the transcript."""
84
+ if isinstance(self._inner, DynamicTools):
85
+ prepared = self._inner.prepare(messages)
86
+ if isawaitable(prepared):
87
+ await prepared
88
+ available = {str(definition.get("name")) for definition in self._inner.list()}
89
+ for message in messages:
90
+ if not isinstance(message, ToolMessage) or not isinstance(message.content, str):
91
+ continue
92
+ for match in _ACTIVATED.finditer(message.content):
93
+ try:
94
+ names = json.loads(match.group(1))
95
+ except json.JSONDecodeError:
96
+ continue
97
+ if isinstance(names, list):
98
+ self._discovered.update(
99
+ name for name in names if isinstance(name, str) and name in available
100
+ )
101
+
102
+ def reset(self) -> None:
103
+ """Clear session-local discoveries while retaining explicit initial tools."""
104
+ self._discovered.clear()
105
+
106
+ def get_context(self) -> Any:
107
+ """Forward the workspace context when the wrapped collection supports it."""
108
+ get_context = getattr(self._inner, "get_context", None)
109
+ if get_context is None:
110
+ raise TypeError("wrapped tools do not expose a workspace context")
111
+ return get_context()
112
+
113
+ def with_context(self, context: Any) -> DeferredTools:
114
+ """Rebind the wrapped tools and retain this conversation's discoveries."""
115
+ with_context = getattr(self._inner, "with_context", None)
116
+ if with_context is None:
117
+ raise TypeError("wrapped tools cannot be rebound to a workspace context")
118
+ return DeferredTools(
119
+ with_context(context),
120
+ deferred=set(self._explicit),
121
+ initially_active=set(self._initially_active),
122
+ _discovered=self._discovered,
123
+ )
124
+
125
+ def _search(self, arguments: Any) -> dict[str, Any]:
126
+ if not isinstance(arguments, Mapping) or not isinstance(arguments.get("query"), str):
127
+ return {"type": "error", "message": "tool_search requires a string 'query'"}
128
+ query = str(arguments["query"]).strip()
129
+ raw_limit = arguments.get("max_results", 5)
130
+ if not isinstance(raw_limit, int) or isinstance(raw_limit, bool):
131
+ return {"type": "error", "message": "tool_search 'max_results' must be an integer"}
132
+ maximum = min(max(raw_limit, 1), 20)
133
+ matches = self._matches(query, maximum)
134
+ self._discovered.update(matches)
135
+ encoded = json.dumps(matches, separators=(",", ":"), ensure_ascii=False)
136
+ visible = (
137
+ "Activated deferred tools:\n" + "\n".join(f"- {name}" for name in matches)
138
+ if matches
139
+ else "No matching deferred tools found."
140
+ )
141
+ return {"type": "text", "text": f"{visible}\n<activated_tools>{encoded}</activated_tools>"}
142
+
143
+ def _matches(self, query: str, maximum: int) -> builtins.list[str]:
144
+ definitions = self._deferred_definitions()
145
+ by_name = {str(definition.get("name")): definition for definition in definitions}
146
+ if query.lower().startswith("select:"):
147
+ requested = [item.strip() for item in query[7:].split(",") if item.strip()]
148
+ lowered = {name.lower(): name for name in by_name}
149
+ selected = dict.fromkeys(
150
+ lowered[item.lower()] for item in requested if item.lower() in lowered
151
+ )
152
+ return list(selected)[:maximum]
153
+
154
+ terms = [term.lower() for term in query.split() if term]
155
+ required = [term[1:] for term in terms if term.startswith("+") and len(term) > 1]
156
+ optional = [term for term in terms if not term.startswith("+")]
157
+ scored: builtins.list[tuple[int, str]] = []
158
+ for name, definition in by_name.items():
159
+ haystack = " ".join(
160
+ (
161
+ name,
162
+ str(definition.get("description", "")),
163
+ str(definition.get("search_hint", "")),
164
+ )
165
+ ).lower()
166
+ if required and not all(term in haystack for term in required):
167
+ continue
168
+ score = sum(
169
+ 10 if term in name.lower() else 2
170
+ for term in [*required, *optional]
171
+ if term in haystack
172
+ )
173
+ if score:
174
+ scored.append((score, name))
175
+ ranked = sorted(scored, key=lambda item: (-item[0], item[1]))[:maximum]
176
+ return [name for _score, name in ranked]
177
+
178
+ def _active_names(self) -> set[str]:
179
+ return set(self._initially_active) | self._discovered
180
+
181
+ def _definition(self, name: str) -> dict[str, Any] | None:
182
+ return next(
183
+ (definition for definition in self._inner.list() if definition.get("name") == name),
184
+ None,
185
+ )
186
+
187
+ def _deferred_definitions(self) -> builtins.list[dict[str, Any]]:
188
+ return [definition for definition in self._inner.list() if self._is_deferred(definition)]
189
+
190
+ def _is_deferred(self, definition: dict[str, Any]) -> bool:
191
+ if definition.get("always_load") is True:
192
+ return False
193
+ name = str(definition.get("name", ""))
194
+ return bool(
195
+ name in self._explicit
196
+ or name.startswith("mcp__")
197
+ or definition.get("is_mcp") is True
198
+ or definition.get("should_defer") is True
199
+ or definition.get("defer_loading") is True
200
+ )
201
+
202
+ def _search_definition(self) -> dict[str, Any]:
203
+ names = sorted(str(definition.get("name")) for definition in self._deferred_definitions())
204
+ return {
205
+ "name": _TOOL_NAME,
206
+ "description": (
207
+ "Load complete schemas for deferred tools. Until selected, only these names are "
208
+ "known and the tools cannot be called. Use 'select:<name>' for exact selection "
209
+ "or capability keywords to search.\n\n<available_deferred_tools>\n"
210
+ + "\n".join(names)
211
+ + "\n</available_deferred_tools>"
212
+ ),
213
+ "parameters": {
214
+ "type": "object",
215
+ "properties": {
216
+ "query": {"type": "string"},
217
+ "max_results": {"type": "integer", "minimum": 1, "maximum": 20},
218
+ },
219
+ "required": ["query"],
220
+ "additionalProperties": False,
221
+ },
222
+ "is_exclusive": True,
223
+ "is_concurrency_safe": True,
224
+ }
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.5
2
+ Name: semora-coding
3
+ Version: 0.2.0
4
+ Summary: A coding agent's toolset, prompts and policies, assembled over the semora core.
5
+ Project-URL: Homepage, https://github.com/donggyun112/semora
6
+ Project-URL: Source, https://github.com/donggyun112/semora
7
+ Project-URL: Changelog, https://github.com/donggyun112/semora/blob/main/CHANGELOG.md
8
+ Author: donggyun112
9
+ License-Expression: MIT
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Typing :: Typed
15
+ Requires-Python: >=3.12
16
+ Requires-Dist: langchain-core<2,>=1
17
+ Requires-Dist: semora==0.2.0
18
+ Description-Content-Type: text/markdown
19
+
20
+ # semora-coding
21
+
22
+ The coding-agent layer over the `semora` core: built-in tools (`read`, `write`, `edit`, `grep`,
23
+ `glob`, `Bash`, `web_fetch`), system-prompt assembly, plan mode, goals, the skill catalog and
24
+ deferred tool search.
25
+
26
+ ```
27
+ uv add "semora[coding]"
28
+ ```
29
+
30
+ It is a reference assembly. The core promises that a tool's effect happens once and that every
31
+ decision about it has a seam; this package is one answer to what those tools and seams say to a
32
+ model. Replace any of it — the core does not know it is here.