@retinue/agentkit 0.1.0 → 0.3.0

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 (246) hide show
  1. package/README.md +59 -277
  2. package/dist/adapters/audio/openai.d.ts +36 -0
  3. package/dist/adapters/audio/openai.js +136 -0
  4. package/dist/adapters/bullmq/lock.d.ts +26 -2
  5. package/dist/adapters/embeddings/openai.d.ts +45 -0
  6. package/dist/adapters/embeddings/openai.js +109 -0
  7. package/dist/adapters/memory/connections.d.ts +10 -0
  8. package/dist/adapters/memory/connections.js +87 -0
  9. package/dist/adapters/memory/graph.d.ts +17 -0
  10. package/dist/adapters/memory/graph.js +267 -0
  11. package/dist/adapters/memory/index.d.ts +3 -0
  12. package/dist/adapters/memory/index.js +3 -0
  13. package/dist/adapters/memory/rate-limit.d.ts +14 -0
  14. package/dist/adapters/memory/rate-limit.js +28 -0
  15. package/dist/adapters/memory/runtime.js +0 -0
  16. package/dist/adapters/postgres/connections.d.ts +15 -0
  17. package/dist/adapters/postgres/connections.js +134 -0
  18. package/dist/adapters/postgres/graph.d.ts +24 -0
  19. package/dist/adapters/postgres/graph.js +325 -0
  20. package/dist/adapters/postgres/index.d.ts +2 -0
  21. package/dist/adapters/postgres/index.js +2 -0
  22. package/dist/adapters/postgres/migrations.js +198 -0
  23. package/dist/adapters/postgres/run-store.js +26 -2
  24. package/dist/adapters/postgres/schema.d.ts +61 -2
  25. package/dist/adapters/postgres/schema.js +95 -10
  26. package/dist/adapters/redis/index.d.ts +1 -0
  27. package/dist/adapters/redis/index.js +1 -0
  28. package/dist/adapters/redis/rate-limit.d.ts +46 -0
  29. package/dist/adapters/redis/rate-limit.js +55 -0
  30. package/dist/adapters/supabase/index.d.ts +15 -0
  31. package/dist/adapters/supabase/index.js +15 -0
  32. package/dist/adapters/supabase/rls.js +33 -0
  33. package/dist/agents/agent.d.ts +99 -1
  34. package/dist/agents/agent.js +215 -16
  35. package/dist/agents/define.js +24 -4
  36. package/dist/agents/engine.d.ts +48 -0
  37. package/dist/agents/engine.js +370 -13
  38. package/dist/audio/index.d.ts +151 -0
  39. package/dist/audio/index.js +183 -0
  40. package/dist/capabilities/index.d.ts +5 -1
  41. package/dist/capabilities/index.js +23 -0
  42. package/dist/capabilities/runtime.d.ts +8 -0
  43. package/dist/connections/cipher.d.ts +103 -0
  44. package/dist/connections/cipher.js +141 -0
  45. package/dist/connections/index.d.ts +156 -0
  46. package/dist/connections/index.js +72 -0
  47. package/dist/connections/oauth/client.d.ts +84 -0
  48. package/dist/connections/oauth/client.js +141 -0
  49. package/dist/connections/oauth/index.d.ts +166 -0
  50. package/dist/connections/oauth/index.js +226 -0
  51. package/dist/connections/oauth/service.d.ts +90 -0
  52. package/dist/connections/oauth/service.js +132 -0
  53. package/dist/connections/pause.d.ts +68 -0
  54. package/dist/connections/pause.js +97 -0
  55. package/dist/connections/resolver.d.ts +59 -0
  56. package/dist/connections/resolver.js +138 -0
  57. package/dist/connections/resume.d.ts +64 -0
  58. package/dist/connections/resume.js +99 -0
  59. package/dist/connections/vault-cipher.d.ts +95 -0
  60. package/dist/connections/vault-cipher.js +206 -0
  61. package/dist/core/budget.d.ts +55 -0
  62. package/dist/core/budget.js +56 -0
  63. package/dist/core/content-parts.d.ts +26 -2
  64. package/dist/core/content-parts.js +1 -0
  65. package/dist/core/context.d.ts +25 -0
  66. package/dist/core/errors.d.ts +1 -1
  67. package/dist/core/errors.js +15 -0
  68. package/dist/core/events.d.ts +110 -2
  69. package/dist/core/events.js +33 -2
  70. package/dist/core/index.d.ts +1 -0
  71. package/dist/core/index.js +1 -0
  72. package/dist/core/validation.js +19 -0
  73. package/dist/documents/index.d.ts +14 -0
  74. package/dist/documents/parsers/text.d.ts +16 -0
  75. package/dist/documents/parsers/text.js +54 -2
  76. package/dist/entries/connections.d.ts +10 -0
  77. package/dist/entries/connections.js +10 -0
  78. package/dist/entries/guardrails.d.ts +14 -0
  79. package/dist/entries/guardrails.js +14 -0
  80. package/dist/entries/knowledge.d.ts +17 -0
  81. package/dist/entries/knowledge.js +16 -0
  82. package/dist/entries/mcp-server.d.ts +9 -0
  83. package/dist/entries/mcp-server.js +9 -0
  84. package/dist/entries/runtime.d.ts +10 -0
  85. package/dist/entries/runtime.js +8 -0
  86. package/dist/entries/testing.d.ts +28 -0
  87. package/dist/entries/testing.js +28 -0
  88. package/dist/graphql/resolvers.d.ts +7 -1
  89. package/dist/graphql/resolvers.js +20 -0
  90. package/dist/graphql/schema.d.ts +1 -1
  91. package/dist/graphql/schema.js +44 -0
  92. package/dist/guardrails/index.d.ts +115 -0
  93. package/dist/guardrails/index.js +108 -0
  94. package/dist/guardrails/moderation.d.ts +53 -0
  95. package/dist/guardrails/moderation.js +75 -0
  96. package/dist/guardrails/pii.d.ts +75 -0
  97. package/dist/guardrails/pii.js +193 -0
  98. package/dist/knowledge/communities.d.ts +166 -0
  99. package/dist/knowledge/communities.js +377 -0
  100. package/dist/knowledge/graph-global.d.ts +153 -0
  101. package/dist/knowledge/graph-global.js +246 -0
  102. package/dist/knowledge/graph-retrieval.d.ts +150 -0
  103. package/dist/knowledge/graph-retrieval.js +303 -0
  104. package/dist/knowledge/graph.d.ts +223 -0
  105. package/dist/knowledge/graph.js +369 -0
  106. package/dist/knowledge/index.d.ts +25 -0
  107. package/dist/knowledge/index.js +37 -1
  108. package/dist/knowledge/navigate.d.ts +89 -0
  109. package/dist/knowledge/navigate.js +107 -0
  110. package/dist/knowledge/retrieval.d.ts +98 -5
  111. package/dist/knowledge/retrieval.js +158 -28
  112. package/dist/mcp-server/index.d.ts +168 -0
  113. package/dist/mcp-server/index.js +175 -0
  114. package/dist/models/extraction.d.ts +73 -0
  115. package/dist/models/extraction.js +118 -0
  116. package/dist/models/index.d.ts +40 -1
  117. package/dist/models/index.js +22 -1
  118. package/dist/models/pricing.d.ts +2 -0
  119. package/dist/models/pricing.js +17 -1
  120. package/dist/models/provider-factory.js +9 -7
  121. package/dist/models/streaming.d.ts +111 -1
  122. package/dist/models/streaming.js +184 -3
  123. package/dist/persistence/index.d.ts +230 -0
  124. package/dist/persistence/index.js +2 -0
  125. package/dist/runtime/index.d.ts +1 -1
  126. package/dist/runtime/index.js +12 -0
  127. package/dist/runtime/worker.d.ts +36 -1
  128. package/dist/runtime/worker.js +37 -4
  129. package/dist/security/checklist.js +55 -0
  130. package/dist/security/findings.js +18 -9
  131. package/dist/server/bin.d.ts +19 -0
  132. package/dist/server/bin.js +226 -0
  133. package/dist/server/boot.d.ts +20 -2
  134. package/dist/server/boot.js +29 -3
  135. package/dist/server/cli.d.ts +13 -0
  136. package/dist/server/cli.js +7 -4
  137. package/dist/server/doctor.d.ts +74 -0
  138. package/dist/server/doctor.js +280 -0
  139. package/dist/skills/catalogue.d.ts +49 -0
  140. package/dist/skills/catalogue.js +61 -0
  141. package/dist/skills/context.d.ts +61 -0
  142. package/dist/skills/context.js +107 -0
  143. package/dist/skills/index.d.ts +2 -0
  144. package/dist/skills/index.js +2 -0
  145. package/dist/telemetry/spans.js +17 -0
  146. package/dist/testing/conformance/artifact-exports.d.ts +20 -0
  147. package/dist/testing/conformance/artifact-exports.js +172 -0
  148. package/dist/testing/conformance/artifacts.d.ts +11 -0
  149. package/dist/testing/conformance/artifacts.js +316 -0
  150. package/dist/testing/conformance/audio.d.ts +38 -0
  151. package/dist/testing/conformance/audio.js +145 -0
  152. package/dist/testing/conformance/capability.d.ts +23 -0
  153. package/dist/testing/conformance/capability.js +27 -0
  154. package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
  155. package/dist/testing/conformance/checkpoint-store.js +72 -0
  156. package/dist/testing/conformance/connections.d.ts +14 -0
  157. package/dist/testing/conformance/connections.js +171 -0
  158. package/dist/testing/conformance/conversation-store.d.ts +10 -0
  159. package/dist/testing/conformance/conversation-store.js +60 -0
  160. package/dist/testing/conformance/evaluation.d.ts +10 -0
  161. package/dist/testing/conformance/evaluation.js +295 -0
  162. package/dist/testing/conformance/files.d.ts +19 -0
  163. package/dist/testing/conformance/files.js +454 -0
  164. package/dist/testing/conformance/flows.d.ts +16 -0
  165. package/dist/testing/conformance/flows.js +193 -0
  166. package/dist/testing/conformance/graph.d.ts +22 -0
  167. package/dist/testing/conformance/graph.js +500 -0
  168. package/dist/testing/conformance/hitl.d.ts +25 -0
  169. package/dist/testing/conformance/hitl.js +523 -0
  170. package/dist/testing/conformance/index.d.ts +163 -0
  171. package/dist/testing/conformance/index.js +321 -0
  172. package/dist/testing/conformance/invariants.d.ts +23 -0
  173. package/dist/testing/conformance/invariants.js +80 -0
  174. package/dist/testing/conformance/knowledge.d.ts +41 -0
  175. package/dist/testing/conformance/knowledge.js +592 -0
  176. package/dist/testing/conformance/parents.d.ts +50 -0
  177. package/dist/testing/conformance/parents.js +39 -0
  178. package/dist/testing/conformance/rate-limit.d.ts +25 -0
  179. package/dist/testing/conformance/rate-limit.js +71 -0
  180. package/dist/testing/conformance/records.d.ts +36 -0
  181. package/dist/testing/conformance/records.js +400 -0
  182. package/dist/testing/conformance/rollups.d.ts +30 -0
  183. package/dist/testing/conformance/rollups.js +425 -0
  184. package/dist/testing/conformance/run-coordinator.d.ts +27 -0
  185. package/dist/testing/conformance/run-coordinator.js +150 -0
  186. package/dist/testing/conformance/run-event-log.d.ts +9 -0
  187. package/dist/testing/conformance/run-event-log.js +138 -0
  188. package/dist/testing/conformance/run-store.d.ts +12 -0
  189. package/dist/testing/conformance/run-store.js +294 -0
  190. package/dist/testing/conformance/session-state.d.ts +38 -0
  191. package/dist/testing/conformance/session-state.js +198 -0
  192. package/dist/testing/conformance/usage-limits.d.ts +10 -0
  193. package/dist/testing/conformance/usage-limits.js +309 -0
  194. package/dist/testing/conformance.d.ts +8 -0
  195. package/dist/testing/conformance.js +8 -0
  196. package/dist/testing/memory-backend.d.ts +39 -0
  197. package/dist/testing/memory-backend.js +41 -0
  198. package/dist/testing/pglite.d.ts +55 -0
  199. package/dist/testing/pglite.js +78 -0
  200. package/dist/testing/stub-model.d.ts +58 -0
  201. package/dist/testing/stub-model.js +71 -0
  202. package/dist/testing/supabase-storage-double.d.ts +35 -0
  203. package/dist/testing/supabase-storage-double.js +101 -0
  204. package/dist/toolkit/files.d.ts +125 -0
  205. package/dist/toolkit/files.js +320 -0
  206. package/dist/toolkit/http.d.ts +27 -0
  207. package/dist/toolkit/http.js +48 -0
  208. package/dist/toolkit/index.d.ts +31 -1
  209. package/dist/toolkit/index.js +26 -1
  210. package/dist/toolkit/mime.d.ts +97 -0
  211. package/dist/toolkit/mime.js +233 -0
  212. package/dist/toolkit/sandbox.d.ts +119 -0
  213. package/dist/toolkit/sandbox.js +239 -0
  214. package/dist/toolkit/ssrf.d.ts +139 -0
  215. package/dist/toolkit/ssrf.js +349 -0
  216. package/dist/toolkit/vendor.d.ts +91 -0
  217. package/dist/toolkit/vendor.js +153 -0
  218. package/dist/toolkit/web.d.ts +13 -0
  219. package/dist/toolkit/web.js +7 -1
  220. package/dist/tools/budget.d.ts +28 -0
  221. package/dist/tools/budget.js +35 -0
  222. package/dist/tools/credentials.d.ts +316 -0
  223. package/dist/tools/credentials.js +369 -0
  224. package/dist/tools/define.d.ts +45 -0
  225. package/dist/tools/define.js +26 -0
  226. package/dist/tools/find.d.ts +109 -0
  227. package/dist/tools/find.js +210 -0
  228. package/dist/tools/index.d.ts +35 -2
  229. package/dist/tools/index.js +11 -0
  230. package/dist/tools/library/audio.d.ts +63 -0
  231. package/dist/tools/library/audio.js +148 -0
  232. package/dist/tools/library/fs.d.ts +24 -0
  233. package/dist/tools/library/fs.js +102 -0
  234. package/dist/tools/library/index.d.ts +37 -2
  235. package/dist/tools/library/index.js +47 -0
  236. package/dist/tools/library/shell.d.ts +45 -0
  237. package/dist/tools/library/shell.js +70 -0
  238. package/dist/tools/meta-tools.js +8 -0
  239. package/dist/tools/registry.d.ts +146 -0
  240. package/dist/tools/registry.js +250 -4
  241. package/dist/usage/index.d.ts +21 -0
  242. package/dist/usage/index.js +1 -0
  243. package/dist/usage/rate-limit.d.ts +122 -0
  244. package/dist/usage/rate-limit.js +131 -0
  245. package/dist/usage/recorder.js +3 -0
  246. package/package.json +33 -2
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Retrieval without vectors — REQ-050 (#209), task #219, AC-4.
3
+ *
4
+ * A **spike**, and the deliverable is a decision with numbers rather than a subsystem. See
5
+ * `docs/26-retrieval-quality.md` for what it scored.
6
+ *
7
+ * ## The idea being tested
8
+ *
9
+ * Embedding-based retrieval matches a query against fragments of text and hopes the fragments it surfaces are the
10
+ * ones that answer it. A person looking something up in a manual does something else entirely: they read the
11
+ * table of contents, decide which chapter is relevant, and then read it. That needs no index, no embedding cost
12
+ * and no re-indexing when a document changes — and its citations name a *document* somebody chose rather than a
13
+ * fragment a cosine distance surfaced.
14
+ *
15
+ * The cost is a model call per query, and latency measured in seconds rather than milliseconds.
16
+ *
17
+ * ## Two things this prototype found immediately
18
+ *
19
+ * **`KnowledgeStore` cannot enumerate its sources.** There is `listBySource`, `get`, `deleteSource` and
20
+ * `staleSources`, and no way to ask "what documents are in here". That is correct for a vector-based design —
21
+ * nothing needed it — and it is exactly what a navigating retriever needs first. So the outline arrives through
22
+ * a port the *host* supplies (`OutlineCatalogue`), which is honest but means this mode is not a drop-in for a
23
+ * deployment that already has hybrid retrieval working.
24
+ *
25
+ * **It does fit behind `RetrievalMode`**, which the issue asked to test. `createRetriever` gains one optional
26
+ * dependency and a fourth mode; every caller — `search_knowledge` included — is unchanged, and a deployment that
27
+ * has not wired a navigator gets a named refusal rather than a silent fall back to semantic search. If it had
28
+ * *not* fit, that would have been the finding; it fits.
29
+ */
30
+ export const DEFAULT_MAX_SOURCES = 3;
31
+ export const DEFAULT_MAX_CHUNKS_PER_SOURCE = 40;
32
+ const reference = (chunk) => ({
33
+ sourceType: chunk.sourceType,
34
+ sourceId: chunk.sourceId,
35
+ chunkIndex: chunk.chunkIndex,
36
+ chunkId: chunk.id,
37
+ ...(chunk.locator === undefined ? {} : { locator: chunk.locator }),
38
+ });
39
+ /**
40
+ * Query terms, for ordering chunks *within* the documents the chooser picked.
41
+ *
42
+ * Six lines of tokenisation rather than a second ranker: there is no fusion here and no relevance floor, because
43
+ * the relevance decision was already made — by a model, over titles and headings. What remains is "which part of
44
+ * this chapter", and term overlap answers that well enough to measure. If the eval had shown this mode worth
45
+ * shipping, this is the first thing to replace.
46
+ */
47
+ const terms = (text) => [
48
+ ...new Set(text.toLowerCase().split(/[^a-z0-9]+/).filter((word) => word.length > 2)),
49
+ ];
50
+ export const createNavigator = (deps) => {
51
+ const maxSources = deps.maxSources ?? DEFAULT_MAX_SOURCES;
52
+ const maxChunks = deps.maxChunksPerSource ?? DEFAULT_MAX_CHUNKS_PER_SOURCE;
53
+ return {
54
+ id: `navigate:${deps.chooser.id}`,
55
+ async navigate(context, input) {
56
+ if (input.authSubjects.length === 0)
57
+ return { found: false, reason: "no-access", message: "There is no material you have access to.", mode: "navigate" };
58
+ const catalogue = await deps.catalogue.list({ tenantId: context.tenantId, authSubjects: input.authSubjects });
59
+ if (catalogue.length === 0)
60
+ return { found: false, reason: "nothing-indexed", message: "There is no indexed material to search yet.", mode: "navigate" };
61
+ const chosen = await deps.chooser.choose({ query: input.query, catalogue, limit: maxSources });
62
+ /**
63
+ * An empty choice is a real answer, and a distinct one.
64
+ *
65
+ * A model that has read the table of contents and concluded nothing there is relevant has told you
66
+ * something a cosine distance cannot: `no-match` rather than the least-bad chapter. This is the mode's most
67
+ * attractive property and the reason it is worth measuring at all.
68
+ */
69
+ if (chosen.length === 0)
70
+ return { found: false, reason: "no-match", message: "Nothing in the available material covers that.", mode: "navigate" };
71
+ const known = new Map(catalogue.map((outline) => [outline.sourceId, outline]));
72
+ const wanted = input.query.toLowerCase();
73
+ const queryTerms = terms(input.query);
74
+ const hits = [];
75
+ for (const sourceId of chosen.slice(0, maxSources)) {
76
+ const outline = known.get(sourceId);
77
+ // A chooser naming a document that is not in the catalogue it was given is a chooser that hallucinated
78
+ // one. Skipped rather than fetched: fetching would be a model choosing which document to read.
79
+ if (outline === undefined)
80
+ continue;
81
+ const page = await deps.store.listBySource({
82
+ tenantId: context.tenantId,
83
+ sourceType: outline.sourceType,
84
+ sourceId,
85
+ limit: maxChunks,
86
+ });
87
+ for (const chunk of page.items) {
88
+ const content = chunk.content.toLowerCase();
89
+ const overlap = queryTerms.filter((term) => content.includes(term)).length;
90
+ hits.push({
91
+ chunk,
92
+ // Not comparable with a fused score, and deliberately so: this number orders chunks inside a chosen
93
+ // document and means nothing outside one.
94
+ score: queryTerms.length === 0 ? 0 : overlap / queryTerms.length + (content.includes(wanted) ? 1 : 0),
95
+ signals: ["navigate"],
96
+ reference: reference(chunk),
97
+ });
98
+ }
99
+ }
100
+ if (hits.length === 0)
101
+ return { found: false, reason: "no-match", message: "The chosen documents had no readable content.", mode: "navigate" };
102
+ const ordered = [...hits].sort((a, b) => b.score !== a.score ? b.score - a.score : a.reference.chunkId.localeCompare(b.reference.chunkId));
103
+ return { found: true, hits: ordered.slice(0, input.limit), mode: "navigate" };
104
+ },
105
+ };
106
+ };
107
+ //# sourceMappingURL=navigate.js.map
@@ -14,9 +14,19 @@
14
14
  *
15
15
  * score(d) = Σ over signals of 1 / (K + rank(d))
16
16
  *
17
- * A document ranked first by one signal and absent from the other still beats one ranked fifth by both, which
18
- * is the behaviour that makes hybrid better than either — the exact-term hit surfaces even though the semantic
19
- * signal never saw it.
17
+ * A document ranked first by one signal and absent from the other still beats one ranked fifth by both, which is
18
+ * the behaviour hybrid exists for — the exact-term hit surfaces even though the semantic signal never saw it.
19
+ *
20
+ * **And on one real corpus it is worse than semantic alone.** This comment used to say hybrid "measurably beats"
21
+ * both parts; #219 measured it over 56 documents of technical prose and hybrid lost 11.1 points of success@5 to
22
+ * semantic-only — two cases out of eighteen. The mechanism is the same paragraph read the other way: RRF weights both signals equally by
23
+ * construction, so fusing a weak list with a strong one demotes the strong list's top hits wherever the weak one
24
+ * disagrees. The lexical signal is weak on natural-language questions over prose.
25
+ *
26
+ * Hybrid remains the default, deliberately — that dataset has 18 queries, one author, and **no identifier
27
+ * queries**, which is precisely the case hybrid exists for. But the claim in this comment was untested for months
28
+ * and turned out to be false where it was finally tested, so it is stated with its evidence now. See
29
+ * `docs/26-retrieval-quality.md`.
20
30
  *
21
31
  * **`K = 60`** is the value from Cormack, Clarke and Buettcher's original TREC work and the one every
22
32
  * implementation since has used. It is large relative to the ranks that matter, which flattens the difference
@@ -30,6 +40,9 @@
30
40
  import type { TenantId } from "../core/ids.js";
31
41
  import type { KeywordIndex, KnowledgeChunk, KnowledgeSourceType, VectorIndex } from "../persistence/index.js";
32
42
  import type { EmbeddingProvider } from "./index.js";
43
+ import type { Navigator } from "./navigate.js";
44
+ import type { GraphLocalSearch } from "./graph-retrieval.js";
45
+ import type { GraphGlobalSearch } from "./graph-global.js";
33
46
  /** The rank-fusion constant. See the note above on why 60 and why rank rather than score. */
34
47
  export declare const RRF_K = 60;
35
48
  /** How many candidates each signal contributes before fusion. */
@@ -42,7 +55,25 @@ export declare const DEFAULT_CANDIDATES = 20;
42
55
  * second-best answer and tight enough to reject a corpus that simply has nothing to say.
43
56
  */
44
57
  export declare const DEFAULT_RELEVANCE_FLOOR = 0.4;
45
- export type RetrievalMode = "semantic" | "keyword" | "hybrid";
58
+ /**
59
+ * `navigate` is a **spike** — REQ-050 (#209), task #219, AC-4.
60
+ *
61
+ * The issue asked whether retrieval without vectors can be expressed as a fourth mode behind this interface, and
62
+ * "if it cannot, that is itself the finding". It can: `createRetriever` gains one optional dependency, and every
63
+ * caller — `search_knowledge` included — is unchanged. A deployment that has not wired a navigator gets a named
64
+ * refusal rather than a silent fall back to semantic search, which is the failure that would have made the mode
65
+ * dangerous rather than merely unused.
66
+ *
67
+ * See `navigate.ts` for what it is and `docs/26-retrieval-quality.md` for what it scored.
68
+ */
69
+ /**
70
+ * `graph-local` joins the union in REQ-064 (#270), task #273.
71
+ *
72
+ * A closed union read in several places, so a new member is threaded rather than cast past — the same
73
+ * discipline #219 applied when it added `not-configured`. Like `navigate`, it is *delegated whole*: it shares
74
+ * no step with the fusion path, because it answers a different kind of question.
75
+ */
76
+ export type RetrievalMode = "semantic" | "keyword" | "hybrid" | "navigate" | "graph-local" | "graph-global";
46
77
  /** What a citation needs, derived from a hit so there is one shape rather than each caller's own (AC-6). */
47
78
  export type SourceReference = {
48
79
  readonly sourceType: KnowledgeSourceType;
@@ -67,7 +98,7 @@ export type RetrievalHit = {
67
98
  * different answer from "you have no indexed documents", and telling a user the first when the second is true
68
99
  * sends them looking for content they never uploaded.
69
100
  */
70
- export declare const NO_RESULT_REASONS: readonly ["nothing-indexed", "no-match", "below-threshold", "no-access"];
101
+ export declare const NO_RESULT_REASONS: readonly ["nothing-indexed", "no-match", "below-threshold", "no-access", "not-configured"];
71
102
  export type NoResultReason = (typeof NO_RESULT_REASONS)[number];
72
103
  export type RetrievalOutcome = {
73
104
  readonly found: true;
@@ -85,6 +116,14 @@ export type RetrievalOutcome = {
85
116
  * A port, and **switchable**, because a reranker's value is a claim that has to be provable. A cross-encoder is
86
117
  * materially more expensive than the retrieval it reorders, so "we rerank" without a measured contribution is
87
118
  * a cost nobody justified. Absent means fusion order stands, which is the honest default.
119
+ *
120
+ * **`createExactTermReranker`'s measured contribution is negative** — #219, over 56 documents of technical prose:
121
+ * −5.6 points of success@5, −5.6 of recall, −0.028 MRR, and no latency saving. It promotes chunks containing query
122
+ * terms verbatim, which on prose queries promotes chunks that happen to repeat a common word. Leave it off.
123
+ *
124
+ * That is a result about *that* reranker, not about reranking: a cross-encoder is a different mechanism and might
125
+ * well earn its cost. This port is how it would be measured, and `docs/26-retrieval-quality.md` is the baseline to
126
+ * measure it against.
88
127
  */
89
128
  export interface Reranker {
90
129
  readonly id: string;
@@ -111,6 +150,30 @@ export type RetrieverDeps = {
111
150
  * "no match"), the best of them normalises to 1.0, and every query finds something.
112
151
  */
113
152
  readonly semanticFloor?: number;
153
+ /**
154
+ * Serves `mode: "navigate"` — task #219, AC-4.
155
+ *
156
+ * Optional, and its absence is a *named refusal* for that mode rather than a fall back to semantic search: a
157
+ * caller that asked for navigation and silently got embeddings would attribute the results to the wrong
158
+ * mechanism, which is the only way this spike could have done harm.
159
+ */
160
+ readonly navigator?: Navigator;
161
+ /**
162
+ * Serves `mode: "graph-local"` — REQ-064 (#270), task #273.
163
+ *
164
+ * Optional for the same reason `navigator` is, and its absence is the same named refusal. GraphRAG is opt-in
165
+ * and expensive to index, so a deployment that never enabled it supplies nothing here and asking for the
166
+ * mode says so rather than quietly returning embeddings.
167
+ */
168
+ readonly graphLocal?: GraphLocalSearch;
169
+ /**
170
+ * Serves `mode: "graph-global"` — REQ-064 (#270), task #274.
171
+ *
172
+ * Separate from `graphLocal` rather than one graph dependency, because they are independently useful: a
173
+ * deployment can run entity-centric retrieval without paying for community summaries at all, and #273 landed
174
+ * before #272 for exactly that reason.
175
+ */
176
+ readonly graphGlobal?: GraphGlobalSearch;
114
177
  };
115
178
  export type RetrieveInput = {
116
179
  readonly query: string;
@@ -126,6 +189,36 @@ export type RetrieveInput = {
126
189
  /** Defaults to `hybrid`. The other two exist so the hybrid claim can be measured against them. */
127
190
  readonly mode?: RetrievalMode;
128
191
  };
192
+ /**
193
+ * Reciprocal rank fusion, extracted so there is exactly one of it — REQ-045 (#204), task #210, AC-2.
194
+ *
195
+ * `find_tools` fuses two signals over tool descriptors and this fuses two signals over knowledge chunks. Those
196
+ * are the same algorithm with a different corpus, and writing it twice is the shape this repository keeps
197
+ * finding defects in: the second copy drifts, usually in the tie-break or the normalisation, and the drift
198
+ * shows up as one ranker being subtly worse with nothing pointing at why.
199
+ *
200
+ * Generic over the item and its key. The **key** is what merges an item found by both signals; without it a
201
+ * chunk in both lists would fuse with itself and score twice.
202
+ *
203
+ * Scores come back normalised against the best fused score, because a raw RRF sum means nothing on its own —
204
+ * `2/61` is not "poor", it is "found first by both signals". Normalising is what lets one relevance floor apply
205
+ * to any corpus, tools included.
206
+ */
207
+ export type FusedEntry<T, S extends string> = {
208
+ readonly item: T;
209
+ /** 0–1, relative to the best entry in this fusion. Comparable within one query, never across two. */
210
+ readonly score: number;
211
+ readonly signals: readonly S[];
212
+ };
213
+ export declare const fuseByRank: <T, S extends string>(input: {
214
+ readonly lists: readonly {
215
+ readonly signal: S;
216
+ readonly items: readonly T[];
217
+ }[];
218
+ readonly keyOf: (item: T) => string;
219
+ /** The rank-fusion constant. Defaults to `RRF_K`; a caller changing it should say why. */
220
+ readonly k?: number;
221
+ }) => readonly FusedEntry<T, S>[];
129
222
  export declare const createRetriever: (deps: RetrieverDeps) => {
130
223
  rerankerId: string | null;
131
224
  retrieve(context: {
@@ -14,9 +14,19 @@
14
14
  *
15
15
  * score(d) = Σ over signals of 1 / (K + rank(d))
16
16
  *
17
- * A document ranked first by one signal and absent from the other still beats one ranked fifth by both, which
18
- * is the behaviour that makes hybrid better than either — the exact-term hit surfaces even though the semantic
19
- * signal never saw it.
17
+ * A document ranked first by one signal and absent from the other still beats one ranked fifth by both, which is
18
+ * the behaviour hybrid exists for — the exact-term hit surfaces even though the semantic signal never saw it.
19
+ *
20
+ * **And on one real corpus it is worse than semantic alone.** This comment used to say hybrid "measurably beats"
21
+ * both parts; #219 measured it over 56 documents of technical prose and hybrid lost 11.1 points of success@5 to
22
+ * semantic-only — two cases out of eighteen. The mechanism is the same paragraph read the other way: RRF weights both signals equally by
23
+ * construction, so fusing a weak list with a strong one demotes the strong list's top hits wherever the weak one
24
+ * disagrees. The lexical signal is weak on natural-language questions over prose.
25
+ *
26
+ * Hybrid remains the default, deliberately — that dataset has 18 queries, one author, and **no identifier
27
+ * queries**, which is precisely the case hybrid exists for. But the claim in this comment was untested for months
28
+ * and turned out to be false where it was finally tested, so it is stated with its evidence now. See
29
+ * `docs/26-retrieval-quality.md`.
20
30
  *
21
31
  * **`K = 60`** is the value from Cormack, Clarke and Buettcher's original TREC work and the one every
22
32
  * implementation since has used. It is large relative to the ranks that matter, which flattens the difference
@@ -47,7 +57,52 @@ export const DEFAULT_RELEVANCE_FLOOR = 0.4;
47
57
  * different answer from "you have no indexed documents", and telling a user the first when the second is true
48
58
  * sends them looking for content they never uploaded.
49
59
  */
50
- export const NO_RESULT_REASONS = ["nothing-indexed", "no-match", "below-threshold", "no-access"];
60
+ export const NO_RESULT_REASONS = [
61
+ "nothing-indexed",
62
+ "no-match",
63
+ "below-threshold",
64
+ "no-access",
65
+ /**
66
+ * The mode asked for is not wired.
67
+ *
68
+ * Its own reason rather than `no-match`, because the two want opposite responses: one says rephrase, this says
69
+ * a deployment has not configured what you asked for. Falling back to another mode silently would be worse
70
+ * than either, since the caller would attribute the results to the mode it named.
71
+ *
72
+ * **Live, not a placeholder** — audited for #256 AC-8. Three code paths return it (`navigate` with no
73
+ * navigator, `graph-local` and `graph-global` with no GraphRAG), and three test files assert it. The optional
74
+ * modes are exactly why it has to exist: GraphRAG ships **off by default** on the measured evidence in
75
+ * `docs/29`, so "you asked for a mode this deployment did not switch on" is an ordinary answer rather than an
76
+ * edge case. The comment previously cited task #219 as if it were pending; it closed.
77
+ */
78
+ "not-configured",
79
+ ];
80
+ export const fuseByRank = (input) => {
81
+ const k = input.k ?? RRF_K;
82
+ const fused = new Map();
83
+ for (const list of input.lists) {
84
+ list.items.forEach((item, rank) => {
85
+ const key = input.keyOf(item);
86
+ const increment = 1 / (k + rank + 1);
87
+ const existing = fused.get(key);
88
+ if (existing === undefined)
89
+ fused.set(key, { item, key, score: increment, signals: new Set([list.signal]) });
90
+ else {
91
+ existing.score += increment;
92
+ existing.signals.add(list.signal);
93
+ }
94
+ });
95
+ }
96
+ // Key order breaks ties, so two runs over the same corpus produce the same ranking. A `Map` iteration order
97
+ // tie-break would depend on which signal happened to return first.
98
+ const ordered = [...fused.values()].sort((a, b) => (b.score !== a.score ? b.score - a.score : a.key.localeCompare(b.key)));
99
+ const best = ordered[0]?.score ?? 0;
100
+ return ordered.map((entry) => ({
101
+ item: entry.item,
102
+ score: best === 0 ? 0 : entry.score / best,
103
+ signals: [...entry.signals],
104
+ }));
105
+ };
51
106
  const referenceFor = (chunk) => ({
52
107
  sourceType: chunk.sourceType,
53
108
  sourceId: chunk.sourceId,
@@ -60,6 +115,7 @@ const NO_RESULT_MESSAGES = {
60
115
  "no-match": "Nothing in the available material matches that.",
61
116
  "below-threshold": "Nothing in the available material is a close enough match to rely on.",
62
117
  "no-access": "There is no material you have access to that matches that.",
118
+ "not-configured": "That retrieval mode is not configured for this deployment.",
63
119
  };
64
120
  export const createRetriever = (deps) => {
65
121
  const candidateCount = deps.candidates ?? DEFAULT_CANDIDATES;
@@ -69,6 +125,91 @@ export const createRetriever = (deps) => {
69
125
  rerankerId: deps.reranker?.id ?? null,
70
126
  async retrieve(context, input) {
71
127
  const mode = input.mode ?? "hybrid";
128
+ /**
129
+ * The graph mode, delegated whole — task #273.
130
+ *
131
+ * Before the `authSubjects` check below only because it does its own: an empty subject list means the
132
+ * same thing here, and the graph search returns an empty result for it rather than traversing first and
133
+ * filtering after. Placed with `navigate` because both are delegated modes rather than fusion inputs.
134
+ */
135
+ if (mode === "graph-local") {
136
+ if (deps.graphLocal === undefined)
137
+ return { found: false, reason: "not-configured", message: NO_RESULT_MESSAGES["not-configured"], mode };
138
+ if (input.authSubjects.length === 0)
139
+ return { found: false, reason: "no-access", message: NO_RESULT_MESSAGES["no-access"], mode };
140
+ const result = await deps.graphLocal.search(context, {
141
+ query: input.query,
142
+ authSubjects: input.authSubjects,
143
+ limit: input.limit,
144
+ ...(input.sourceTypes === undefined ? {} : { sourceTypes: input.sourceTypes }),
145
+ });
146
+ if (result.hits.length === 0)
147
+ /**
148
+ * An honest empty result, and the reason distinguishes the two ways it happens.
149
+ *
150
+ * Nothing in the question named an entity the graph knows is `no-match` — rephrasing might work.
151
+ * Entities matched but reached no readable chunk is also `no-match` rather than `no-access`, because
152
+ * the principal may genuinely have access to a corpus that simply says nothing more.
153
+ */
154
+ return { found: false, reason: "no-match", message: NO_RESULT_MESSAGES["no-match"], mode };
155
+ return {
156
+ found: true,
157
+ mode,
158
+ hits: result.hits.map((hit) => ({
159
+ chunk: hit.chunk,
160
+ score: hit.score,
161
+ signals: ["graph-local"],
162
+ reference: referenceFor(hit.chunk),
163
+ })),
164
+ };
165
+ }
166
+ /**
167
+ * The corpus-level mode — task #274.
168
+ *
169
+ * Its cost ceiling **throws** rather than returning an outcome, and that is deliberate: exceeding it is
170
+ * not "found nothing", it is "this question is too expensive to answer this way", and the two want
171
+ * different responses. `RetrievalOutcome`'s `found: false` reasons are all about the corpus; this is
172
+ * about the request.
173
+ */
174
+ if (mode === "graph-global") {
175
+ if (deps.graphGlobal === undefined)
176
+ return { found: false, reason: "not-configured", message: NO_RESULT_MESSAGES["not-configured"], mode };
177
+ if (input.authSubjects.length === 0)
178
+ return { found: false, reason: "no-access", message: NO_RESULT_MESSAGES["no-access"], mode };
179
+ const result = await deps.graphGlobal.search(context, {
180
+ query: input.query,
181
+ authSubjects: input.authSubjects,
182
+ limit: input.limit,
183
+ ...(input.sourceTypes === undefined ? {} : { sourceTypes: input.sourceTypes }),
184
+ });
185
+ if (result.hits.length === 0)
186
+ return { found: false, reason: "no-match", message: NO_RESULT_MESSAGES["no-match"], mode };
187
+ return {
188
+ found: true,
189
+ mode,
190
+ hits: result.hits.map((hit) => ({
191
+ chunk: hit.chunk,
192
+ score: hit.score,
193
+ signals: ["graph-global"],
194
+ reference: referenceFor(hit.chunk),
195
+ })),
196
+ };
197
+ }
198
+ // The spike's mode, delegated whole: it shares no step with the fusion path below.
199
+ if (mode === "navigate") {
200
+ if (deps.navigator === undefined)
201
+ return {
202
+ found: false,
203
+ reason: "not-configured",
204
+ message: NO_RESULT_MESSAGES["not-configured"],
205
+ mode,
206
+ };
207
+ return deps.navigator.navigate(context, {
208
+ query: input.query,
209
+ authSubjects: input.authSubjects,
210
+ limit: input.limit,
211
+ });
212
+ }
72
213
  // Checked before either index is asked. An empty subject list is not a query with no results — it is a
73
214
  // caller with no access, and the two want different sentences.
74
215
  if (input.authSubjects.length === 0)
@@ -93,30 +234,19 @@ export const createRetriever = (deps) => {
93
234
  const lexical = mode === "semantic" ? [] : await deps.keyword.search({ ...scope, query: input.query });
94
235
  if (semantic.length === 0 && lexical.length === 0)
95
236
  return { found: false, reason: "no-match", message: NO_RESULT_MESSAGES["no-match"], mode };
96
- // RRF. Rank, not score: see the note at the top on why adding two incomparable scales fails silently.
97
- const fused = new Map();
98
- const contribute = (hits, signal) => {
99
- hits.forEach((hit, rank) => {
100
- const existing = fused.get(hit.chunk.id);
101
- const increment = 1 / (RRF_K + rank + 1);
102
- if (existing === undefined)
103
- fused.set(hit.chunk.id, { chunk: hit.chunk, score: increment, signals: new Set([signal]) });
104
- else {
105
- existing.score += increment;
106
- existing.signals.add(signal);
107
- }
108
- });
109
- };
110
- contribute(semantic, "semantic");
111
- contribute(lexical, "keyword");
112
- const ordered = [...fused.values()].sort((a, b) => b.score !== a.score ? b.score - a.score : a.chunk.id.localeCompare(b.chunk.id));
113
- const best = ordered[0]?.score ?? 0;
114
- const candidates = ordered.map((entry) => ({
115
- chunk: entry.chunk,
116
- // Normalised against the best fused score, so the floor means the same thing whatever the corpus.
117
- score: best === 0 ? 0 : entry.score / best,
118
- signals: [...entry.signals],
119
- reference: referenceFor(entry.chunk),
237
+ // RRF, through the shared implementation. Rank, not score: see the note at the top on why adding two
238
+ // incomparable scales fails silently, and `fuseByRank` on why there is only one of these.
239
+ const candidates = fuseByRank({
240
+ lists: [
241
+ { signal: "semantic", items: semantic },
242
+ { signal: "keyword", items: lexical },
243
+ ],
244
+ keyOf: (hit) => hit.chunk.id,
245
+ }).map((entry) => ({
246
+ chunk: entry.item.chunk,
247
+ score: entry.score,
248
+ signals: entry.signals,
249
+ reference: referenceFor(entry.item.chunk),
120
250
  }));
121
251
  const relevant = candidates.filter((hit) => hit.score >= floor);
122
252
  if (relevant.length === 0)
@@ -0,0 +1,168 @@
1
+ /**
2
+ * An inbound MCP server — REQ-059 (#249), task #250.
3
+ *
4
+ * `backend/src/mcp/` is the **outbound** direction: a tenant registers their MCP server and this platform
5
+ * consumes it. Its own header says so, and points at an inbound server that lives in the *old Chorus
6
+ * repository* — not in this package. So `@retinue/agentkit` could consume an MCP server and could not be one,
7
+ * and a deployment's tools were unreachable from Claude Code, Claude Desktop, Cursor and every other MCP client.
8
+ *
9
+ * Nothing here re-implements a capability. The registry already does authorization, the tenant's toolset, the
10
+ * approval gate, validation, idempotency and audit attribution; this is a protocol surface in front of it, and
11
+ * every call goes through `registry.execute` exactly as an agent's would.
12
+ *
13
+ * ## The trust direction inverts, and that is the thing to get right
14
+ *
15
+ * Outbound, `mcp/index.ts` establishes the rule: a remote server's `readOnlyHint`/`destructiveHint` are
16
+ * *advisory and untrusted*, and "a remote server cannot talk its way down to" a weaker effect. Inbound, **this
17
+ * package is the remote server**. Two consequences, and both are enforced rather than intended:
18
+ *
19
+ * 1. What it advertises must be what it enforces. The annotations are derived from `ToolEffect` by one
20
+ * function, so they cannot drift from the effect the registry acts on.
21
+ * 2. Nothing the client sends is trusted — not an effect, not a hint, not a claim about a prior approval. The
22
+ * client supplies a tool name and arguments; everything else comes from the context the *host* built.
23
+ *
24
+ * ## Authentication is structural
25
+ *
26
+ * `createMcpToolServer` takes a resolved `ExecutionContext`. There is no `authenticate` callback to forget and
27
+ * no default: a host that has not authenticated has nothing to pass, so it cannot construct a server. That is
28
+ * the same decision `server/main.ts` makes — "a permissive default would serve an open API to anyone who forgot
29
+ * to set it" — expressed in the type instead of in a runtime check, because this surface gets exposed to the
30
+ * internet and a runtime check can be skipped.
31
+ *
32
+ * One server per authenticated session: a stdio server is launched per client and carries that client's
33
+ * identity for its lifetime; an HTTP mount builds one per authenticated request.
34
+ */
35
+ import type { ExecutionContext } from "../core/context.js";
36
+ import type { ToolDescriptor, ToolEffect, ToolRegistry } from "../tools/index.js";
37
+ /**
38
+ * The MCP SDK, imported for its types only where possible.
39
+ *
40
+ * An **optional peer dependency**, following `vitest` on `./testing`: a consumer who never mounts an MCP server
41
+ * never installs it, and the only path to this module is its own subpath.
42
+ */
43
+ export type McpServerLike = {
44
+ /**
45
+ * `any`, and it is the correct choice rather than a shortcut.
46
+ *
47
+ * The SDK's real signature is generic —
48
+ * `<T extends AnyObjectSchema>(schema: T, handler: (request: SchemaOutput<T>, extra: RequestHandlerExtra) => …)`
49
+ * — and this package cannot restate those generics without importing the SDK, which would stop it being an
50
+ * optional peer. A structural type with `unknown`/`never` parameters looks stricter and is **not assignable
51
+ * from** the SDK's `Server`, so the first version of this failed to typecheck at the only call site anybody
52
+ * will write. `check:consumer` caught it by compiling the documented sample against the packed tarball:
53
+ *
54
+ * Argument of type 'Server<…>' is not assignable to parameter of type 'McpServerLike'
55
+ * Type 'any' is not assignable to type 'never'
56
+ *
57
+ * A type nothing real satisfies is worse than a permissive one.
58
+ */
59
+ setRequestHandler(schema: any,
60
+ /**
61
+ * The result is an object, not `unknown` — the second half of the same lesson.
62
+ *
63
+ * The SDK's handler must return a `ServerResult`, and `Promise<unknown>` is *wider* than that, so a `Server`
64
+ * was still not assignable even after the parameters were relaxed. A permissive object shape satisfies it
65
+ * in both directions.
66
+ */
67
+ handler: (request: any, extra?: any) => Promise<Record<string, unknown>>): void;
68
+ connect(transport: any): Promise<void>;
69
+ };
70
+ export type McpToolAnnotations = {
71
+ readonly readOnlyHint: boolean;
72
+ readonly destructiveHint: boolean;
73
+ readonly idempotentHint: boolean;
74
+ /** True when the tool reaches something outside this deployment. */
75
+ readonly openWorldHint: boolean;
76
+ };
77
+ /**
78
+ * `ToolEffect` → MCP annotations, in **one** place — AC-7.
79
+ *
80
+ * One function, so the advertised hint and the enforced effect cannot drift: a test asserts this mapping over
81
+ * every effect, and the exposed set is checked against it. Two tables would be two chances to disagree, and the
82
+ * disagreement would be a client told a tool is read-only calling something that writes.
83
+ *
84
+ * `destructive` is the only `destructiveHint`, and `read` the only `readOnlyHint` — deliberately narrow.
85
+ * Over-claiming read-only is the dangerous direction: a client may skip a confirmation on it.
86
+ */
87
+ export declare const annotationsFor: (effect: ToolEffect) => McpToolAnnotations;
88
+ /**
89
+ * A tool's input schema, in the shape MCP requires — found by a real client.
90
+ *
91
+ * MCP's `tools/list` demands `inputSchema.type === "object"`, and the SDK's *client* validates it: a tool whose
92
+ * descriptor holds a Zod schema made the whole listing fail with `expected "object"` at one index. So the
93
+ * failure was not "one tool looks odd", it was **no tools at all**, from one non-conforming entry.
94
+ *
95
+ * Three cases, in order of fidelity:
96
+ *
97
+ * 1. Already a JSON-Schema object — pass it through unchanged. It is what execution validates against, so
98
+ * advertising anything else would advertise a contract nothing enforces.
99
+ * 2. A Zod schema — convert it. `defineTool` documents `inputSchema` as "Zod schema or JSON-schema object", so
100
+ * both are legitimate and a client deserves the real one either way.
101
+ * 3. Anything else — `{ type: "object" }`, permissive. Deliberately the *weaker* advertisement rather than a
102
+ * refusal to list: the registry still validates the real schema on execution, so the failure direction is a
103
+ * rejected call rather than an accepted bad one.
104
+ */
105
+ export declare const toMcpInputSchema: (schema: unknown) => Record<string, unknown>;
106
+ /** What a client is shown for one tool. */
107
+ export declare const describeForMcp: (descriptor: ToolDescriptor) => {
108
+ name: string;
109
+ title: string;
110
+ description: string;
111
+ inputSchema: Record<string, unknown>;
112
+ annotations: McpToolAnnotations;
113
+ };
114
+ export type McpToolServerDeps = {
115
+ readonly registry: ToolRegistry;
116
+ /**
117
+ * The authenticated caller. **Required, and there is no default.**
118
+ *
119
+ * A host that has not authenticated cannot construct a server, which is the guarantee expressed in the type
120
+ * rather than in a check somebody can skip.
121
+ */
122
+ readonly context: ExecutionContext;
123
+ readonly serverInfo?: {
124
+ readonly name: string;
125
+ readonly version: string;
126
+ };
127
+ };
128
+ /**
129
+ * The result of a `tools/call`, in MCP's shape.
130
+ *
131
+ * A refusal is `isError: true` with the reason as text — **not** a protocol error. That distinction matters: a
132
+ * protocol error tells the client the server is broken, and a tool refusal is a normal outcome the model should
133
+ * see and respond to. It is the same decision `streamModelTurn` makes for `tool-error`.
134
+ */
135
+ export declare const toMcpResult: (outcome: {
136
+ readonly ok: true;
137
+ readonly data: unknown;
138
+ } | {
139
+ readonly ok: false;
140
+ readonly error: {
141
+ readonly code: string;
142
+ readonly message: string;
143
+ };
144
+ }) => {
145
+ content: {
146
+ type: "text";
147
+ text: string;
148
+ }[];
149
+ isError?: undefined;
150
+ } | {
151
+ content: {
152
+ type: "text";
153
+ text: string;
154
+ }[];
155
+ isError: true;
156
+ };
157
+ /**
158
+ * Registers the two handlers on an SDK `Server`.
159
+ *
160
+ * Takes the server rather than constructing it, so the SDK stays an optional peer of this package: the host
161
+ * imports `@modelcontextprotocol/sdk`, builds a `Server`, and hands it here. That also leaves the host in
162
+ * charge of the transport, which is where the authentication story differs between stdio and HTTP.
163
+ */
164
+ export declare const registerRetinueTools: (server: McpServerLike, schemas: {
165
+ readonly listTools: unknown;
166
+ readonly callTool: unknown;
167
+ }, deps: McpToolServerDeps) => void;
168
+ //# sourceMappingURL=index.d.ts.map