plm-skill-kernel 1.0.0__tar.gz

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.
Files changed (53) hide show
  1. plm_skill_kernel-1.0.0/PKG-INFO +135 -0
  2. plm_skill_kernel-1.0.0/README.md +115 -0
  3. plm_skill_kernel-1.0.0/plm_skill_kernel/__init__.py +33 -0
  4. plm_skill_kernel-1.0.0/plm_skill_kernel/__main__.py +13 -0
  5. plm_skill_kernel-1.0.0/plm_skill_kernel/dispatcher.py +141 -0
  6. plm_skill_kernel-1.0.0/plm_skill_kernel/registry.py +80 -0
  7. plm_skill_kernel-1.0.0/plm_skill_kernel/sdk/__init__.py +29 -0
  8. plm_skill_kernel-1.0.0/plm_skill_kernel/sdk/_registry.py +111 -0
  9. plm_skill_kernel-1.0.0/plm_skill_kernel/sdk/context.py +66 -0
  10. plm_skill_kernel-1.0.0/plm_skill_kernel/sdk/decorator.py +147 -0
  11. plm_skill_kernel-1.0.0/plm_skill_kernel/sdk/types.py +86 -0
  12. plm_skill_kernel-1.0.0/plm_skill_kernel/server.py +173 -0
  13. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/__init__.py +16 -0
  14. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/agents/__init__.py +13 -0
  15. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/agents/suggest.py +191 -0
  16. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/bpmn/__init__.py +9 -0
  17. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/bpmn/generate.py +147 -0
  18. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/cleansing/__init__.py +10 -0
  19. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/cleansing/dedupe.py +162 -0
  20. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/cleansing/normalise.py +178 -0
  21. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/documents/__init__.py +12 -0
  22. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/documents/parse.py +202 -0
  23. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/__init__.py +32 -0
  24. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/_v1_corpus.py +153 -0
  25. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/get_record.py +102 -0
  26. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/resolve.py +143 -0
  27. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/retrieve.py +143 -0
  28. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/search.py +161 -0
  29. plm_skill_kernel-1.0.0/plm_skill_kernel/skills/knowledge/validate.py +222 -0
  30. plm_skill_kernel-1.0.0/plm_skill_kernel/skills_legacy/__init__.py +0 -0
  31. plm_skill_kernel-1.0.0/plm_skill_kernel/skills_legacy/knowledge_pack_loader.py +17 -0
  32. plm_skill_kernel-1.0.0/plm_skill_kernel/skills_legacy/knowledge_search_engine.py +25 -0
  33. plm_skill_kernel-1.0.0/plm_skill_kernel/skills_legacy/plm_skill_registry.py +573 -0
  34. plm_skill_kernel-1.0.0/plm_skill_kernel/skills_legacy/plm_tool_definitions.py +44 -0
  35. plm_skill_kernel-1.0.0/plm_skill_kernel/skills_legacy/skill_orchestrator.py +163 -0
  36. plm_skill_kernel-1.0.0/plm_skill_kernel.egg-info/PKG-INFO +135 -0
  37. plm_skill_kernel-1.0.0/plm_skill_kernel.egg-info/SOURCES.txt +51 -0
  38. plm_skill_kernel-1.0.0/plm_skill_kernel.egg-info/dependency_links.txt +1 -0
  39. plm_skill_kernel-1.0.0/plm_skill_kernel.egg-info/entry_points.txt +2 -0
  40. plm_skill_kernel-1.0.0/plm_skill_kernel.egg-info/requires.txt +12 -0
  41. plm_skill_kernel-1.0.0/plm_skill_kernel.egg-info/top_level.txt +1 -0
  42. plm_skill_kernel-1.0.0/pyproject.toml +113 -0
  43. plm_skill_kernel-1.0.0/setup.cfg +4 -0
  44. plm_skill_kernel-1.0.0/tests/test_agents_suggest_asgi.py +208 -0
  45. plm_skill_kernel-1.0.0/tests/test_agents_suggest_unit.py +72 -0
  46. plm_skill_kernel-1.0.0/tests/test_dispatcher.py +98 -0
  47. plm_skill_kernel-1.0.0/tests/test_documents_parse_asgi.py +197 -0
  48. plm_skill_kernel-1.0.0/tests/test_documents_parse_unit.py +99 -0
  49. plm_skill_kernel-1.0.0/tests/test_sdk_decorator.py +113 -0
  50. plm_skill_kernel-1.0.0/tests/test_server.py +202 -0
  51. plm_skill_kernel-1.0.0/tests/test_skills_cleansing.py +203 -0
  52. plm_skill_kernel-1.0.0/tests/test_skills_knowledge.py +376 -0
  53. plm_skill_kernel-1.0.0/tests/test_skills_knowledge_registry.py +81 -0
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: plm-skill-kernel
3
+ Version: 1.0.0
4
+ Summary: TracePulse PLM Skill Kernel — Wave 2 Conv A. Decorator-based Skill SDK + V1 in-process dispatcher + Kernel HTTP server stub matching CR.10 §7.bis (POST /v1/skills/{id}/invoke). Ships 3 starter skills (cleansing.normalise, cleansing.dedupe, bpmn.generate). Sibling of plm-engine-core; the two communicate over the V1.1 HTTP loopback (SKILL_KERNEL_LOOPBACK=on) — no Python-level coupling at the dispatch boundary.
5
+ Author: TracePulse
6
+ License: Proprietary
7
+ Requires-Python: >=3.12
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: plm-shared
10
+ Requires-Dist: pydantic>=2.5.0
11
+ Requires-Dist: fastapi>=0.110
12
+ Requires-Dist: uvicorn>=0.27
13
+ Requires-Dist: httpx>=0.27.0
14
+ Requires-Dist: plm-engine-core
15
+ Requires-Dist: plm-knowledge
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest; extra == "test"
18
+ Requires-Dist: pytest-asyncio; extra == "test"
19
+ Requires-Dist: httpx<1,>=0.27; extra == "test"
20
+
21
+ # plm-skill-kernel
22
+
23
+ Wave 2 Conv A (FTR-604). The Skill Kernel package — sibling of
24
+ `plm-engine-core` — that hosts the decorator-based Skill SDK, the V1
25
+ in-process dispatcher, and the Kernel HTTP server stub matching CR.10
26
+ §7.bis verbatim.
27
+
28
+ ## Layout
29
+
30
+ ```
31
+ 02_App/plm-skill-kernel/
32
+ ├── pyproject.toml # editable install + import-linter contracts
33
+ ├── plm_skill_kernel/
34
+ │ ├── __init__.py # re-exports `skill`, `SkillContext`, etc.
35
+ │ ├── __main__.py # `python -m plm_skill_kernel` → server
36
+ │ ├── server.py # FastAPI app at POST /v1/skills/{id}/invoke
37
+ │ ├── dispatcher.py # V1 in-process invoker
38
+ │ ├── registry.py # boot wiring — discovers + registers skills
39
+ │ ├── sdk/
40
+ │ │ ├── decorator.py # `@skill(id=, version=)`
41
+ │ │ ├── context.py # SkillContext Pydantic
42
+ │ │ ├── types.py # SkillInvokeRequest / SkillInvokeResult
43
+ │ │ └── _registry.py # in-process decorator registry
44
+ │ └── skills/ # 3 starter skills (Q-W2A-1 nested layout)
45
+ │ ├── cleansing/
46
+ │ │ ├── normalise.py # cleansing.normalise / 1.0.0 / L2
47
+ │ │ └── dedupe.py # cleansing.dedupe / 1.0.0 / L2
48
+ │ └── bpmn/
49
+ │ └── generate.py # bpmn.generate / 1.0.0 / L1 (thin proxy)
50
+ └── tests/ # unit + ASGI in-process integration
51
+ ```
52
+
53
+ ## Editable install
54
+
55
+ ```bash
56
+ cd 02_App/backend
57
+ .\venv\Scripts\Activate.ps1 # Windows
58
+ pip install -e ../plm-skill-kernel
59
+ ```
60
+
61
+ Mirrors the plm-engine-core editable-install pattern.
62
+
63
+ ## Dev launch (Q-W2A-3)
64
+
65
+ ```bash
66
+ python -m plm_skill_kernel # uvicorn on port 8100
67
+ plm-skill-kernel --port 8100 # equivalent CLI
68
+ ```
69
+
70
+ The backend at port 8000 is unaffected. The V1.1 loopback round-trip
71
+ (D-CONV-M-4 closure) sends HTTP from the Engine Core dispatcher to
72
+ this Kernel server when `SKILL_KERNEL_LOOPBACK=on`.
73
+
74
+ ## V1 wire contract
75
+
76
+ `POST /v1/skills/{id}/invoke` — frozen at CR.10 §7.bis.
77
+
78
+ ```
79
+ Headers:
80
+ X-Core-Caller: <UUID> (required when caller is Core)
81
+ Idempotency-Key: <uuid> (optional, V1)
82
+ traceparent: <W3C> (optional)
83
+
84
+ Request body (SkillInvokeRequest):
85
+ payload: Dict[str, Any]
86
+ version: str ("1.0.0", etc.)
87
+ autonomy_hint: Optional[str] ("L1" / "L2" / "L3")
88
+
89
+ Response body (SkillInvokeResult):
90
+ ok: bool
91
+ result: Optional[Dict[str, Any]]
92
+ error: Optional[SkillErrorEnvelope]
93
+ deferred_to: Optional[str]
94
+ cancelled: Optional[bool]
95
+ ```
96
+
97
+ ## Skill authoring (Decision #59 — decorator)
98
+
99
+ ```python
100
+ from plm_skill_kernel import skill, SkillContext
101
+
102
+ @skill(id="cleansing.normalise", version="1.0.0")
103
+ async def normalise(payload: dict, ctx: SkillContext) -> dict:
104
+ return {"normalised": _normalise_records(payload["records"])}
105
+ ```
106
+
107
+ The decorator auto-registers into the in-process registry at module
108
+ import time + carries `version` through to the federated manifest
109
+ builder (CR.9).
110
+
111
+ ## Import-linter contracts
112
+
113
+ Two Forbidden contracts live in `pyproject.toml`:
114
+
115
+ 1. `plm_skill_kernel` MUST NOT import `plm_engine_core` — federation
116
+ contract preservation. The two communicate over HTTP, not Python.
117
+ 2. `plm_skill_kernel` MUST NOT import `plm_accelerators` (Workbench)
118
+ — mirror of the plm-engine-core constraint.
119
+
120
+ Run:
121
+
122
+ ```bash
123
+ lint-imports --config 02_App/plm-skill-kernel/pyproject.toml
124
+ ```
125
+
126
+ ## Tests
127
+
128
+ ```bash
129
+ cd 02_App/plm-skill-kernel
130
+ python -m pytest -q
131
+ ```
132
+
133
+ The V1.1 HTTP round-trip integration test mounts the FastAPI app via
134
+ httpx.AsyncClient(ASGITransport) — no subprocess, no real port
135
+ (Q-W2A-4).
@@ -0,0 +1,115 @@
1
+ # plm-skill-kernel
2
+
3
+ Wave 2 Conv A (FTR-604). The Skill Kernel package — sibling of
4
+ `plm-engine-core` — that hosts the decorator-based Skill SDK, the V1
5
+ in-process dispatcher, and the Kernel HTTP server stub matching CR.10
6
+ §7.bis verbatim.
7
+
8
+ ## Layout
9
+
10
+ ```
11
+ 02_App/plm-skill-kernel/
12
+ ├── pyproject.toml # editable install + import-linter contracts
13
+ ├── plm_skill_kernel/
14
+ │ ├── __init__.py # re-exports `skill`, `SkillContext`, etc.
15
+ │ ├── __main__.py # `python -m plm_skill_kernel` → server
16
+ │ ├── server.py # FastAPI app at POST /v1/skills/{id}/invoke
17
+ │ ├── dispatcher.py # V1 in-process invoker
18
+ │ ├── registry.py # boot wiring — discovers + registers skills
19
+ │ ├── sdk/
20
+ │ │ ├── decorator.py # `@skill(id=, version=)`
21
+ │ │ ├── context.py # SkillContext Pydantic
22
+ │ │ ├── types.py # SkillInvokeRequest / SkillInvokeResult
23
+ │ │ └── _registry.py # in-process decorator registry
24
+ │ └── skills/ # 3 starter skills (Q-W2A-1 nested layout)
25
+ │ ├── cleansing/
26
+ │ │ ├── normalise.py # cleansing.normalise / 1.0.0 / L2
27
+ │ │ └── dedupe.py # cleansing.dedupe / 1.0.0 / L2
28
+ │ └── bpmn/
29
+ │ └── generate.py # bpmn.generate / 1.0.0 / L1 (thin proxy)
30
+ └── tests/ # unit + ASGI in-process integration
31
+ ```
32
+
33
+ ## Editable install
34
+
35
+ ```bash
36
+ cd 02_App/backend
37
+ .\venv\Scripts\Activate.ps1 # Windows
38
+ pip install -e ../plm-skill-kernel
39
+ ```
40
+
41
+ Mirrors the plm-engine-core editable-install pattern.
42
+
43
+ ## Dev launch (Q-W2A-3)
44
+
45
+ ```bash
46
+ python -m plm_skill_kernel # uvicorn on port 8100
47
+ plm-skill-kernel --port 8100 # equivalent CLI
48
+ ```
49
+
50
+ The backend at port 8000 is unaffected. The V1.1 loopback round-trip
51
+ (D-CONV-M-4 closure) sends HTTP from the Engine Core dispatcher to
52
+ this Kernel server when `SKILL_KERNEL_LOOPBACK=on`.
53
+
54
+ ## V1 wire contract
55
+
56
+ `POST /v1/skills/{id}/invoke` — frozen at CR.10 §7.bis.
57
+
58
+ ```
59
+ Headers:
60
+ X-Core-Caller: <UUID> (required when caller is Core)
61
+ Idempotency-Key: <uuid> (optional, V1)
62
+ traceparent: <W3C> (optional)
63
+
64
+ Request body (SkillInvokeRequest):
65
+ payload: Dict[str, Any]
66
+ version: str ("1.0.0", etc.)
67
+ autonomy_hint: Optional[str] ("L1" / "L2" / "L3")
68
+
69
+ Response body (SkillInvokeResult):
70
+ ok: bool
71
+ result: Optional[Dict[str, Any]]
72
+ error: Optional[SkillErrorEnvelope]
73
+ deferred_to: Optional[str]
74
+ cancelled: Optional[bool]
75
+ ```
76
+
77
+ ## Skill authoring (Decision #59 — decorator)
78
+
79
+ ```python
80
+ from plm_skill_kernel import skill, SkillContext
81
+
82
+ @skill(id="cleansing.normalise", version="1.0.0")
83
+ async def normalise(payload: dict, ctx: SkillContext) -> dict:
84
+ return {"normalised": _normalise_records(payload["records"])}
85
+ ```
86
+
87
+ The decorator auto-registers into the in-process registry at module
88
+ import time + carries `version` through to the federated manifest
89
+ builder (CR.9).
90
+
91
+ ## Import-linter contracts
92
+
93
+ Two Forbidden contracts live in `pyproject.toml`:
94
+
95
+ 1. `plm_skill_kernel` MUST NOT import `plm_engine_core` — federation
96
+ contract preservation. The two communicate over HTTP, not Python.
97
+ 2. `plm_skill_kernel` MUST NOT import `plm_accelerators` (Workbench)
98
+ — mirror of the plm-engine-core constraint.
99
+
100
+ Run:
101
+
102
+ ```bash
103
+ lint-imports --config 02_App/plm-skill-kernel/pyproject.toml
104
+ ```
105
+
106
+ ## Tests
107
+
108
+ ```bash
109
+ cd 02_App/plm-skill-kernel
110
+ python -m pytest -q
111
+ ```
112
+
113
+ The V1.1 HTTP round-trip integration test mounts the FastAPI app via
114
+ httpx.AsyncClient(ASGITransport) — no subprocess, no real port
115
+ (Q-W2A-4).
@@ -0,0 +1,33 @@
1
+ """TracePulse PLM Skill Kernel — Wave 2 Conv A (FTR-604).
2
+
3
+ Sibling of ``plm-engine-core``. Owns:
4
+
5
+ * The decorator-based Skill SDK (Decision #59 — Q-W2A-1 nested layout).
6
+ * The V1 in-process dispatcher.
7
+ * The Kernel HTTP server stub at ``POST /v1/skills/{id}/invoke``
8
+ matching CR.10 §7.bis verbatim.
9
+ * 3 starter skills harvested per Decision #60 — ``cleansing.normalise``,
10
+ ``cleansing.dedupe``, ``bpmn.generate``.
11
+
12
+ The Kernel and the Engine Core are deployed-separable. Their only
13
+ sanctioned interaction is the V1.1 HTTP loopback (env-flagged on
14
+ ``SKILL_KERNEL_LOOPBACK``); any Python-level reach-in is forbidden by
15
+ the import-linter contract in ``pyproject.toml``.
16
+ """
17
+ __version__ = "0.1.0-alpha"
18
+
19
+ # The decorator + sdk surface is the public authoring API. Re-exported
20
+ # here so callers write ``from plm_skill_kernel import skill`` rather
21
+ # than the deeper path. Skill author imports stay short + stable.
22
+ from .sdk.decorator import skill
23
+ from .sdk.context import SkillContext
24
+ from .sdk.types import SkillInvokeRequest, SkillInvokeResult, SkillErrorEnvelope
25
+
26
+ __all__ = [
27
+ "SkillContext",
28
+ "SkillErrorEnvelope",
29
+ "SkillInvokeRequest",
30
+ "SkillInvokeResult",
31
+ "__version__",
32
+ "skill",
33
+ ]
@@ -0,0 +1,13 @@
1
+ """``python -m plm_skill_kernel`` entry — delegates to :func:`server._cli_entry`.
2
+
3
+ Q-W2A-3 picked the standalone launch shape. The ``__main__`` module is
4
+ the canonical Python idiom; the ``plm-skill-kernel`` console script
5
+ points at the same function via ``pyproject.toml [project.scripts]``.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from .server import _cli_entry
10
+
11
+
12
+ if __name__ == "__main__":
13
+ _cli_entry()
@@ -0,0 +1,141 @@
1
+ """V1 in-process Kernel dispatcher.
2
+
3
+ Resolves ``(skill_id, version)`` against the SDK registry, validates the
4
+ payload + context, and invokes the registered callable. Exceptions are
5
+ caught + converted to a typed :class:`SkillInvokeResult` carrying a
6
+ :class:`SkillErrorEnvelope` — bare exceptions never escape the dispatch
7
+ boundary so the HTTP server can rely on always getting back a
8
+ serialisable result.
9
+
10
+ Decision #51 (Conv M / Q-M2 = (c) "Both"): the dispatcher lives in
11
+ plm-skill-kernel; the plm-engine-core dispatcher (under
12
+ ``plm_engine_core.agent_runtime.dispatcher``) becomes a thin transport
13
+ proxy when ``SKILL_KERNEL_LOOPBACK=on`` (Wave 2 Conv A item 8 — the
14
+ V1.1 transport closure for D-CONV-M-4).
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import logging
19
+ from typing import Any, Dict, Optional
20
+
21
+ from .sdk._registry import RegisteredSkill, _Registry, get_registry
22
+ from .sdk.context import SkillContext
23
+ from .sdk.types import (
24
+ SkillErrorEnvelope,
25
+ SkillInvokeRequest,
26
+ SkillInvokeResult,
27
+ )
28
+
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ # ── Error codes (D-AUDIT-7 entries owned by the Skill Kernel) ────────────
34
+ #
35
+ # These mirror string-values declared on
36
+ # ``plm_engine_core.control_plane.capability_registry.errors`` +
37
+ # ``services/llm_errors.py``. Wave 2 Conv A re-uses the existing
38
+ # ``SKILL_NOT_FOUND`` / ``SKILL_VERSION_NOT_FOUND`` /
39
+ # ``KERNEL_DISPATCH_DISABLED`` strings (no new codes — the close
40
+ # audit at Conv N tallied 57 codes; this conv keeps that total).
41
+ SKILL_NOT_FOUND = "SKILL_NOT_FOUND"
42
+ SKILL_VERSION_NOT_FOUND = "SKILL_VERSION_NOT_FOUND"
43
+ KERNEL_DISPATCH_FAILED = "KERNEL_DISPATCH_FAILED"
44
+
45
+
46
+ class InProcessDispatcher:
47
+ """V1 in-process invoker.
48
+
49
+ Constructor accepts an explicit registry so tests can isolate
50
+ registration state. Production callers receive the module-level
51
+ singleton via :func:`get_registry`.
52
+ """
53
+
54
+ def __init__(self, registry: Optional[_Registry] = None) -> None:
55
+ self._registry = registry if registry is not None else get_registry()
56
+
57
+ async def dispatch(
58
+ self,
59
+ skill_id: str,
60
+ request: SkillInvokeRequest,
61
+ *,
62
+ context: SkillContext,
63
+ ) -> SkillInvokeResult:
64
+ """Resolve + invoke a skill.
65
+
66
+ Returns a :class:`SkillInvokeResult` always — exceptions raised
67
+ by the skill body are caught and surfaced via the
68
+ ``KERNEL_DISPATCH_FAILED`` envelope.
69
+ """
70
+ entry = self._registry.resolve(skill_id, request.version)
71
+ if entry is None:
72
+ return self._not_found(skill_id, request.version)
73
+
74
+ try:
75
+ result = await entry.fn(dict(request.payload), context)
76
+ except Exception as exc: # noqa: BLE001 — boundary; we re-package
77
+ logger.exception(
78
+ "skill dispatch failed skill_id=%s version=%s",
79
+ skill_id,
80
+ request.version,
81
+ )
82
+ return SkillInvokeResult(
83
+ ok=False,
84
+ error=SkillErrorEnvelope(
85
+ error_code=KERNEL_DISPATCH_FAILED,
86
+ message=f"skill {skill_id!r} raised {type(exc).__name__}",
87
+ detail={"exception_message": str(exc)},
88
+ ),
89
+ )
90
+
91
+ # V1 contract: skills return a JSON-serialisable dict.
92
+ if not isinstance(result, dict):
93
+ return SkillInvokeResult(
94
+ ok=False,
95
+ error=SkillErrorEnvelope(
96
+ error_code=KERNEL_DISPATCH_FAILED,
97
+ message=(
98
+ f"skill {skill_id!r} returned a non-dict result; "
99
+ "V1 contract requires `dict[str, Any]`"
100
+ ),
101
+ detail={"actual_type": type(result).__name__},
102
+ ),
103
+ )
104
+
105
+ return SkillInvokeResult(ok=True, result=result)
106
+
107
+ def _not_found(self, skill_id: str, version: str) -> SkillInvokeResult:
108
+ # Differentiate "skill_id has no entries" from
109
+ # "skill_id exists but the requested version is missing" by
110
+ # walking the registry once.
111
+ any_version_for_id = any(
112
+ e.skill_id == skill_id for e in self._registry.list_all()
113
+ )
114
+ if any_version_for_id:
115
+ return SkillInvokeResult(
116
+ ok=False,
117
+ error=SkillErrorEnvelope(
118
+ error_code=SKILL_VERSION_NOT_FOUND,
119
+ message=(
120
+ f"skill {skill_id!r} version {version!r} is not "
121
+ "registered"
122
+ ),
123
+ detail={"skill_id": skill_id, "version": version},
124
+ ),
125
+ )
126
+ return SkillInvokeResult(
127
+ ok=False,
128
+ error=SkillErrorEnvelope(
129
+ error_code=SKILL_NOT_FOUND,
130
+ message=f"no skill registered for id={skill_id!r}",
131
+ detail={"skill_id": skill_id, "version": version},
132
+ ),
133
+ )
134
+
135
+
136
+ __all__ = [
137
+ "InProcessDispatcher",
138
+ "KERNEL_DISPATCH_FAILED",
139
+ "SKILL_NOT_FOUND",
140
+ "SKILL_VERSION_NOT_FOUND",
141
+ ]
@@ -0,0 +1,80 @@
1
+ """Boot-time skill discovery + registration.
2
+
3
+ Importing :func:`load_starter_skills` triggers the import of each
4
+ starter-skill module. Each module's top-level ``@skill(...)``
5
+ decorator runs as a side-effect of import, so by the time
6
+ :func:`load_starter_skills` returns the SDK registry contains every
7
+ starter skill.
8
+
9
+ This module is the V1 wiring point — production ``server.py`` calls
10
+ :func:`load_starter_skills` at startup. Tests that need a clean
11
+ registry call ``get_registry().clear()`` first.
12
+
13
+ Wave 3 Conv A (per Decision #60) added ``documents.parse`` +
14
+ ``agents.suggest`` to the fan-out — bringing the starter set to 5
15
+ skills (3 Wave 2 + 2 Wave 3).
16
+
17
+ Wave 5 Conv C (FTR-603 / Decision #122 + #124) adds the 5 active V1
18
+ ``knowledge.*`` primitives — bringing the boot-loaded set to 10
19
+ skills (3 Wave 2 + 2 Wave 3 + 5 Wave 5). The 2 deferred primitives
20
+ (``explain`` / ``context_pack``) stay YAML ``draft`` capabilities
21
+ only and do NOT register a kernel skill in V1; their dispatch is
22
+ short-circuited by the engine-core envelope returning
23
+ ``KS_PRIMITIVE_DEFERRED_V1_1``.
24
+ """
25
+ from __future__ import annotations
26
+
27
+ import logging
28
+ from typing import List, Tuple
29
+
30
+ from .sdk._registry import RegisteredSkill, get_registry
31
+
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+
36
+ def load_starter_skills() -> List[RegisteredSkill]:
37
+ """Import the 5 starter skills + return the resulting list.
38
+
39
+ The imports themselves run the decorator side-effects; this
40
+ function returns the registry's view post-load so callers can
41
+ log or assert on the exact skills available.
42
+
43
+ Idempotent — re-importing a module is a no-op (Python caches),
44
+ and the SDK registry tolerates same-callable re-registration.
45
+ """
46
+ # Local imports — module-load side effect is the registration. The
47
+ # imports MUST be inside the function so test fixtures that clear
48
+ # the registry can re-trigger registration by re-calling.
49
+ # NOTE: `noqa: F401` because the bound names are unused; the
50
+ # imports' side effect (decorator registration) is the point.
51
+ from .skills.cleansing import dedupe as _dedupe # noqa: F401
52
+ from .skills.cleansing import normalise as _normalise # noqa: F401
53
+ from .skills.bpmn import generate as _generate # noqa: F401
54
+ from .skills.documents import parse as _parse # noqa: F401
55
+ from .skills.agents import suggest as _suggest # noqa: F401
56
+ # Wave 5 Conv C — FTR-603 Knowledge Services V1 active primitives.
57
+ from .skills.knowledge import search as _ks_search # noqa: F401
58
+ from .skills.knowledge import get_record as _ks_get_record # noqa: F401
59
+ from .skills.knowledge import retrieve as _ks_retrieve # noqa: F401
60
+ from .skills.knowledge import resolve as _ks_resolve # noqa: F401
61
+ from .skills.knowledge import validate as _ks_validate # noqa: F401
62
+
63
+ entries = get_registry().list_all()
64
+ logger.info(
65
+ "skill_kernel boot_loaded count=%s ids=%s",
66
+ len(entries),
67
+ ",".join(sorted(f"{e.skill_id}@{e.version}" for e in entries)),
68
+ )
69
+ return entries
70
+
71
+
72
+ def list_registered_skill_ids() -> List[Tuple[str, str]]:
73
+ """Return ``(skill_id, version)`` pairs for every registered entry.
74
+
75
+ Used by the federation manifest builder + introspection endpoints.
76
+ """
77
+ return [(e.skill_id, e.version) for e in get_registry().list_all()]
78
+
79
+
80
+ __all__ = ["list_registered_skill_ids", "load_starter_skills"]
@@ -0,0 +1,29 @@
1
+ """Public Skill SDK surface (Wave 2 Conv A / Decision #59).
2
+
3
+ Skill authors import from here:
4
+
5
+ from plm_skill_kernel import skill, SkillContext
6
+ # or, equivalently:
7
+ from plm_skill_kernel.sdk import skill, SkillContext
8
+
9
+ The decorator + context + wire types are the V1-stable authoring
10
+ surface; internal helpers (the registry implementation, validation
11
+ helpers) live under ``plm_skill_kernel.sdk._registry`` and are NOT
12
+ re-exported.
13
+ """
14
+ from .context import SkillContext
15
+ from .decorator import SkillDecoratorError, skill
16
+ from .types import (
17
+ SkillErrorEnvelope,
18
+ SkillInvokeRequest,
19
+ SkillInvokeResult,
20
+ )
21
+
22
+ __all__ = [
23
+ "SkillContext",
24
+ "SkillDecoratorError",
25
+ "SkillErrorEnvelope",
26
+ "SkillInvokeRequest",
27
+ "SkillInvokeResult",
28
+ "skill",
29
+ ]
@@ -0,0 +1,111 @@
1
+ """Internal in-process decorator registry.
2
+
3
+ The :func:`plm_skill_kernel.sdk.skill` decorator stamps each
4
+ ``async def`` it wraps into this module-level registry at import
5
+ time. The Kernel dispatcher then resolves ``(skill_id, version)``
6
+ back to the callable at dispatch time.
7
+
8
+ Hot-reload (V1.1) replaces a single entry atomically; V1 callers that
9
+ re-import a skill module will get a duplicate-registration error so
10
+ test suites can detect accidental re-registration.
11
+
12
+ Decision #25 / #41 / #52 / #53 V1 in-memory + Protocol pattern:
13
+ the registry is a module-level dict + an ``RLock``. Wave 4+ may swap
14
+ the backing store for a process-shared cache without changing the
15
+ public surface (which is just ``register`` + ``resolve`` + ``list_all``).
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import threading
20
+ from dataclasses import dataclass
21
+ from typing import Awaitable, Callable, Dict, List, Optional, Tuple
22
+
23
+ from .context import SkillContext
24
+
25
+
26
+ # Type alias for a registered skill body — every skill is
27
+ # ``async def fn(payload: dict, ctx: SkillContext) -> dict``.
28
+ SkillCallable = Callable[
29
+ [Dict, SkillContext], Awaitable[Dict]
30
+ ]
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class RegisteredSkill:
35
+ """One entry in the decorator registry.
36
+
37
+ The ``min_autonomy`` + ``streaming`` + ``hitl_required`` fields
38
+ are advisory metadata the SDK can pass through to the federation
39
+ manifest builder (CR.9) at boot. They are NOT enforced at dispatch
40
+ time — Core's selector + gating already ran upstream.
41
+ """
42
+
43
+ skill_id: str
44
+ version: str
45
+ fn: SkillCallable
46
+ description: Optional[str]
47
+ min_autonomy: str # "L1" / "L2" / "L3"
48
+ streaming: bool
49
+ hitl_required: bool
50
+
51
+
52
+ class _Registry:
53
+ """Thread-safe registry of decorated skills.
54
+
55
+ Keyed by ``(skill_id, version)``. Adding a duplicate raises a
56
+ :class:`RuntimeError` so test fixtures catch double-registration
57
+ rather than silently shadowing.
58
+ """
59
+
60
+ def __init__(self) -> None:
61
+ self._lock = threading.RLock()
62
+ self._entries: Dict[Tuple[str, str], RegisteredSkill] = {}
63
+
64
+ def register(self, entry: RegisteredSkill) -> None:
65
+ key = (entry.skill_id, entry.version)
66
+ with self._lock:
67
+ existing = self._entries.get(key)
68
+ if existing is not None and existing.fn is not entry.fn:
69
+ raise RuntimeError(
70
+ f"skill {entry.skill_id!r} version {entry.version!r} "
71
+ "is already registered with a different callable; "
72
+ "duplicate registration is forbidden in V1"
73
+ )
74
+ self._entries[key] = entry
75
+
76
+ def resolve(
77
+ self, skill_id: str, version: str
78
+ ) -> Optional[RegisteredSkill]:
79
+ with self._lock:
80
+ return self._entries.get((skill_id, version))
81
+
82
+ def list_all(self) -> List[RegisteredSkill]:
83
+ with self._lock:
84
+ return list(self._entries.values())
85
+
86
+ def clear(self) -> None:
87
+ """Test-only — unregister everything. Production callers MUST NOT."""
88
+ with self._lock:
89
+ self._entries.clear()
90
+
91
+
92
+ # Module-level singleton. Imported by the decorator + the dispatcher.
93
+ _REGISTRY = _Registry()
94
+
95
+
96
+ def get_registry() -> _Registry:
97
+ """Return the module-level registry singleton.
98
+
99
+ Tests that need isolation can call ``get_registry().clear()`` in a
100
+ fixture's setup; production callers never construct a fresh registry
101
+ (skills register at import time into the singleton).
102
+ """
103
+ return _REGISTRY
104
+
105
+
106
+ __all__ = [
107
+ "RegisteredSkill",
108
+ "SkillCallable",
109
+ "_Registry",
110
+ "get_registry",
111
+ ]