ph-text-index 0.1.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.
- ph_text_index-0.1.0/.gitignore +31 -0
- ph_text_index-0.1.0/LICENSE +21 -0
- ph_text_index-0.1.0/PKG-INFO +342 -0
- ph_text_index-0.1.0/README.md +307 -0
- ph_text_index-0.1.0/pyproject.toml +107 -0
- ph_text_index-0.1.0/src/ph_text_index/__init__.py +781 -0
- ph_text_index-0.1.0/src/ph_text_index/_chunk.py +157 -0
- ph_text_index-0.1.0/src/ph_text_index/_embed.py +221 -0
- ph_text_index-0.1.0/src/ph_text_index/_store.py +435 -0
- ph_text_index-0.1.0/src/ph_text_index/bundle.yaml +21 -0
- ph_text_index-0.1.0/src/ph_text_index/py.typed +0 -0
- ph_text_index-0.1.0/src/ph_text_index/skills/text-search/SKILL.md +68 -0
- ph_text_index-0.1.0/tests/test_text_index.py +1074 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Build and environment
|
|
2
|
+
.venv/
|
|
3
|
+
dist/
|
|
4
|
+
build/
|
|
5
|
+
*.egg-info/
|
|
6
|
+
__pycache__/
|
|
7
|
+
*.py[cod]
|
|
8
|
+
jjt/
|
|
9
|
+
w2/
|
|
10
|
+
|
|
11
|
+
# Tooling caches
|
|
12
|
+
.pytest_cache/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.ruff_cache/
|
|
15
|
+
.coverage
|
|
16
|
+
# Written by the guest's subprocess collectors and combined at the end of the
|
|
17
|
+
# run; see `conftest.GUEST_COVERAGE_RC`.
|
|
18
|
+
.coverage-guest*
|
|
19
|
+
htmlcov/
|
|
20
|
+
# Dropped at the repo root by pytest-textual-snapshot when a snapshot test
|
|
21
|
+
# fails. The reference snapshots under `__snapshots__/` are the committed
|
|
22
|
+
# expectation; this is the diff viewer for a run that did not match one.
|
|
23
|
+
snapshot_report.html
|
|
24
|
+
|
|
25
|
+
# Reference checkouts of the upstream projects this port reads from. Vendored
|
|
26
|
+
# locally so the plans' citations are verifiable; never part of this repo.
|
|
27
|
+
sources/
|
|
28
|
+
|
|
29
|
+
# Local scratch
|
|
30
|
+
.ph/
|
|
31
|
+
*.local.yaml
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Charles Tabor
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: ph-text-index
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: pH plugin: `text_index` and `text_search`, semantic retrieval over documents on a turbovec index.
|
|
5
|
+
Project-URL: Homepage, https://github.com/chastabor/pH
|
|
6
|
+
Project-URL: Repository, https://github.com/chastabor/pH
|
|
7
|
+
Project-URL: Documentation, https://github.com/chastabor/pH/blob/main/docs/README.md
|
|
8
|
+
Project-URL: Issues, https://github.com/chastabor/pH/issues
|
|
9
|
+
Author: Charles Tabor
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: agent,embeddings,llm,retrieval,semantic-search
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: MacOS
|
|
16
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
21
|
+
Classifier: Topic :: Software Development
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Requires-Dist: numpy>=1.20
|
|
25
|
+
Requires-Dist: ph-core==0.1.0
|
|
26
|
+
Requires-Dist: turbovec>=1.0
|
|
27
|
+
Provides-Extra: local
|
|
28
|
+
Requires-Dist: sentence-transformers>=3.0; extra == 'local'
|
|
29
|
+
Requires-Dist: torch>=2.2; extra == 'local'
|
|
30
|
+
Provides-Extra: nomic
|
|
31
|
+
Requires-Dist: einops>=0.7; extra == 'nomic'
|
|
32
|
+
Requires-Dist: sentence-transformers>=3.0; extra == 'nomic'
|
|
33
|
+
Requires-Dist: torch>=2.2; extra == 'nomic'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# ph-text-index
|
|
37
|
+
|
|
38
|
+
*`text_index` and `text_search`: semantic retrieval over the agent's own
|
|
39
|
+
documents, on a local [turbovec][turbovec] index.*
|
|
40
|
+
|
|
41
|
+
`grep` finds the string you typed. This finds the passage you meant — and hands
|
|
42
|
+
back the `path:start-end` it came from, so the model can answer from the passage
|
|
43
|
+
or `read` the file around it.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
phern --profile llama --provider llama --model <model> \
|
|
47
|
+
--patch '{insert: [{id: text-index, name: text-index},
|
|
48
|
+
{id: text-index-local, name: text-index-local}]}' \
|
|
49
|
+
-p "index docs/ then tell me how the workspace seam decides a containment tier"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Two rows, and why
|
|
53
|
+
|
|
54
|
+
| row | what it is |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `text-index` | the seam: `ctx.text_index`, the chunker, the turbovec store, and the two tools |
|
|
57
|
+
| `text-index-local` | the provider: a `sentence-transformers` model, loaded on first use |
|
|
58
|
+
|
|
59
|
+
The split is the shape `subprocess`/`subprocess-local` and
|
|
60
|
+
`code-runtime`/`code-runtime-python` already have in this tree, and it pays for
|
|
61
|
+
itself three times:
|
|
62
|
+
|
|
63
|
+
* a deployment with an embeddings **endpoint** — llama.cpp's `/v1/embeddings`, a
|
|
64
|
+
provider's API — writes its own row and keeps the tools;
|
|
65
|
+
* the **tests** run the real turbovec index against a deterministic stub
|
|
66
|
+
embedder, so the suite proves chunking, paging and persistence without
|
|
67
|
+
downloading a model;
|
|
68
|
+
* the row that pulls in **torch** is one a profile can leave out.
|
|
69
|
+
|
|
70
|
+
**The tools appear only once an embedder is claimed.** `TextIndexSeam.register`
|
|
71
|
+
registers them on the provider's own scope, so a profile with the seam and no
|
|
72
|
+
provider advertises nothing at all — the rule `subagent-task` states, for the
|
|
73
|
+
same reason: a tool named in every prompt and refused on every call spends
|
|
74
|
+
context teaching the model a capability the deployment does not have.
|
|
75
|
+
|
|
76
|
+
## The two tools
|
|
77
|
+
|
|
78
|
+
**`text_index(paths, glob?, forget?)`** reads each document through `ctx.fs`,
|
|
79
|
+
cuts it into passages, embeds them and stores them. Re-indexing a file replaces
|
|
80
|
+
its passages, so running it again after an edit is correct. `forget: true`
|
|
81
|
+
removes instead.
|
|
82
|
+
|
|
83
|
+
**Nothing watches the filesystem** — no daemon, no watcher. The index changes
|
|
84
|
+
when `text_index` runs and at no other moment, so run it again after edits.
|
|
85
|
+
|
|
86
|
+
That is cheap now because **git or jj is asked first** (`ph.seams.changes`): a
|
|
87
|
+
document the version control vouches for is not read, not re-cut, and — the part
|
|
88
|
+
that matters — **not re-embedded**. This loop has no content digest of its own,
|
|
89
|
+
so before the filter every call re-embedded the whole corpus: 20 s of MiniLM for
|
|
90
|
+
this repo's `docs/`, or 185 s under nomic. Which backend answers is the
|
|
91
|
+
workspace provider's to state; a tree with no version control behaves exactly as
|
|
92
|
+
it did before, which is a test rather than a hope. A document past `max_bytes` is *skipped and
|
|
93
|
+
reported*, not an error — a call over a directory should not fail because it
|
|
94
|
+
found a minified bundle, and a caller who never learns what was skipped cannot
|
|
95
|
+
tell a quiet corpus from a quiet failure.
|
|
96
|
+
|
|
97
|
+
**`text_search(query, k?, paths?)`** returns the passages with their line
|
|
98
|
+
ranges and scores. `paths` restricts the search to a subtree.
|
|
99
|
+
|
|
100
|
+
An earlier version of this paragraph claimed a filtered search costs *less* than
|
|
101
|
+
an unfiltered one, because turbovec filters inside the SIMD kernel. Measured,
|
|
102
|
+
that is wrong: the kernel does, but building the id list on the Python side
|
|
103
|
+
dominates it by roughly fifteen times (0.11 ms against a 0.22 ms unfiltered
|
|
104
|
+
search on a 10 000-chunk index, and selectivity changes nothing). Use `paths`
|
|
105
|
+
for **precision**, not for speed.
|
|
106
|
+
|
|
107
|
+
## Everything is read through `ctx.fs`
|
|
108
|
+
|
|
109
|
+
Every indexed byte arrives via `ctx.fs.read`, so `fs/read-intent` fires,
|
|
110
|
+
`permissions-fs` decides, the workspace tier bounds the path and every
|
|
111
|
+
registered screen gets its say — the same door `read` goes through, and for the
|
|
112
|
+
reason `tool-attach` insists on it (I-9). It matters more here, because
|
|
113
|
+
indexing is a *bulk* read: a tool that walked the tree with `Path.open` would be
|
|
114
|
+
an exfiltration primitive with a glob argument.
|
|
115
|
+
|
|
116
|
+
The corollary: the index is **per-deployment, not per-agent**. Two agents share
|
|
117
|
+
`$PH_CACHE/text-index/<embedder digest>` unless a profile says otherwise, so a
|
|
118
|
+
passage one indexed is retrievable by another. Right for a documentation corpus,
|
|
119
|
+
wrong for anything private — `path:` in the row's config is how you separate
|
|
120
|
+
them.
|
|
121
|
+
|
|
122
|
+
## Chunking
|
|
123
|
+
|
|
124
|
+
Paragraphs, not a fixed character stride. A stride is simpler and reliably
|
|
125
|
+
splits the one sentence that answers the query across two chunks, so neither
|
|
126
|
+
retrieves; blank lines are where the document's own author already said "new
|
|
127
|
+
idea". Every chunk carries its 1-based line span, because a chunk that does not
|
|
128
|
+
know its place in the file is a wall of prose the agent must then go and locate.
|
|
129
|
+
`overlap_chars` carries the previous chunk's tail forward, which is the
|
|
130
|
+
concession to a paragraph whose meaning depends on the one before it.
|
|
131
|
+
|
|
132
|
+
## On disk
|
|
133
|
+
|
|
134
|
+
`index.tvim` is turbovec's own format, written with `sync()` — incremental, one
|
|
135
|
+
fsync per call, crash-safe at any byte. `chunks.json` is the sidecar: path, line
|
|
136
|
+
span and the passage text.
|
|
137
|
+
|
|
138
|
+
**The sidecar holds the text on purpose.** Storing only pointers would keep the
|
|
139
|
+
file small and make every hit a second tool call before the model learns whether
|
|
140
|
+
the hit was any good — answered from a file that may have changed since it was
|
|
141
|
+
indexed. It is also the ceiling: the sidecar is rewritten whole on every save,
|
|
142
|
+
which is fine for a corpus of documents and wrong for millions.
|
|
143
|
+
|
|
144
|
+
A crash between the two writes can leave them diverged, so vectors are committed
|
|
145
|
+
first and `open` reconciles by trusting the sidecar and dropping any id the index
|
|
146
|
+
cannot answer for — a chunk nobody can retrieve is invisible, where a vector
|
|
147
|
+
with no text would surface as a hit this row could not describe.
|
|
148
|
+
|
|
149
|
+
### Calibration
|
|
150
|
+
|
|
151
|
+
turbovec's TQ+ calibration is worth 2.5 to 8.7 points of R@10, and upstream is
|
|
152
|
+
emphatic about the one way to get it wrong: the sample must be a uniform random,
|
|
153
|
+
representative draw of what the index will hold, and a clustered prefix "fits a
|
|
154
|
+
calibration that actively destroys recall". An incremental indexer has no such
|
|
155
|
+
draw at the moment it would have to commit one — the first document is the most
|
|
156
|
+
clustered prefix there is.
|
|
157
|
+
|
|
158
|
+
So it is committed in exactly the situation upstream describes: the index is
|
|
159
|
+
**empty** and the incoming batch is large enough (1 024 rows) to sample from,
|
|
160
|
+
in which case a random sample of that batch *is* a representative draw and
|
|
161
|
+
calibrating before the add is the documented order. Otherwise the index stays
|
|
162
|
+
uncalibrated, which is plain TurboQuant — good rather than wrong. `calibrate:
|
|
163
|
+
false` turns even that off.
|
|
164
|
+
|
|
165
|
+
## Using it from the RLM
|
|
166
|
+
|
|
167
|
+
Nothing to do: under Code Mode every registered tool is in the generated SDK
|
|
168
|
+
listing, so these arrive as `await tools.text_index(...)` and
|
|
169
|
+
`await tools.text_search(...)` with no work from this package. The
|
|
170
|
+
[`rlm-indexed`](../ph-app/src/ph_app/profiles.py) profile is `rlm-stable` plus
|
|
171
|
+
this bundle and `ph-code-graph`'s:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
phern --profile rlm-indexed --provider llama --model <model> --mode tui
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
This package registers a `ph.bundles` entry point so that profile is *hidden*
|
|
178
|
+
on an install without this distribution rather than offered and then failing at
|
|
179
|
+
mount — `available_profiles()` gates on bundle resolution, and the refusal names
|
|
180
|
+
the package to install.
|
|
181
|
+
|
|
182
|
+
The bundle carries **both** rows, and that matters: the seam registers no tools
|
|
183
|
+
until an embedder is claimed, so a bundle shipping only `text-index` would mount
|
|
184
|
+
a service and advertise nothing, with no error anywhere. A deployment bringing
|
|
185
|
+
its own embedder disables `text-index-local` and mounts its own provider row.
|
|
186
|
+
|
|
187
|
+
## Installing
|
|
188
|
+
|
|
189
|
+
`sentence-transformers` is a **hard dependency**, so installing this package is
|
|
190
|
+
how a deployment gets a working index. That is most of a gigabyte of torch, in
|
|
191
|
+
every environment that resolves the package, CI included. Moving it to an
|
|
192
|
+
extra is one line in `pyproject.toml`; the cost of that is that
|
|
193
|
+
`pip install ph-text-index` no longer gives you something that runs. The
|
|
194
|
+
seam/provider split keeps the choice cheap either way — nothing but
|
|
195
|
+
`text-index-local` imports it.
|
|
196
|
+
|
|
197
|
+
## Switching models
|
|
198
|
+
|
|
199
|
+
The default is **`sentence-transformers/all-MiniLM-L6-v2`** — 384-dimensional,
|
|
200
|
+
about 90 MB, symmetric so it needs no prefixes, and fast enough on a CPU that
|
|
201
|
+
indexing a documentation tree is a coffee break. It is the *small* choice, not
|
|
202
|
+
the best one.
|
|
203
|
+
|
|
204
|
+
The index directory is keyed by a digest of the embedder's identity — model
|
|
205
|
+
name **and** both prefixes, because all three move the vector space — so
|
|
206
|
+
switching is safe to try: a new model gets its own index and switching back
|
|
207
|
+
finds the old one intact. Pointing `path:` at a fixed directory and then
|
|
208
|
+
changing the model raises `IndexMismatch` rather than returning neighbours
|
|
209
|
+
computed in a space nothing shares.
|
|
210
|
+
|
|
211
|
+
### A worked upgrade: `nomic-embed-text-v1.5` — measured
|
|
212
|
+
|
|
213
|
+
Run against this repository's `docs/seams` (30 documents, 235 passages), same
|
|
214
|
+
queries, same chunking:
|
|
215
|
+
|
|
216
|
+
| | MiniLM-L6-v2 | nomic-embed-text-v1.5 |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| dimensions | 384 | 768 |
|
|
219
|
+
| load, cold | 19 s | 26 s |
|
|
220
|
+
| index 235 passages | **5.4 s** | 41.7 s |
|
|
221
|
+
| *"stop an agent writing in my own checkout"* | workspace.md (0.319) | workspace.md (**0.599**) |
|
|
222
|
+
| *"what confines a shell command on linux"* | shell.md (0.529) | shell.md (**0.753**) |
|
|
223
|
+
|
|
224
|
+
On a harder corpus — all 57 files of `docs/`, 539 passages, eight paraphrase
|
|
225
|
+
queries scored on whether the top hit was the right *file* — nomic got **3/8**
|
|
226
|
+
against MiniLM's **1/8**, at 185 s of indexing against 20 s.
|
|
227
|
+
|
|
228
|
+
Read that honestly in both directions. nomic retrieves better and separates hits
|
|
229
|
+
far more confidently; it also costs roughly **8× the indexing time**, and 3/8 is
|
|
230
|
+
not good retrieval in absolute terms. Some of those misses are the scoring being
|
|
231
|
+
crude (a filename substring), but not all of them: a corpus of long design notes
|
|
232
|
+
is genuinely hard, and *narrowing the corpus* helped more than changing the model
|
|
233
|
+
— MiniLM found `workspace.md` over `docs/seams` and missed it over all of
|
|
234
|
+
`docs/`. Use `paths=` before reaching for a bigger model.
|
|
235
|
+
|
|
236
|
+
**MiniLM stays the default** for that reason: the cheap thing is good enough for
|
|
237
|
+
a scoped corpus, and the expensive thing does not rescue an unscoped one.
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
768-dimensional, 8192-token context, materially better retrieval than MiniLM.
|
|
242
|
+
Three things it needs, and each is a field rather than a special case:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
pip install 'ph-text-index[nomic]' # einops, which its remote code imports
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
```yaml
|
|
249
|
+
- id: text-index-local
|
|
250
|
+
config:
|
|
251
|
+
model: nomic-ai/nomic-embed-text-v1.5
|
|
252
|
+
# Asymmetric: it wants to know whether it is embedding a question or a
|
|
253
|
+
# passage. Both prefixes are part of the index identity, so this gets its
|
|
254
|
+
# own directory automatically.
|
|
255
|
+
queryPrefix: "search_query: "
|
|
256
|
+
documentPrefix: "search_document: "
|
|
257
|
+
# Its config.json carries an `auto_map` pointing at nomic-ai/nomic-bert-2048,
|
|
258
|
+
# so sentence-transformers will not load it without this. Read the next
|
|
259
|
+
# paragraph before setting it.
|
|
260
|
+
trustRemoteCode: true
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**`trustRemoteCode` is a trust decision, not a compatibility flag.** It
|
|
264
|
+
downloads Python from the model's repository — here from a *second* repository
|
|
265
|
+
via `auto_map` — and executes it in the harness process. It is off by default
|
|
266
|
+
and nothing infers it from a load failure, because "retry with arbitrary code
|
|
267
|
+
execution enabled" is not a fallback. It is deliberately *not* part of the index
|
|
268
|
+
identity: it governs what may load, not where a vector lands, so granting or
|
|
269
|
+
revoking it does not invalidate an index.
|
|
270
|
+
|
|
271
|
+
A model whose weights are safetensors and whose architecture `transformers`
|
|
272
|
+
already knows needs none of this — `BAAI/bge-base-en-v1.5` and the `e5` family
|
|
273
|
+
are drop-in with prefixes alone.
|
|
274
|
+
|
|
275
|
+
**And the `[nomic]` extra is not optional politeness.** Without `einops` the
|
|
276
|
+
model downloads its weights *and* its remote code successfully and then dies at
|
|
277
|
+
import:
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
ImportError: This modeling file requires the following packages that were not
|
|
281
|
+
found in your environment: einops
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A missing transitive dependency that only the model's own code knows about, and
|
|
285
|
+
that no amount of pre-downloading would have caught. It is the reason
|
|
286
|
+
provisioning **loads** the model rather than fetching it — see below.
|
|
287
|
+
|
|
288
|
+
## Where the weights live, and installing on purpose
|
|
289
|
+
|
|
290
|
+
`$PH_CACHE/models`, set by the row (`cache:` overrides it). Left to
|
|
291
|
+
`sentence-transformers` they would go to `$HF_HOME` or `~/.cache/huggingface` —
|
|
292
|
+
outside all three of pH's roots, so a gigabyte of weights would sit somewhere
|
|
293
|
+
`phern doctor` never mentions and `rm -rf $PH_CACHE` would not reclaim. Rebuildable
|
|
294
|
+
and large is the lifecycle `$PH_CACHE` names (Q1), and the runtime venv is there
|
|
295
|
+
for the same reason.
|
|
296
|
+
|
|
297
|
+
**Provision before the agent needs it**, three ways, in increasing order of
|
|
298
|
+
"nobody is watching":
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
/text-index status # is the model ready? costs nothing, downloads nothing
|
|
302
|
+
/text-index install # fetch and load it now, and say what happened
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
A **command**, not a tool: a person asks the harness to do this, and routing it
|
|
306
|
+
through a model turn would put the model in the log as having decided it. It
|
|
307
|
+
costs no turn. `phern doctor` answers the same question without mounting an agent —
|
|
308
|
+
the section reports `model: not loaded — /text-index install`.
|
|
309
|
+
|
|
310
|
+
For an **unattended** run — a daemon, a scheduled tick, `phern -p` in CI — there is
|
|
311
|
+
nobody to type either:
|
|
312
|
+
|
|
313
|
+
```yaml
|
|
314
|
+
- id: text-index-local
|
|
315
|
+
config:
|
|
316
|
+
preload: true
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
That loads the model at **mount** and refuses the mount with a `MountRefusal`
|
|
320
|
+
sentence if it cannot, which is the cookbook's own rule: refuse at mount, not at
|
|
321
|
+
first use, because by then the agent is running and "refuse to start" has already
|
|
322
|
+
been disobeyed. Off by default, because a person at a TUI would rather the
|
|
323
|
+
harness start now and pay for the model when they use it.
|
|
324
|
+
|
|
325
|
+
All three call the same thing, and it **loads** the model rather than fetching
|
|
326
|
+
it — the `einops` failure above is exactly why. Whatever upstream says on
|
|
327
|
+
failure is passed through verbatim, because that sentence names the fix.
|
|
328
|
+
|
|
329
|
+
## The skill
|
|
330
|
+
|
|
331
|
+
The package installs a `text-search` skill, so an RLM under progressive
|
|
332
|
+
disclosure sees one line in its catalog and can read the page when it needs
|
|
333
|
+
it — how to scope a search, that a score is not a confidence, that a passage can
|
|
334
|
+
be an argument the document goes on to reject.
|
|
335
|
+
|
|
336
|
+
Registered by the row rather than found by a directory scan, so it arrives
|
|
337
|
+
exactly when the tools do and leaves with them: `skills-progressive` ships an
|
|
338
|
+
empty `paths` on purpose, because scanning a well-known directory would make
|
|
339
|
+
"install a skill" mean "drop a file somewhere", and a skill is something a
|
|
340
|
+
distribution installs deliberately (I7).
|
|
341
|
+
|
|
342
|
+
[turbovec]: https://pypi.org/project/turbovec/
|