langchain-skill-router 0.3.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.
@@ -0,0 +1,264 @@
1
+ Metadata-Version: 2.3
2
+ Name: langchain-skill-router
3
+ Version: 0.3.0
4
+ Summary: Per-turn skill routing for LangChain deepagents: a pluggable judge (Jev included, or your own) loads only the SKILL.md skills each turn needs, keeping a catalog of hundreds out of the prompt.
5
+ Keywords: langchain,langgraph,deepagents,agent-skills,skill-md,skill-routing,skill-selection,llm-routing,context-engineering,middleware,jev,typesafe,self-hosted,ai-agents,llm
6
+ Author: deyna256
7
+ Author-email: deyna256 <literallybugcreator@gmail.com>
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Typing :: Typed
19
+ Classifier: Framework :: AsyncIO
20
+ Requires-Dist: langchain>=1.0
21
+ Requires-Dist: deepagents>=0.7.15
22
+ Requires-Dist: typesafe-sdk>=0.7.0 ; extra == 'jev'
23
+ Requires-Python: >=3.11
24
+ Project-URL: Homepage, https://github.com/deyna256/langchain-skill-router
25
+ Project-URL: Documentation, https://github.com/deyna256/langchain-skill-router/blob/main/docs/design.md
26
+ Project-URL: Changelog, https://github.com/deyna256/langchain-skill-router/blob/main/CHANGELOG.md
27
+ Project-URL: Issues, https://github.com/deyna256/langchain-skill-router/issues
28
+ Provides-Extra: jev
29
+ Description-Content-Type: text/markdown
30
+
31
+ <div align="center">
32
+
33
+ <img src="https://raw.githubusercontent.com/deyna256/langchain-skill-router/main/docs/assets/banner.svg" alt="Skill Router: per-turn skill selection for LangChain deepagents, with a pluggable judge" width="100%">
34
+
35
+ <h3>Per-turn skill routing for LangChain deepagents</h3>
36
+
37
+ <p>A drop-in replacement for the <a href="https://github.com/langchain-ai/deepagents">deepagents</a>
38
+ <code>SkillsMiddleware</code>: on each user turn a fast judge decides which <code>SKILL.md</code> skills are
39
+ needed, and only those are loaded, so a catalog of hundreds stays out of the prompt. Bring any judge: a
40
+ hosted model, a self-hosted one, or plain rules. An adapter for
41
+ <a href="https://docs.typesafe.ai/introduction">Jev</a> is included.</p>
42
+
43
+ <p><strong>113.0k&nbsp;&rarr;&nbsp;25.8k input tokens per turn</strong> on a 236-skill catalog, and the agent
44
+ answered <strong>90%</strong> of questions correctly against <strong>88%</strong> with the whole catalog in
45
+ the prompt. <a href="#results">See the benchmark&nbsp;&rarr;</a></p>
46
+
47
+ <p><sub>Formerly <code>langchain-loadout</code>.</sub></p>
48
+
49
+ [![CI](https://github.com/deyna256/langchain-skill-router/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/deyna256/langchain-skill-router/actions/workflows/ci.yml)
50
+ [![PyPI](https://img.shields.io/pypi/v/langchain-skill-router)](https://pypi.org/project/langchain-skill-router/)
51
+ [![Python](https://img.shields.io/pypi/pyversions/langchain-skill-router)](https://pypi.org/project/langchain-skill-router/)
52
+ [![License: MIT](https://img.shields.io/github/license/deyna256/langchain-skill-router)](https://github.com/deyna256/langchain-skill-router/blob/main/LICENSE)
53
+ [![Judge: pluggable](https://img.shields.io/badge/judge-pluggable-4b32c3)](#bring-your-own-judge)
54
+ <br>
55
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
56
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
57
+ [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
58
+
59
+ [Quick start](#quick-start) · [How it works](#how-it-works) · [Your own judge](#bring-your-own-judge) · [Results](#results) · [FAQ](#faq) · [Docs](https://github.com/deyna256/langchain-skill-router/blob/main/docs/design.md)
60
+
61
+ </div>
62
+
63
+ ---
64
+
65
+ ## Why Skill Router
66
+
67
+ [deepagents](https://github.com/langchain-ai/deepagents) lists every skill's name and description in
68
+ the system prompt on every model call. With a handful of skills that is fine. With hundreds, the list
69
+ takes tens of thousands of tokens per call, and the model has to pick the right procedure from a crowd
70
+ of similar ones.
71
+
72
+ Skill Router replaces the built-in `SkillsMiddleware` with one that decides **per user turn**:
73
+
74
+ - **Only what the turn needs.** A confident pick is loaded with its instructions. When the pick is unsure,
75
+ the model gets a short list of up to three candidates to choose from, and it can search the rest of the
76
+ catalog with the `find_skill` tool.
77
+ - **Any judge, by design.** The judge is a small protocol: typed questions in, probabilities out. Use the
78
+ included Jev adapter, a self-hosted inference model, deterministic rules or anything else that can answer.
79
+ The routing core depends on neither LangChain nor any provider.
80
+ - **Calibrated, not guessed.** Every question is answered with a probability, so "load it", "offer it"
81
+ and "skip it" are thresholds you can read and tune. They are not buried in a prompt.
82
+ - **Conversation-aware.** The judge also sees the recent conversation, so a follow-up like "and for
83
+ April?" still routes to the skill the thread is about.
84
+ - **Safe by default.** A timeout or an outage of the judge gives the agent the full catalog, exactly as it
85
+ would be without Skill Router. Bad credentials raise instead of hiding a broken setup.
86
+ - **Cache-friendly.** The system prompt is identical on every call, and the turn's skills are written after
87
+ the user's message. The provider's prompt cache keeps working across turns.
88
+
89
+ ## Quick start
90
+
91
+ ```sh
92
+ pip install "langchain-skill-router[jev]" # with the Jev adapter
93
+ pip install langchain-skill-router # with your own judge
94
+ ```
95
+
96
+ Put skills in `./skills/<name>/SKILL.md`, with `name` and `description` in YAML front matter (the Agent
97
+ Skills format deepagents uses). Set `TYPESAFE_API_KEY` ([get a key](https://console.typesafe.ai/settings/keys))
98
+ and your model provider's key. Jev is the judge in this example; see [below](#bring-your-own-judge) for
99
+ your own.
100
+
101
+ ```python
102
+ from deepagents import create_deep_agent
103
+ from deepagents.backends import FilesystemBackend
104
+
105
+ from langchain_skill_router.langchain import SkillRouterMiddleware
106
+ from langchain_skill_router.providers.jev import JevJudge
107
+
108
+ backend = FilesystemBackend(root_dir=".", virtual_mode=True)
109
+ skill_router = SkillRouterMiddleware(backend=backend, sources=["/skills/"], judge=JevJudge())
110
+
111
+ agent = create_deep_agent(
112
+ model="anthropic:claude-sonnet-5",
113
+ backend=backend,
114
+ skills=["/skills/"],
115
+ middleware=[skill_router], # takes the place of the built-in SkillsMiddleware
116
+ )
117
+ await agent.ainvoke({"messages": [{"role": "user", "content": "I need a statement for the embassy"}]})
118
+ ```
119
+
120
+ Selection runs on `ainvoke` and `astream`. A synchronous run falls back to the ordinary skills
121
+ middleware. Requires Python 3.11+.
122
+
123
+ ## How it works
124
+
125
+ <img src="https://raw.githubusercontent.com/deyna256/langchain-skill-router/main/docs/assets/how-it-works.svg" alt="A user turn is ranked with a need gate, then verified; the skill is loaded, offered in a short list, or nothing is loaded. A pluggable judge answers every question; on failure the agent gets the full catalog." width="100%">
126
+
127
+ On each new user message, Skill Router makes one decision, and the rest of the turn's model calls reuse it:
128
+
129
+ 1. **Rank and gate, in parallel.** The judge ranks the catalog by description against the request and the
130
+ recent conversation. In the same round it answers whether the request needs a skill at all. A catalog
131
+ larger than the judge's declared limits (for Jev, 32k tokens and 255 options per call) is split into
132
+ parts, keeping related skills together, and the part winners are ranked again.
133
+ 2. **Verify.** The judge reads the start of each top candidate's `SKILL.md`, picks one and checks whether
134
+ each candidate does what the user asked. A ranking that is already sure skips this second call.
135
+ 3. **Load or offer.** A pick verified at 0.9 or higher is loaded with its instructions. Otherwise the model
136
+ is offered a short list. If no skill is needed, nothing is loaded.
137
+
138
+ A decision is capped at two seconds by default (`Settings.timeout`). The routing core has no framework
139
+ dependency: `SkillRouter` works on any list of skills.
140
+
141
+ ## Bring your own judge
142
+
143
+ A judge answers two kinds of question in one call: `Pick` (a probability for every option) and `YesNo`
144
+ (a probability of yes). It declares its per-call `limits`, which the router uses to split a large catalog,
145
+ and optionally a per-call `timeout`:
146
+
147
+ ```python
148
+ from langchain_skill_router import Answer, Limits, Pick, YesNo
149
+
150
+
151
+ class MyJudge:
152
+ limits = Limits(max_tokens=8_000, max_options=100) # what one call can take; Limits() for no limit
153
+ timeout = 5.0 # seconds per call, enforced by the router; None for no limit
154
+
155
+ async def ask(self, state, questions):
156
+ # state: {"request": ..., "context": ...}; questions: {key: Pick | YesNo}
157
+ # Call a self-hosted model, a classifier or rules here, and answer every key.
158
+ return {
159
+ key: Answer({option: 1 / len(q.options) for option in q.options}) if isinstance(q, Pick) else Answer({"yes": 0.5})
160
+ for key, q in questions.items()
161
+ }
162
+ ```
163
+
164
+ Pass it as `SkillRouterMiddleware(..., judge=MyJudge())`. `langchain_skill_router.testing.check_judge` checks
165
+ an adapter against the contract, and `ScriptedJudge` answers from a script in your tests. The probabilities
166
+ are compared against thresholds, so the closer they are to calibrated, the better the defaults fit.
167
+
168
+ ## Results
169
+
170
+ Benchmark of **langchain-skill-router 0.2.2** on a bank-statement assistant built with deepagents: 236 skills,
171
+ **55 conversations × 5 turns** per variant (275 turns each), Jev as the judge, one agent model for all
172
+ variants. "Perfect selection" always loads the skill the question was written for: the ceiling for any router.
173
+
174
+ <img src="https://raw.githubusercontent.com/deyna256/langchain-skill-router/main/docs/assets/results.svg" alt="Input tokens per turn: 113.0k with the full catalog against 25.8k with Skill Router, 4.4 times less. Right skill in front of the model: 55% against 85%. Correct answers: 88% against 90%." width="100%">
175
+
176
+ | Metric | Skill Router | Full catalog | Perfect selection |
177
+ |---|---|---|---|
178
+ | Input tokens per turn | **25.8k** | 113.0k | 26.8k |
179
+ | Skills in the prompt, characters per call | **2.6k** | 89.2k | 2.7k |
180
+ | Right skill in front of the model | **85%** | 55% | 97% |
181
+ | Loaded skill was the right one | **96%** (230 of 239) | — | 100% |
182
+ | Input from the prompt cache on a new turn | 77% | 95% | 77% |
183
+ | Correct answers | **90%** | 88% | 88% |
184
+ | Answers that depend on a rule inside a skill (21 turns) | **76%** | 67% | 62% |
185
+
186
+ - **4.4× less context per turn**, with the right skill in front of the model far more often.
187
+ - **Accuracy holds.** +1.5 points against the full catalog (95% interval −1.5 to +4.7, sign test p = 0.63):
188
+ the same answers from a fraction of the prompt. Without any skills the agent scored 82%, so the catalog
189
+ does matter — it just does not have to be in the prompt.
190
+ - **Where a skill carries a rule the model cannot infer, routing wins**: 76% against 67% for the full list.
191
+ - **A wrong skill costs the most.** In the 18 turns where the model worked from a wrong skill, 72% of
192
+ answers were correct, against 92% with the right one. Raise `load_at` if your catalog has many
193
+ near-duplicate skills.
194
+ - **The cache holds across turns.** 77% of a new turn's first call came from the cache, against 41% before
195
+ 0.2.2, when the turn's skills were kept out of the conversation.
196
+
197
+ An earlier run of the same benchmark put Skill Router 5 points *below* the full catalog. The difference was six
198
+ skills in the testbed catalog whose instructions contradicted the rule the expected answer was computed
199
+ from; they dragged down every variant that loads skills, including perfect selection. Skill quality is the
200
+ ceiling of any router.
201
+
202
+ Generated data, one judge and one agent model. Fit the thresholds to your own data.
203
+ [Methodology, earlier measurements and known limits →](https://github.com/deyna256/langchain-skill-router/blob/main/docs/design.md)
204
+
205
+ ## Configuration
206
+
207
+ Everything is in `Settings`, passed as `SkillRouterMiddleware(..., settings=Settings(...))`:
208
+
209
+ | Knob | Default | What it does |
210
+ |---|---|---|
211
+ | `load_at` | 0.9 | How sure verification must be of its pick to load it |
212
+ | `max_suggest` | 3 | How many candidates may be offered when the pick is unsure |
213
+ | `need_at` | 0.3 | Below this "is a skill needed" probability, nothing is loaded |
214
+ | `skip_verify_at` | 0.9 | Ranking this sure skips verification (`None` turns it off) |
215
+ | `timeout` | 2.0 | Seconds for the whole decision, after which the full catalog is used |
216
+ | `need_question`, `rank_question`, … | general wording | The questions Jev is asked. Naming your domain separates better |
217
+
218
+ `JevJudge(timeout=...)` limits each call to Jev. `on_decision=` on the middleware receives every
219
+ decision's trace (probabilities, stage, timing) for logs and metrics.
220
+
221
+ ## FAQ
222
+
223
+ **Does Skill Router work without deepagents?**
224
+ Yes. `langchain_skill_router` (the core) has no framework or provider dependency:
225
+ `SkillRouter(catalog, judge).decide(Turn(request, context))` returns what to load and what to suggest.
226
+
227
+ **Do I need Jev?**
228
+ No. Jev is the included adapter and what Skill Router was measured with, but any `Judge` works: a self-hosted
229
+ inference model behind your own adapter, deterministic rules, or another provider. See
230
+ [Bring your own judge](#bring-your-own-judge).
231
+
232
+ **What happens if the judge is slow or down?**
233
+ The turn gets the full catalog, as if Skill Router were not installed, and the trace records why.
234
+
235
+ **What does the judge see?**
236
+ The request and the recent conversation (the user's messages and the agent's replies, without tool
237
+ output) are sent, capped at `request_chars` and `context_chars`. So are skill names, descriptions and the
238
+ start of the candidates' instructions. Supply your own `context=` function to send less, or a
239
+ self-hosted judge to keep everything in your network.
240
+
241
+ **Does it keep state between turns?**
242
+ No. Every turn is decided from scratch. No checkpointer or extra storage is required.
243
+
244
+ ## Documentation
245
+
246
+ | Read | Covers |
247
+ |---|---|
248
+ | [How it works](https://github.com/deyna256/langchain-skill-router/blob/main/docs/design.md) | Selection flow, judge interface, settings, measurements and known limits |
249
+ | [Development guide](https://github.com/deyna256/langchain-skill-router/blob/main/docs/development.md) | Architecture, conventions and testing |
250
+ | [Contributing](https://github.com/deyna256/langchain-skill-router/blob/main/CONTRIBUTING.md) | Local setup, checks, issues and pull requests |
251
+ | [Changelog](https://github.com/deyna256/langchain-skill-router/blob/main/CHANGELOG.md) | Release history |
252
+
253
+ The public API may change in minor releases while the version is `0.x`.
254
+
255
+ ## Acknowledgements
256
+
257
+ Skill Router was inspired by [Jev](https://docs.typesafe.ai/introduction), TypeSafe AI's model for typed
258
+ decisions, and its first round follows TypeSafe's
259
+ [skill suggestion cookbook](https://docs.typesafe.ai/cookbooks/skill_suggestion). Skill Router is an independent
260
+ open-source project, not affiliated with or endorsed by TypeSafe AI.
261
+
262
+ ## License
263
+
264
+ [MIT](https://github.com/deyna256/langchain-skill-router/blob/main/LICENSE) © 2026 Ivan Deyna
@@ -0,0 +1,234 @@
1
+ <div align="center">
2
+
3
+ <img src="https://raw.githubusercontent.com/deyna256/langchain-skill-router/main/docs/assets/banner.svg" alt="Skill Router: per-turn skill selection for LangChain deepagents, with a pluggable judge" width="100%">
4
+
5
+ <h3>Per-turn skill routing for LangChain deepagents</h3>
6
+
7
+ <p>A drop-in replacement for the <a href="https://github.com/langchain-ai/deepagents">deepagents</a>
8
+ <code>SkillsMiddleware</code>: on each user turn a fast judge decides which <code>SKILL.md</code> skills are
9
+ needed, and only those are loaded, so a catalog of hundreds stays out of the prompt. Bring any judge: a
10
+ hosted model, a self-hosted one, or plain rules. An adapter for
11
+ <a href="https://docs.typesafe.ai/introduction">Jev</a> is included.</p>
12
+
13
+ <p><strong>113.0k&nbsp;&rarr;&nbsp;25.8k input tokens per turn</strong> on a 236-skill catalog, and the agent
14
+ answered <strong>90%</strong> of questions correctly against <strong>88%</strong> with the whole catalog in
15
+ the prompt. <a href="#results">See the benchmark&nbsp;&rarr;</a></p>
16
+
17
+ <p><sub>Formerly <code>langchain-loadout</code>.</sub></p>
18
+
19
+ [![CI](https://github.com/deyna256/langchain-skill-router/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/deyna256/langchain-skill-router/actions/workflows/ci.yml)
20
+ [![PyPI](https://img.shields.io/pypi/v/langchain-skill-router)](https://pypi.org/project/langchain-skill-router/)
21
+ [![Python](https://img.shields.io/pypi/pyversions/langchain-skill-router)](https://pypi.org/project/langchain-skill-router/)
22
+ [![License: MIT](https://img.shields.io/github/license/deyna256/langchain-skill-router)](https://github.com/deyna256/langchain-skill-router/blob/main/LICENSE)
23
+ [![Judge: pluggable](https://img.shields.io/badge/judge-pluggable-4b32c3)](#bring-your-own-judge)
24
+ <br>
25
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
26
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
27
+ [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
28
+
29
+ [Quick start](#quick-start) · [How it works](#how-it-works) · [Your own judge](#bring-your-own-judge) · [Results](#results) · [FAQ](#faq) · [Docs](https://github.com/deyna256/langchain-skill-router/blob/main/docs/design.md)
30
+
31
+ </div>
32
+
33
+ ---
34
+
35
+ ## Why Skill Router
36
+
37
+ [deepagents](https://github.com/langchain-ai/deepagents) lists every skill's name and description in
38
+ the system prompt on every model call. With a handful of skills that is fine. With hundreds, the list
39
+ takes tens of thousands of tokens per call, and the model has to pick the right procedure from a crowd
40
+ of similar ones.
41
+
42
+ Skill Router replaces the built-in `SkillsMiddleware` with one that decides **per user turn**:
43
+
44
+ - **Only what the turn needs.** A confident pick is loaded with its instructions. When the pick is unsure,
45
+ the model gets a short list of up to three candidates to choose from, and it can search the rest of the
46
+ catalog with the `find_skill` tool.
47
+ - **Any judge, by design.** The judge is a small protocol: typed questions in, probabilities out. Use the
48
+ included Jev adapter, a self-hosted inference model, deterministic rules or anything else that can answer.
49
+ The routing core depends on neither LangChain nor any provider.
50
+ - **Calibrated, not guessed.** Every question is answered with a probability, so "load it", "offer it"
51
+ and "skip it" are thresholds you can read and tune. They are not buried in a prompt.
52
+ - **Conversation-aware.** The judge also sees the recent conversation, so a follow-up like "and for
53
+ April?" still routes to the skill the thread is about.
54
+ - **Safe by default.** A timeout or an outage of the judge gives the agent the full catalog, exactly as it
55
+ would be without Skill Router. Bad credentials raise instead of hiding a broken setup.
56
+ - **Cache-friendly.** The system prompt is identical on every call, and the turn's skills are written after
57
+ the user's message. The provider's prompt cache keeps working across turns.
58
+
59
+ ## Quick start
60
+
61
+ ```sh
62
+ pip install "langchain-skill-router[jev]" # with the Jev adapter
63
+ pip install langchain-skill-router # with your own judge
64
+ ```
65
+
66
+ Put skills in `./skills/<name>/SKILL.md`, with `name` and `description` in YAML front matter (the Agent
67
+ Skills format deepagents uses). Set `TYPESAFE_API_KEY` ([get a key](https://console.typesafe.ai/settings/keys))
68
+ and your model provider's key. Jev is the judge in this example; see [below](#bring-your-own-judge) for
69
+ your own.
70
+
71
+ ```python
72
+ from deepagents import create_deep_agent
73
+ from deepagents.backends import FilesystemBackend
74
+
75
+ from langchain_skill_router.langchain import SkillRouterMiddleware
76
+ from langchain_skill_router.providers.jev import JevJudge
77
+
78
+ backend = FilesystemBackend(root_dir=".", virtual_mode=True)
79
+ skill_router = SkillRouterMiddleware(backend=backend, sources=["/skills/"], judge=JevJudge())
80
+
81
+ agent = create_deep_agent(
82
+ model="anthropic:claude-sonnet-5",
83
+ backend=backend,
84
+ skills=["/skills/"],
85
+ middleware=[skill_router], # takes the place of the built-in SkillsMiddleware
86
+ )
87
+ await agent.ainvoke({"messages": [{"role": "user", "content": "I need a statement for the embassy"}]})
88
+ ```
89
+
90
+ Selection runs on `ainvoke` and `astream`. A synchronous run falls back to the ordinary skills
91
+ middleware. Requires Python 3.11+.
92
+
93
+ ## How it works
94
+
95
+ <img src="https://raw.githubusercontent.com/deyna256/langchain-skill-router/main/docs/assets/how-it-works.svg" alt="A user turn is ranked with a need gate, then verified; the skill is loaded, offered in a short list, or nothing is loaded. A pluggable judge answers every question; on failure the agent gets the full catalog." width="100%">
96
+
97
+ On each new user message, Skill Router makes one decision, and the rest of the turn's model calls reuse it:
98
+
99
+ 1. **Rank and gate, in parallel.** The judge ranks the catalog by description against the request and the
100
+ recent conversation. In the same round it answers whether the request needs a skill at all. A catalog
101
+ larger than the judge's declared limits (for Jev, 32k tokens and 255 options per call) is split into
102
+ parts, keeping related skills together, and the part winners are ranked again.
103
+ 2. **Verify.** The judge reads the start of each top candidate's `SKILL.md`, picks one and checks whether
104
+ each candidate does what the user asked. A ranking that is already sure skips this second call.
105
+ 3. **Load or offer.** A pick verified at 0.9 or higher is loaded with its instructions. Otherwise the model
106
+ is offered a short list. If no skill is needed, nothing is loaded.
107
+
108
+ A decision is capped at two seconds by default (`Settings.timeout`). The routing core has no framework
109
+ dependency: `SkillRouter` works on any list of skills.
110
+
111
+ ## Bring your own judge
112
+
113
+ A judge answers two kinds of question in one call: `Pick` (a probability for every option) and `YesNo`
114
+ (a probability of yes). It declares its per-call `limits`, which the router uses to split a large catalog,
115
+ and optionally a per-call `timeout`:
116
+
117
+ ```python
118
+ from langchain_skill_router import Answer, Limits, Pick, YesNo
119
+
120
+
121
+ class MyJudge:
122
+ limits = Limits(max_tokens=8_000, max_options=100) # what one call can take; Limits() for no limit
123
+ timeout = 5.0 # seconds per call, enforced by the router; None for no limit
124
+
125
+ async def ask(self, state, questions):
126
+ # state: {"request": ..., "context": ...}; questions: {key: Pick | YesNo}
127
+ # Call a self-hosted model, a classifier or rules here, and answer every key.
128
+ return {
129
+ key: Answer({option: 1 / len(q.options) for option in q.options}) if isinstance(q, Pick) else Answer({"yes": 0.5})
130
+ for key, q in questions.items()
131
+ }
132
+ ```
133
+
134
+ Pass it as `SkillRouterMiddleware(..., judge=MyJudge())`. `langchain_skill_router.testing.check_judge` checks
135
+ an adapter against the contract, and `ScriptedJudge` answers from a script in your tests. The probabilities
136
+ are compared against thresholds, so the closer they are to calibrated, the better the defaults fit.
137
+
138
+ ## Results
139
+
140
+ Benchmark of **langchain-skill-router 0.2.2** on a bank-statement assistant built with deepagents: 236 skills,
141
+ **55 conversations × 5 turns** per variant (275 turns each), Jev as the judge, one agent model for all
142
+ variants. "Perfect selection" always loads the skill the question was written for: the ceiling for any router.
143
+
144
+ <img src="https://raw.githubusercontent.com/deyna256/langchain-skill-router/main/docs/assets/results.svg" alt="Input tokens per turn: 113.0k with the full catalog against 25.8k with Skill Router, 4.4 times less. Right skill in front of the model: 55% against 85%. Correct answers: 88% against 90%." width="100%">
145
+
146
+ | Metric | Skill Router | Full catalog | Perfect selection |
147
+ |---|---|---|---|
148
+ | Input tokens per turn | **25.8k** | 113.0k | 26.8k |
149
+ | Skills in the prompt, characters per call | **2.6k** | 89.2k | 2.7k |
150
+ | Right skill in front of the model | **85%** | 55% | 97% |
151
+ | Loaded skill was the right one | **96%** (230 of 239) | — | 100% |
152
+ | Input from the prompt cache on a new turn | 77% | 95% | 77% |
153
+ | Correct answers | **90%** | 88% | 88% |
154
+ | Answers that depend on a rule inside a skill (21 turns) | **76%** | 67% | 62% |
155
+
156
+ - **4.4× less context per turn**, with the right skill in front of the model far more often.
157
+ - **Accuracy holds.** +1.5 points against the full catalog (95% interval −1.5 to +4.7, sign test p = 0.63):
158
+ the same answers from a fraction of the prompt. Without any skills the agent scored 82%, so the catalog
159
+ does matter — it just does not have to be in the prompt.
160
+ - **Where a skill carries a rule the model cannot infer, routing wins**: 76% against 67% for the full list.
161
+ - **A wrong skill costs the most.** In the 18 turns where the model worked from a wrong skill, 72% of
162
+ answers were correct, against 92% with the right one. Raise `load_at` if your catalog has many
163
+ near-duplicate skills.
164
+ - **The cache holds across turns.** 77% of a new turn's first call came from the cache, against 41% before
165
+ 0.2.2, when the turn's skills were kept out of the conversation.
166
+
167
+ An earlier run of the same benchmark put Skill Router 5 points *below* the full catalog. The difference was six
168
+ skills in the testbed catalog whose instructions contradicted the rule the expected answer was computed
169
+ from; they dragged down every variant that loads skills, including perfect selection. Skill quality is the
170
+ ceiling of any router.
171
+
172
+ Generated data, one judge and one agent model. Fit the thresholds to your own data.
173
+ [Methodology, earlier measurements and known limits →](https://github.com/deyna256/langchain-skill-router/blob/main/docs/design.md)
174
+
175
+ ## Configuration
176
+
177
+ Everything is in `Settings`, passed as `SkillRouterMiddleware(..., settings=Settings(...))`:
178
+
179
+ | Knob | Default | What it does |
180
+ |---|---|---|
181
+ | `load_at` | 0.9 | How sure verification must be of its pick to load it |
182
+ | `max_suggest` | 3 | How many candidates may be offered when the pick is unsure |
183
+ | `need_at` | 0.3 | Below this "is a skill needed" probability, nothing is loaded |
184
+ | `skip_verify_at` | 0.9 | Ranking this sure skips verification (`None` turns it off) |
185
+ | `timeout` | 2.0 | Seconds for the whole decision, after which the full catalog is used |
186
+ | `need_question`, `rank_question`, … | general wording | The questions Jev is asked. Naming your domain separates better |
187
+
188
+ `JevJudge(timeout=...)` limits each call to Jev. `on_decision=` on the middleware receives every
189
+ decision's trace (probabilities, stage, timing) for logs and metrics.
190
+
191
+ ## FAQ
192
+
193
+ **Does Skill Router work without deepagents?**
194
+ Yes. `langchain_skill_router` (the core) has no framework or provider dependency:
195
+ `SkillRouter(catalog, judge).decide(Turn(request, context))` returns what to load and what to suggest.
196
+
197
+ **Do I need Jev?**
198
+ No. Jev is the included adapter and what Skill Router was measured with, but any `Judge` works: a self-hosted
199
+ inference model behind your own adapter, deterministic rules, or another provider. See
200
+ [Bring your own judge](#bring-your-own-judge).
201
+
202
+ **What happens if the judge is slow or down?**
203
+ The turn gets the full catalog, as if Skill Router were not installed, and the trace records why.
204
+
205
+ **What does the judge see?**
206
+ The request and the recent conversation (the user's messages and the agent's replies, without tool
207
+ output) are sent, capped at `request_chars` and `context_chars`. So are skill names, descriptions and the
208
+ start of the candidates' instructions. Supply your own `context=` function to send less, or a
209
+ self-hosted judge to keep everything in your network.
210
+
211
+ **Does it keep state between turns?**
212
+ No. Every turn is decided from scratch. No checkpointer or extra storage is required.
213
+
214
+ ## Documentation
215
+
216
+ | Read | Covers |
217
+ |---|---|
218
+ | [How it works](https://github.com/deyna256/langchain-skill-router/blob/main/docs/design.md) | Selection flow, judge interface, settings, measurements and known limits |
219
+ | [Development guide](https://github.com/deyna256/langchain-skill-router/blob/main/docs/development.md) | Architecture, conventions and testing |
220
+ | [Contributing](https://github.com/deyna256/langchain-skill-router/blob/main/CONTRIBUTING.md) | Local setup, checks, issues and pull requests |
221
+ | [Changelog](https://github.com/deyna256/langchain-skill-router/blob/main/CHANGELOG.md) | Release history |
222
+
223
+ The public API may change in minor releases while the version is `0.x`.
224
+
225
+ ## Acknowledgements
226
+
227
+ Skill Router was inspired by [Jev](https://docs.typesafe.ai/introduction), TypeSafe AI's model for typed
228
+ decisions, and its first round follows TypeSafe's
229
+ [skill suggestion cookbook](https://docs.typesafe.ai/cookbooks/skill_suggestion). Skill Router is an independent
230
+ open-source project, not affiliated with or endorsed by TypeSafe AI.
231
+
232
+ ## License
233
+
234
+ [MIT](https://github.com/deyna256/langchain-skill-router/blob/main/LICENSE) © 2026 Ivan Deyna
@@ -0,0 +1,87 @@
1
+ [project]
2
+ name = "langchain-skill-router"
3
+ version = "0.3.0"
4
+ description = "Per-turn skill routing for LangChain deepagents: a pluggable judge (Jev included, or your own) loads only the SKILL.md skills each turn needs, keeping a catalog of hundreds out of the prompt."
5
+ keywords = [
6
+ "langchain",
7
+ "langgraph",
8
+ "deepagents",
9
+ "agent-skills",
10
+ "skill-md",
11
+ "skill-routing",
12
+ "skill-selection",
13
+ "llm-routing",
14
+ "context-engineering",
15
+ "middleware",
16
+ "jev",
17
+ "typesafe",
18
+ "self-hosted",
19
+ "ai-agents",
20
+ "llm",
21
+ ]
22
+ classifiers = [
23
+ "Development Status :: 3 - Alpha",
24
+ "Intended Audience :: Developers",
25
+ "License :: OSI Approved :: MIT License",
26
+ "Operating System :: OS Independent",
27
+ "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
32
+ "Topic :: Software Development :: Libraries :: Python Modules",
33
+ "Typing :: Typed",
34
+ "Framework :: AsyncIO",
35
+ ]
36
+ readme = "README.md"
37
+ requires-python = ">=3.11"
38
+ dependencies = [
39
+ "langchain>=1.0",
40
+ "deepagents>=0.7.15",
41
+ ]
42
+
43
+ [[project.authors]]
44
+ name = "deyna256"
45
+ email = "literallybugcreator@gmail.com"
46
+
47
+ [project.urls]
48
+ Homepage = "https://github.com/deyna256/langchain-skill-router"
49
+ Documentation = "https://github.com/deyna256/langchain-skill-router/blob/main/docs/design.md"
50
+ Changelog = "https://github.com/deyna256/langchain-skill-router/blob/main/CHANGELOG.md"
51
+ Issues = "https://github.com/deyna256/langchain-skill-router/issues"
52
+
53
+ [project.optional-dependencies]
54
+ jev = ["typesafe-sdk>=0.7.0"]
55
+
56
+ [build-system]
57
+ requires = ["uv_build>=0.12.16,<0.13.0"]
58
+ build-backend = "uv_build"
59
+
60
+ [dependency-groups]
61
+ dev = [
62
+ "pytest>=9.1.1",
63
+ "pytest-asyncio>=1.4.0",
64
+ "ruff>=0.16.8",
65
+ "ty>=0.0.82",
66
+ ]
67
+
68
+ [tool.pytest.ini_options]
69
+ asyncio_mode = "auto"
70
+ testpaths = ["tests"]
71
+
72
+ [tool.ruff]
73
+ line-length = 135
74
+ target-version = "py311"
75
+
76
+ [tool.ruff.lint]
77
+ select = [
78
+ "E",
79
+ "W",
80
+ "F",
81
+ "I",
82
+ "B",
83
+ "C4",
84
+ "UP",
85
+ "SIM",
86
+ "RUF",
87
+ ]
@@ -0,0 +1,63 @@
1
+ [project]
2
+ name = "langchain-skill-router"
3
+ version = "0.3.0"
4
+ description = "Per-turn skill routing for LangChain deepagents: a pluggable judge (Jev included, or your own) loads only the SKILL.md skills each turn needs, keeping a catalog of hundreds out of the prompt."
5
+ keywords = [
6
+ "langchain", "langgraph", "deepagents", "agent-skills", "skill-md", "skill-routing", "skill-selection",
7
+ "llm-routing", "context-engineering", "middleware", "jev", "typesafe", "self-hosted", "ai-agents", "llm",
8
+ ]
9
+ classifiers = [
10
+ "Development Status :: 3 - Alpha",
11
+ "Intended Audience :: Developers",
12
+ "License :: OSI Approved :: MIT License",
13
+ "Operating System :: OS Independent",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
19
+ "Topic :: Software Development :: Libraries :: Python Modules",
20
+ "Typing :: Typed",
21
+ "Framework :: AsyncIO",
22
+ ]
23
+ readme = "README.md"
24
+ authors = [
25
+ { name = "deyna256", email = "literallybugcreator@gmail.com" }
26
+ ]
27
+ requires-python = ">=3.11"
28
+ dependencies = [
29
+ "langchain>=1.0",
30
+ "deepagents>=0.7.15",
31
+ ]
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/deyna256/langchain-skill-router"
35
+ Documentation = "https://github.com/deyna256/langchain-skill-router/blob/main/docs/design.md"
36
+ Changelog = "https://github.com/deyna256/langchain-skill-router/blob/main/CHANGELOG.md"
37
+ Issues = "https://github.com/deyna256/langchain-skill-router/issues"
38
+
39
+ [project.optional-dependencies]
40
+ jev = ["typesafe-sdk>=0.7.0"]
41
+
42
+ [build-system]
43
+ requires = ["uv_build>=0.12.16,<0.13.0"]
44
+ build-backend = "uv_build"
45
+
46
+ [dependency-groups]
47
+ dev = [
48
+ "pytest>=9.1.1",
49
+ "pytest-asyncio>=1.4.0",
50
+ "ruff>=0.16.8",
51
+ "ty>=0.0.82",
52
+ ]
53
+
54
+ [tool.pytest.ini_options]
55
+ asyncio_mode = "auto"
56
+ testpaths = ["tests"]
57
+
58
+ [tool.ruff]
59
+ line-length = 135
60
+ target-version = "py311"
61
+
62
+ [tool.ruff.lint]
63
+ select = ["E", "W", "F", "I", "B", "C4", "UP", "SIM", "RUF"]