bantamkit-mcp 0.26.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 (148) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +398 -0
  3. package/assets/contracts/default.yaml +90 -0
  4. package/assets/evals/devteam/manifest.yaml +351 -0
  5. package/assets/evals/devteam/repo/HISTORY.md +18 -0
  6. package/assets/evals/devteam/repo/README.md +12 -0
  7. package/assets/evals/devteam/repo/docs/architecture.md +17 -0
  8. package/assets/evals/devteam/repo/docs/runbook.md +10 -0
  9. package/assets/evals/devteam/repo/issues/142-settlement-timeout.md +23 -0
  10. package/assets/evals/devteam/repo/patches/0009-retry-budget.patch +38 -0
  11. package/assets/evals/devteam/repo/src/ledger/__init__.py +3 -0
  12. package/assets/evals/devteam/repo/src/ledger/config.py +35 -0
  13. package/assets/evals/devteam/repo/src/ledger/errors.py +13 -0
  14. package/assets/evals/devteam/repo/src/ledger/posting.py +12 -0
  15. package/assets/evals/devteam/repo/src/ledger/registry.py +7 -0
  16. package/assets/evals/devteam/repo/src/ledger/report.py +9 -0
  17. package/assets/evals/devteam/repo/src/ledger/retry.py +17 -0
  18. package/assets/evals/devteam/repo/src/ledger/settle.py +16 -0
  19. package/assets/evals/devteam/repo/src/ledger/validate.py +14 -0
  20. package/assets/evals/devteam/repo/tests/test_posting.py +13 -0
  21. package/assets/evals/devteam/repo/tests/test_settle.py +9 -0
  22. package/assets/evals/devteam/tasks/dt-error-contract.yaml +186 -0
  23. package/assets/evals/devteam/tasks/dt-handler-map.yaml +183 -0
  24. package/assets/evals/devteam/tasks/dt-patch-before-after.yaml +182 -0
  25. package/assets/evals/devteam/tasks/dt-retry-attempts.yaml +181 -0
  26. package/assets/evals/devteam/tasks/dt-settlement-config.yaml +185 -0
  27. package/assets/evals/devteam/tasks/dt-symbol-home.yaml +181 -0
  28. package/assets/evals/devteam/tasks/dt-trace-blame.yaml +182 -0
  29. package/assets/evals/devteam/tasks/dt-unread-key.yaml +180 -0
  30. package/assets/evals/document/tasks/doc-large-in-137.yaml +38 -0
  31. package/assets/evals/document/tasks/doc-large-in-359.yaml +44 -0
  32. package/assets/evals/document/tasks/doc-large-in-372.yaml +38 -0
  33. package/assets/evals/document/tasks/doc-large-out-11764.yaml +37 -0
  34. package/assets/evals/document/tasks/doc-large-out-4137.yaml +37 -0
  35. package/assets/evals/document/tasks/doc-large-out-8022.yaml +37 -0
  36. package/assets/evals/document/tasks/doc-small-137.yaml +37 -0
  37. package/assets/evals/document/tasks/doc-small-261.yaml +37 -0
  38. package/assets/evals/document/tasks/doc-small-388.yaml +37 -0
  39. package/assets/evals/fixtures/.gitkeep +0 -0
  40. package/assets/evals/fixtures/catalog.json +6 -0
  41. package/assets/evals/perturbations/task-completion.yaml +576 -0
  42. package/assets/evals/tasks/.gitkeep +0 -0
  43. package/assets/evals/tasks/extract-contact.yaml +14 -0
  44. package/assets/evals/tasks/extract-invoice.yaml +14 -0
  45. package/assets/evals/tasks/extract-order.yaml +15 -0
  46. package/assets/evals/tasks/extract-schedule.yaml +14 -0
  47. package/assets/evals/tasks/extract-versions.yaml +17 -0
  48. package/assets/evals/tasks/nav-prod-port.yaml +84 -0
  49. package/assets/evals/tasks/nav-release-bundle.yaml +87 -0
  50. package/assets/evals/tasks/recall-audit-retention.yaml +17 -0
  51. package/assets/evals/tasks/recall-cache-ttl.yaml +13 -0
  52. package/assets/evals/tasks/recall-db-port.yaml +17 -0
  53. package/assets/evals/tasks/recall-deploy.yaml +13 -0
  54. package/assets/evals/tasks/recall-env-endpoint.yaml +18 -0
  55. package/assets/evals/tasks/recall-oncall-rotation.yaml +21 -0
  56. package/assets/evals/tasks/recall-oncall.yaml +13 -0
  57. package/assets/evals/tasks/recall-org-quota.yaml +18 -0
  58. package/assets/evals/tasks/recall-owner.yaml +13 -0
  59. package/assets/evals/tasks/shop-basket-total.yaml +10 -0
  60. package/assets/evals/tasks/shop-cheapest.yaml +9 -0
  61. package/assets/evals/tasks/shop-compare.yaml +9 -0
  62. package/assets/evals/tasks/shop-gadget-value.yaml +9 -0
  63. package/assets/evals/tasks/shop-stock-total.yaml +9 -0
  64. package/assets/evals/tasks/shop-total.yaml +9 -0
  65. package/assets/profiles/default.yaml +31 -0
  66. package/assets/profiles/patient.yaml +31 -0
  67. package/assets/rubrics/.gitkeep +0 -0
  68. package/assets/rubrics/code-quality.yaml +20 -0
  69. package/assets/rubrics/grounded-completion.yaml +37 -0
  70. package/assets/rubrics/task-completion.yaml +28 -0
  71. package/assets/schemas/shiftwork-checkpoint.json +188 -0
  72. package/assets/skills/.gitkeep +0 -0
  73. package/assets/skills/file-graph.md +7 -0
  74. package/assets/skills/memory.md +35 -0
  75. package/assets/tools/.gitkeep +0 -0
  76. package/assets/tools/bantamkit_read.json +48 -0
  77. package/assets/tools/bantamkit_status.json +25 -0
  78. package/assets/tools/build_identity.json +17 -0
  79. package/assets/tools/document_list.json +12 -0
  80. package/assets/tools/document_read.json +31 -0
  81. package/assets/tools/file_graph.json +12 -0
  82. package/assets/tools/memory_compact.json +31 -0
  83. package/assets/tools/memory_recall.json +38 -0
  84. package/assets/tools/memory_save.json +61 -0
  85. package/assets/tools/shiftwork_clock_in.json +25 -0
  86. package/assets/tools/shiftwork_clock_out.json +60 -0
  87. package/assets/tools/shiftwork_status.json +25 -0
  88. package/assets/tools/skill_audit.json +70 -0
  89. package/assets/tools/validate_json.json +31 -0
  90. package/dist/assets.d.ts +48 -0
  91. package/dist/assets.js +109 -0
  92. package/dist/charsets.d.ts +17 -0
  93. package/dist/charsets.js +580 -0
  94. package/dist/cli.d.ts +12 -0
  95. package/dist/cli.js +280 -0
  96. package/dist/contract.d.ts +145 -0
  97. package/dist/contract.js +608 -0
  98. package/dist/docread.d.ts +357 -0
  99. package/dist/docread.js +3315 -0
  100. package/dist/errors.d.ts +11 -0
  101. package/dist/errors.js +14 -0
  102. package/dist/eventlog.d.ts +137 -0
  103. package/dist/eventlog.js +294 -0
  104. package/dist/htmlentities.d.ts +2 -0
  105. package/dist/htmlentities.js +16 -0
  106. package/dist/index.d.ts +23 -0
  107. package/dist/index.js +13 -0
  108. package/dist/mcp/identity.d.ts +25 -0
  109. package/dist/mcp/identity.js +275 -0
  110. package/dist/mcp/pyargs.d.ts +80 -0
  111. package/dist/mcp/pyargs.js +362 -0
  112. package/dist/mcp/sdkjson.d.ts +56 -0
  113. package/dist/mcp/sdkjson.js +91 -0
  114. package/dist/mcp/server.d.ts +45 -0
  115. package/dist/mcp/server.js +740 -0
  116. package/dist/mcp/status.d.ts +210 -0
  117. package/dist/mcp/status.js +357 -0
  118. package/dist/mcp/transport.d.ts +68 -0
  119. package/dist/mcp/transport.js +111 -0
  120. package/dist/mcpreport.d.ts +214 -0
  121. package/dist/mcpreport.js +758 -0
  122. package/dist/memory/cli.d.ts +2 -0
  123. package/dist/memory/cli.js +396 -0
  124. package/dist/memory/component.d.ts +256 -0
  125. package/dist/memory/component.js +497 -0
  126. package/dist/memory/factfile.d.ts +120 -0
  127. package/dist/memory/factfile.js +406 -0
  128. package/dist/memory/layers.d.ts +92 -0
  129. package/dist/memory/layers.js +410 -0
  130. package/dist/memory/pyfs.d.ts +521 -0
  131. package/dist/memory/pyfs.js +1608 -0
  132. package/dist/memory/pyyaml.d.ts +155 -0
  133. package/dist/memory/pyyaml.js +868 -0
  134. package/dist/memory/store.d.ts +478 -0
  135. package/dist/memory/store.js +1034 -0
  136. package/dist/pyargparse.d.ts +141 -0
  137. package/dist/pyargparse.js +813 -0
  138. package/dist/pyjson.d.ts +110 -0
  139. package/dist/pyjson.js +524 -0
  140. package/dist/pyjsonschema.d.ts +50 -0
  141. package/dist/pyjsonschema.js +951 -0
  142. package/dist/shiftwork.d.ts +60 -0
  143. package/dist/shiftwork.js +324 -0
  144. package/dist/skillaudit.d.ts +172 -0
  145. package/dist/skillaudit.js +914 -0
  146. package/dist/statusline.d.ts +106 -0
  147. package/dist/statusline.js +281 -0
  148. package/package.json +45 -0
@@ -0,0 +1,362 @@
1
+ /**
2
+ * Argument validation, in the reference runtime's own words.
3
+ *
4
+ * WHY THIS IS REPRODUCED RATHER THAN RULED. The text below is not a diagnostic in a log —
5
+ * it is the `content[0].text` of a tool result, which means the MODEL reads it and decides
6
+ * what to do next. `zod`'s wording is entirely different, so a port that let zod speak would
7
+ * ship a server whose recovery behaviour differs from the reference's on every mistyped
8
+ * argument, and that is a product difference, not a cosmetic one.
9
+ *
10
+ * WHAT THE URL IS. `https://errors.pydantic.dev/2.13/v/<type>` names an entry in pydantic's
11
+ * public error catalogue. This runtime has no pydantic; the URL is part of the sentence the
12
+ * reference emits, reproduced because the sentence is the contract. `2.13` is therefore a
13
+ * PIN on the reference, not a claim about this process — and it is compared against the
14
+ * live server by `tools/conformance/suites/wire.mjs`, so the day the Python side bumps to
15
+ * 2.14 the suite goes red here instead of the two runtimes drifting apart quietly.
16
+ *
17
+ * EVERY RULE BELOW WAS MEASURED, not read off pydantic's documentation. The probes are in
18
+ * `scratchpad/job38-n7/probe{4,7,8,9}.py` and each drove the real stdio server:
19
+ *
20
+ * * errors come in MODEL FIELD order, not in the order the arguments arrived (probe7 id 80
21
+ * sends `body, name, description, type` and gets `type` before `name`);
22
+ * * `int` is LAX — `True`, `'2'`, `' 3 '`, `'2_0'`, `'+2'` and `3.0` all validate, while
23
+ * `'2.5'`, `'1e3'`, `'0x10'` and `''` are `int_parsing` and `2.5` is `int_from_float`;
24
+ * * a float too large for an i64 is `int_parsing_size`, with a DIFFERENT message shape
25
+ * (no "Input should be a valid integer," prefix);
26
+ * * `input_value` is truncated on the UTF-8 BYTE length of `repr(value)`: over 50 bytes it
27
+ * becomes head-25-bytes + `...` + tail-24-bytes, each end floored to a character
28
+ * boundary. Thirty Thai characters are 34 repr CHARACTERS and are still truncated.
29
+ */
30
+ import { reprValue } from '../pyjson.js';
31
+ const req = (name, kind) => ({ name, kind, optional: false });
32
+ const opt = (name, kind, bound = {}) => ({
33
+ name,
34
+ kind,
35
+ optional: true,
36
+ ...bound,
37
+ });
38
+ /**
39
+ * `assets/tools/bantamkit_read.json` `offset.maximum` — `Number.MAX_SAFE_INTEGER`, the
40
+ * largest integer `JSON.parse` reads back unchanged (F1). The reference binds it as
41
+ * `Annotated[int, Field(le=OFFSET_MAXIMUM)]`, so above it the refusal is pydantic's
42
+ * `less_than_equal` frame and the handler never runs; this side reads the JSON with its
43
+ * own decoder, so `9007199254740993` arrives exact and is refused with the same text.
44
+ */
45
+ const OFFSET_MAXIMUM = 9007199254740991n;
46
+ /**
47
+ * One model per handler, mirroring `build_server`'s closures signature for signature.
48
+ *
49
+ * `build_identity`'s model is `build_identity_toolArguments` and not `build_identityArguments`
50
+ * — pydantic names it after the PYTHON FUNCTION, and the function is the closure
51
+ * `build_identity_tool`. The same leak is already in the shipped manifest
52
+ * (`output_schema.title` is `build_identity_toolDictOutput`), so the name is not an accident
53
+ * this port gets to tidy: it is on the wire today, and a client that pinned it would break.
54
+ *
55
+ * MEASURED SURVIVOR, AND IT STAYS. A mutation changing this one string to
56
+ * `build_identityArguments` survives both gates, because `build_identity` has no parameters
57
+ * and a model with no fields can never fail — the name is unreachable from the wire. It is
58
+ * still spelled correctly: the day the tool grows an argument, the sweep's kill comes for
59
+ * free, and a name that was wrong until then would be a defect nobody planted. The leak that
60
+ * IS observable is the manifest's `output_schema.title`, and that one is compared byte for
61
+ * byte by the conformance suite's `advertisement: id 2` case.
62
+ */
63
+ export const ARG_MODELS = {
64
+ memory_save: {
65
+ model: 'memory_saveArguments',
66
+ fields: [req('type', 'str'), req('name', 'str'), req('description', 'str'), req('body', 'str'), opt('links', 'listStr')],
67
+ },
68
+ memory_recall: { model: 'memory_recallArguments', fields: [req('query', 'str'), opt('k', 'int')] },
69
+ validate_json: { model: 'validate_jsonArguments', fields: [req('output', 'str'), req('schema', 'dict')] },
70
+ shiftwork_clock_in: { model: 'shiftwork_clock_inArguments', fields: [req('checkpoint', 'str')] },
71
+ shiftwork_clock_out: {
72
+ model: 'shiftwork_clock_outArguments',
73
+ fields: [
74
+ req('checkpoint', 'str'),
75
+ req('unit_id', 'str'),
76
+ req('status', 'str'),
77
+ req('handoff_patch', 'dict'),
78
+ req('history_entry', 'dict'),
79
+ opt('accounting', 'dict'),
80
+ ],
81
+ },
82
+ shiftwork_status: { model: 'shiftwork_statusArguments', fields: [req('checkpoint', 'str')] },
83
+ build_identity: { model: 'build_identity_toolArguments', fields: [] },
84
+ // `bantamkit_statusArguments`, not `bantamkit_status_toolArguments`: the model name is
85
+ // pydantic's, derived from the reference's CLOSURE name, and the status handler is called
86
+ // `bantamkit_status` there where the identity one is called `build_identity_tool`. It is
87
+ // the title `assets/tools/bantamkit_status.json` already advertises.
88
+ bantamkit_status: { model: 'bantamkit_statusArguments', fields: [] },
89
+ // `reserve: int | None = None` on the reference, so it is the same lax `int` as `k`:
90
+ // `'2'`, `True` and `3.0` validate, `'2.5'` is `int_parsing`, `2.5` is `int_from_float`,
91
+ // and an explicit `null` is the default. The manifest's `minimum: 0` is advisory to the
92
+ // client; nothing here floors it — a negative value reaches `MemoryStore.compact`, whose
93
+ // `max(0, min(reserve, budget // 2))` is the only clamp on either side (6b966e5).
94
+ memory_compact: { model: 'memory_compactArguments', fields: [opt('reserve', 'int')] },
95
+ // `bantamkit_read(path: str, part: str | None = None, offset: int | None = None,
96
+ // limit: int | None = None)` on the reference (job43). `path` and `part` are the strict
97
+ // `str` every other string field is — `123` is `string_type`, measured for `memory_save`
98
+ // and the same validator. `offset` and `limit` are the lax `int` that `k` and `reserve`
99
+ // are: `'2'`, `True` and `3.0` validate, `'2.5'` is `int_parsing`, `2.5` is
100
+ // `int_from_float`, and an explicit `null` is the default. The manifest's `minimum` and
101
+ // `maximum` are advisory to the client for `limit` and for `offset`'s floor; the handler
102
+ // clamps `limit` to `[1, 200]` and `offset` to `>= 0` itself, the way `memory_recall`
103
+ // clamps `k`. `offset`'s CEILING is bound in the model, as it is on the reference (F2).
104
+ bantamkit_read: {
105
+ model: 'bantamkit_readArguments',
106
+ fields: [req('path', 'str'), opt('part', 'str'), opt('offset', 'int', { le: OFFSET_MAXIMUM }), opt('limit', 'int')],
107
+ },
108
+ // `skill_audit(root: str, enabled: list[str] | None = None, usage: dict[str, int] | None =
109
+ // None, check: str | None = None, budget: int | None = None)` on the reference (job44).
110
+ // `usage` is the only `dict[str, int]` on the surface and the reason `dictInt` exists:
111
+ // pydantic validates a typed dict's VALUES, so `{'a': 'x'}` is `usage.a` / `int_parsing`
112
+ // where `handoff_patch`'s `dict[str, Any]` takes anything. `budget` carries no `le`,
113
+ // because the reference binds none — the manifest's `maximum` is advisory to the client,
114
+ // and the handler's own refusal is the negative one. `versions` is the only
115
+ // `dict[str, str]`, and the reason `dictStr` exists beside it: a `str` field is STRICT even
116
+ // in lax mode, so `{'a': 1}` is `versions.a` / `string_type` where the same value under
117
+ // `usage` would validate.
118
+ skill_audit: {
119
+ model: 'skill_auditArguments',
120
+ fields: [
121
+ req('root', 'str'),
122
+ opt('enabled', 'listStr'),
123
+ opt('usage', 'dictInt'),
124
+ opt('check', 'str'),
125
+ opt('budget', 'int'),
126
+ opt('versions', 'dictStr'),
127
+ ],
128
+ },
129
+ };
130
+ /** `type(value).__name__`, for the `input_type=` half of the sentence. */
131
+ function pyTypeName(value) {
132
+ switch (value.t) {
133
+ case 'null':
134
+ return 'NoneType';
135
+ case 'bool':
136
+ return 'bool';
137
+ case 'int':
138
+ return 'int';
139
+ case 'float':
140
+ return 'float';
141
+ case 'str':
142
+ return 'str';
143
+ case 'list':
144
+ return 'list';
145
+ case 'dict':
146
+ return 'dict';
147
+ }
148
+ }
149
+ const encoder = new TextEncoder();
150
+ const decoder = new TextDecoder('utf-8');
151
+ /**
152
+ * pydantic-core's `truncate_input_value`: 50 UTF-8 bytes, then `head[..25] + "..." + tail[24..]`
153
+ * with both cuts moved off a multi-byte character rather than splitting one.
154
+ *
155
+ * The floor at each end is what makes 30 Thai characters render as 7 + `...` + 7 instead of
156
+ * 8 + `...` + 8 — measured, probe8 case 104.
157
+ */
158
+ function truncateRepr(text) {
159
+ const bytes = encoder.encode(text);
160
+ if (bytes.length <= 50)
161
+ return text;
162
+ let head = 25;
163
+ while (head > 0 && (bytes[head] & 0xc0) === 0x80)
164
+ head -= 1;
165
+ let tail = bytes.length - 24;
166
+ while (tail < bytes.length && (bytes[tail] & 0xc0) === 0x80)
167
+ tail += 1;
168
+ return `${decoder.decode(bytes.slice(0, head))}...${decoder.decode(bytes.slice(tail))}`;
169
+ }
170
+ /** i64, which is the width pydantic parses an int into before it complains about size. */
171
+ const I64_MAX = 9223372036854775807n;
172
+ const I64_MIN = -9223372036854775808n;
173
+ /** `int(text)` as Python accepts it: optional sign, underscores between digits. */
174
+ const PY_INT_TEXT = /^[+-]?\d+(?:_\d+)*$/;
175
+ /** …and pydantic additionally accepts a trailing `.0`-style fraction that is all zeros. */
176
+ const PY_INT_DECIMAL_TEXT = /^([+-]?\d+(?:_\d+)*)\.0+$/;
177
+ /**
178
+ * Validate one field, returning either the coerced value or the error pydantic reports.
179
+ *
180
+ * Lax mode, which is pydantic's default and therefore the reference's: `bool` IS an `int`
181
+ * here, and a string that spells an integer IS one. Only `str` is strict, because pydantic
182
+ * refuses every non-`str` for a `str` field even in lax mode (measured: `123` -> string_type).
183
+ */
184
+ function checkField(spec, value) {
185
+ if (spec.optional && value.t === 'null')
186
+ return { value };
187
+ switch (spec.kind) {
188
+ case 'str':
189
+ if (value.t === 'str')
190
+ return { value };
191
+ return [{ loc: spec.name, type: 'string_type', msg: 'Input should be a valid string', input: value }];
192
+ case 'dict':
193
+ if (value.t === 'dict')
194
+ return { value };
195
+ return [{ loc: spec.name, type: 'dict_type', msg: 'Input should be a valid dictionary', input: value }];
196
+ case 'dictStr': {
197
+ // `dict[str, str]`. The same shape as `dictInt` next door, with the STRICT `str`
198
+ // validator on the values: one error per bad entry, in the dict's own order, with the
199
+ // KEY in the location — `versions.a`, never `versions.0`.
200
+ if (value.t !== 'dict') {
201
+ return [{ loc: spec.name, type: 'dict_type', msg: 'Input should be a valid dictionary', input: value }];
202
+ }
203
+ const bad = [];
204
+ for (const [key, item] of value.v) {
205
+ if (item.t !== 'str') {
206
+ bad.push({
207
+ loc: `${spec.name}.${key}`,
208
+ type: 'string_type',
209
+ msg: 'Input should be a valid string',
210
+ input: item,
211
+ });
212
+ }
213
+ }
214
+ return bad.length > 0 ? bad : { value };
215
+ }
216
+ case 'dictInt': {
217
+ // `dict[str, int]`. pydantic validates the VALUES too, one error per bad entry and in
218
+ // the dict's own order, with the KEY in the location — `usage.a`, never `usage.0`. The
219
+ // coerced value is what the handler gets, so `{'a': '0'}` reaches `skillaudit` as `0`
220
+ // and is a `never-invoked` finding exactly as `{'a': 0}` is.
221
+ if (value.t !== 'dict') {
222
+ return [{ loc: spec.name, type: 'dict_type', msg: 'Input should be a valid dictionary', input: value }];
223
+ }
224
+ const bad = [];
225
+ const coerced = new Map();
226
+ for (const [key, item] of value.v) {
227
+ const checked = checkInt(`${spec.name}.${key}`, item);
228
+ if ('value' in checked)
229
+ coerced.set(key, checked.value);
230
+ else
231
+ bad.push(checked);
232
+ }
233
+ return bad.length > 0 ? bad : { value: { t: 'dict', v: coerced } };
234
+ }
235
+ case 'listStr': {
236
+ if (value.t !== 'list') {
237
+ return [{ loc: spec.name, type: 'list_type', msg: 'Input should be a valid list', input: value }];
238
+ }
239
+ // EVERY bad item, not the first: pydantic validates the whole list and reports one
240
+ // error per element, so `links: [1, 'ok', None]` is TWO errors, `links.0` and `links.2`.
241
+ const bad = [];
242
+ for (let i = 0; i < value.v.length; i += 1) {
243
+ const item = value.v[i];
244
+ if (item.t !== 'str') {
245
+ bad.push({ loc: `${spec.name}.${i}`, type: 'string_type', msg: 'Input should be a valid string', input: item });
246
+ }
247
+ }
248
+ return bad.length > 0 ? bad : { value };
249
+ }
250
+ case 'int': {
251
+ const checked = checkInt(spec.name, value);
252
+ if (!('value' in checked))
253
+ return [checked];
254
+ // `less_than_equal` names the ORIGINAL input, as every constraint error does.
255
+ if (spec.le !== undefined && checked.value.t === 'int' && checked.value.v > spec.le) {
256
+ return [
257
+ {
258
+ loc: spec.name,
259
+ type: 'less_than_equal',
260
+ msg: `Input should be less than or equal to ${spec.le}`,
261
+ input: value,
262
+ },
263
+ ];
264
+ }
265
+ return checked;
266
+ }
267
+ }
268
+ }
269
+ function checkInt(loc, value) {
270
+ const tooBig = () => ({
271
+ loc,
272
+ type: 'int_parsing_size',
273
+ msg: 'Unable to parse input string as an integer, exceeded maximum size',
274
+ input: value,
275
+ });
276
+ if (value.t === 'int') {
277
+ return value.v > I64_MAX || value.v < I64_MIN ? tooBig() : { value };
278
+ }
279
+ if (value.t === 'bool')
280
+ return { value: { t: 'int', v: value.v ? 1n : 0n } };
281
+ if (value.t === 'float') {
282
+ if (!Number.isInteger(value.v)) {
283
+ return {
284
+ loc,
285
+ type: 'int_from_float',
286
+ msg: 'Input should be a valid integer, got a number with a fractional part',
287
+ input: value,
288
+ };
289
+ }
290
+ if (!(Math.abs(value.v) <= 9223372036854775807))
291
+ return tooBig();
292
+ return { value: { t: 'int', v: BigInt(value.v) } };
293
+ }
294
+ if (value.t === 'str') {
295
+ const text = value.v.trim();
296
+ const decimal = PY_INT_DECIMAL_TEXT.exec(text);
297
+ const digits = PY_INT_TEXT.test(text) ? text : decimal?.[1];
298
+ if (digits === undefined) {
299
+ return {
300
+ loc,
301
+ type: 'int_parsing',
302
+ msg: 'Input should be a valid integer, unable to parse string as an integer',
303
+ input: value,
304
+ };
305
+ }
306
+ const parsed = BigInt(digits.replace(/_/g, ''));
307
+ return parsed > I64_MAX || parsed < I64_MIN ? tooBig() : { value: { t: 'int', v: parsed } };
308
+ }
309
+ return { loc, type: 'int_type', msg: 'Input should be a valid integer', input: value };
310
+ }
311
+ /**
312
+ * The pydantic `ValidationError` text, WITHOUT the `Error executing tool <name>: ` prefix.
313
+ *
314
+ * The prefix is the SDK's, added by `Tool.run`'s `except Exception` for every failure a tool
315
+ * can have — not just an argument one — so it belongs at the one place that catches them all.
316
+ */
317
+ export class PyValidationFailure extends Error {
318
+ detail;
319
+ constructor(detail) {
320
+ super(detail);
321
+ this.detail = detail;
322
+ this.name = 'PyValidationFailure';
323
+ }
324
+ }
325
+ /**
326
+ * `model.model_validate(arguments)` for one tool, or the exact `ValidationError` text.
327
+ *
328
+ * Unknown keys are IGNORED, because pydantic's default `model_config` ignores them and the
329
+ * measured server accepts `{"query": "a", "extra": 1}` without complaint. The absent-key case
330
+ * and the explicit-`null` case are different: `{}` is `missing`, `{"handoff_patch": null}` is
331
+ * `dict_type`, and only an OPTIONAL field treats `null` as its default.
332
+ */
333
+ export function validateArguments(model, args) {
334
+ const supplied = args.t === 'dict' ? args.v : new Map();
335
+ const errors = [];
336
+ const bound = new Map();
337
+ for (const spec of model.fields) {
338
+ if (!supplied.has(spec.name)) {
339
+ if (spec.optional) {
340
+ bound.set(spec.name, { t: 'null' });
341
+ continue;
342
+ }
343
+ errors.push({ loc: spec.name, type: 'missing', msg: 'Field required', input: args });
344
+ continue;
345
+ }
346
+ const checked = checkField(spec, supplied.get(spec.name));
347
+ if (Array.isArray(checked))
348
+ errors.push(...checked);
349
+ else
350
+ bound.set(spec.name, checked.value);
351
+ }
352
+ if (errors.length === 0)
353
+ return bound;
354
+ const count = errors.length;
355
+ const head = `${count} validation error${count === 1 ? '' : 's'} for ${model.model}`;
356
+ const body = errors
357
+ .map((e) => `${e.loc}\n ${e.msg} [type=${e.type}, input_value=${truncateRepr(reprValue(e.input))}, ` +
358
+ `input_type=${pyTypeName(e.input)}]\n For further information visit ` +
359
+ `https://errors.pydantic.dev/2.13/v/${e.type}`)
360
+ .join('\n');
361
+ throw new PyValidationFailure(`${head}\n${body}`);
362
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The SDK's serializer — which is NOT `json.dumps`, and that is the whole reason this file
3
+ * exists next to `pyjson.ts`.
4
+ *
5
+ * Two different writers reach the wire from the Python server and only one of them is
6
+ * CPython's:
7
+ *
8
+ * * every FILE bantamkit writes (`checkpoint.json`, `.log.jsonl`) goes through
9
+ * `json.dumps` — `ensure_ascii=True`, `", "`/`": "` separators, codepoint `sort_keys`.
10
+ * That is `pyjson.dumpJson`, N6's, and nothing here replaces it.
11
+ * * every JSON-RPC FRAME, and the `content[0].text` inside a tool result, goes through
12
+ * `pydantic_core.to_json` — Rust `serde_json`, which escapes nothing outside the JSON
13
+ * minimum, emits raw UTF-8, and formats floats with ryu.
14
+ *
15
+ * Measured, `mcp` 2.0.0 / pydantic-core 2.x (`scratchpad/job38-n7/tj.py`):
16
+ *
17
+ * to_json({'x': 'em — dash'}) -> {"x":"em — dash"} (NOT —)
18
+ * to_json({'k': '\x7f\x01'}) -> {"k":"?"} -> DEL RAW,  escaped
19
+ * to_json(1e-6) -> 1e-6 while repr(1e-6) is '1e-06'
20
+ * to_json(1e-5) -> 0.00001 while repr(1e-5) is '1e-05'
21
+ * to_json(1.0) -> 1.0 while String(1.0) is '1'
22
+ *
23
+ * The first two are what `JSON.stringify` already does, so strings ride on it. The last two
24
+ * are not, and are why numbers are formatted by hand: a `5.0` that arrived on the wire and
25
+ * left as `5` is exactly the defect N6 measured on the file side, relocated to the frame.
26
+ */
27
+ import type { PyValue } from '../pyjson.js';
28
+ /**
29
+ * ryu's presentation rule, as `serde_json` uses it, derived by sweeping 99 values against
30
+ * the running `pydantic_core` (`scratchpad/job38-n7/tf2.py`).
31
+ *
32
+ * It agrees with CPython's `repr` everywhere except the exponent, in two ways:
33
+ * 1. the exponent is NOT zero-padded — `1e-6`, not `1e-06`;
34
+ * 2. the decimal/scientific boundary on the small side is one decade lower — `1e-5`
35
+ * prints as `0.00001` where `repr` gives `1e-05`.
36
+ * The large side agrees: both switch to scientific at 1e16.
37
+ *
38
+ * `-0.0` keeps its sign in both. NaN and the infinities cannot occur: they are not JSON,
39
+ * every value here arrived by parsing a JSON document, and `pyjson.parseJson` rejects
40
+ * Python's `NaN`/`Infinity` extension the way a strict reader must.
41
+ */
42
+ export declare function serdeFloat(x: number): string;
43
+ /**
44
+ * `pydantic_core.to_json(value, indent=<indent>)`.
45
+ *
46
+ * `indent = null` is the compact form the JSON-RPC frame uses — `,` and `:` with NO spaces,
47
+ * which is serde's default and NOT `json.dumps`' default. `indent = 2` is the form
48
+ * `_convert_to_content` uses for the text block of every dict-returning tool.
49
+ *
50
+ * Strings go through `JSON.stringify`, which was checked against serde over the escape
51
+ * boundary: both emit the short escapes for `\b \t \n \f \r " \\`, `\u00XX` for the other
52
+ * C0 controls, and raw bytes for everything else including DEL and `/`. They part company
53
+ * only on a LONE SURROGATE, which JS escapes and pydantic-core refuses to encode at all —
54
+ * unreachable here, because a lone surrogate cannot survive UTF-8 decoding of the wire.
55
+ */
56
+ export declare function sdkJson(value: PyValue, indent?: number | null): string;
@@ -0,0 +1,91 @@
1
+ /**
2
+ * ryu's presentation rule, as `serde_json` uses it, derived by sweeping 99 values against
3
+ * the running `pydantic_core` (`scratchpad/job38-n7/tf2.py`).
4
+ *
5
+ * It agrees with CPython's `repr` everywhere except the exponent, in two ways:
6
+ * 1. the exponent is NOT zero-padded — `1e-6`, not `1e-06`;
7
+ * 2. the decimal/scientific boundary on the small side is one decade lower — `1e-5`
8
+ * prints as `0.00001` where `repr` gives `1e-05`.
9
+ * The large side agrees: both switch to scientific at 1e16.
10
+ *
11
+ * `-0.0` keeps its sign in both. NaN and the infinities cannot occur: they are not JSON,
12
+ * every value here arrived by parsing a JSON document, and `pyjson.parseJson` rejects
13
+ * Python's `NaN`/`Infinity` extension the way a strict reader must.
14
+ */
15
+ export function serdeFloat(x) {
16
+ if (!Number.isFinite(x)) {
17
+ // `to_json` writes `null` for a non-finite float; unreachable from parsed JSON, kept so
18
+ // the function is total rather than silently emitting invalid JSON.
19
+ return 'null';
20
+ }
21
+ const sign = Object.is(x, -0) || x < 0 ? '-' : '';
22
+ const abs = Math.abs(x);
23
+ if (abs === 0)
24
+ return `${sign}0.0`;
25
+ // `toExponential()` with no argument gives the SHORTEST round-tripping digits, which is
26
+ // the same digit string ryu produces; only the presentation is ours.
27
+ const [mantissa, exponentText] = abs.toExponential().split('e');
28
+ const exponent = Number(exponentText);
29
+ const digits = mantissa.replace('.', '');
30
+ if (exponent >= -5 && exponent < 16) {
31
+ if (exponent >= 0) {
32
+ const whole = digits.padEnd(exponent + 1, '0').slice(0, exponent + 1);
33
+ const fraction = digits.slice(exponent + 1);
34
+ return `${sign}${whole}.${fraction === '' ? '0' : fraction}`;
35
+ }
36
+ return `${sign}0.${'0'.repeat(-exponent - 1)}${digits}`;
37
+ }
38
+ const head = digits.length === 1 ? digits : `${digits[0]}.${digits.slice(1)}`;
39
+ return `${sign}${head}e${exponent < 0 ? '-' : '+'}${Math.abs(exponent)}`;
40
+ }
41
+ /**
42
+ * `pydantic_core.to_json(value, indent=<indent>)`.
43
+ *
44
+ * `indent = null` is the compact form the JSON-RPC frame uses — `,` and `:` with NO spaces,
45
+ * which is serde's default and NOT `json.dumps`' default. `indent = 2` is the form
46
+ * `_convert_to_content` uses for the text block of every dict-returning tool.
47
+ *
48
+ * Strings go through `JSON.stringify`, which was checked against serde over the escape
49
+ * boundary: both emit the short escapes for `\b \t \n \f \r " \\`, `\u00XX` for the other
50
+ * C0 controls, and raw bytes for everything else including DEL and `/`. They part company
51
+ * only on a LONE SURROGATE, which JS escapes and pydantic-core refuses to encode at all —
52
+ * unreachable here, because a lone surrogate cannot survive UTF-8 decoding of the wire.
53
+ */
54
+ export function sdkJson(value, indent = null) {
55
+ const pad = indent === null ? '' : ' '.repeat(indent);
56
+ const itemSep = indent === null ? ',' : ',';
57
+ const keySep = indent === null ? ':' : ': ';
58
+ const encode = (v, level) => {
59
+ switch (v.t) {
60
+ case 'null':
61
+ return 'null';
62
+ case 'bool':
63
+ return v.v ? 'true' : 'false';
64
+ case 'int':
65
+ return v.v.toString();
66
+ case 'float':
67
+ return serdeFloat(v.v);
68
+ case 'str':
69
+ return JSON.stringify(v.v);
70
+ case 'list': {
71
+ if (v.v.length === 0)
72
+ return '[]';
73
+ const parts = v.v.map((item) => encode(item, level + 1));
74
+ if (indent === null)
75
+ return `[${parts.join(itemSep)}]`;
76
+ const nl = `\n${pad.repeat(level + 1)}`;
77
+ return `[${nl}${parts.join(itemSep + nl)}\n${pad.repeat(level)}]`;
78
+ }
79
+ case 'dict': {
80
+ if (v.v.size === 0)
81
+ return '{}';
82
+ const parts = [...v.v].map(([k, item]) => `${JSON.stringify(k)}${keySep}${encode(item, level + 1)}`);
83
+ if (indent === null)
84
+ return `{${parts.join(itemSep)}}`;
85
+ const nl = `\n${pad.repeat(level + 1)}`;
86
+ return `{${nl}${parts.join(itemSep + nl)}\n${pad.repeat(level)}}`;
87
+ }
88
+ }
89
+ };
90
+ return encode(value, 0);
91
+ }
@@ -0,0 +1,45 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import { EventLog } from '../eventlog.js';
3
+ import type { Memory } from '../memory/component.js';
4
+ import type { RawStdioTransport } from './transport.js';
5
+ /**
6
+ * The eleven, in the order `build_server` lists them — which is the order `tools/list` emits.
7
+ *
8
+ * `bantamkit_status` went LAST rather than first, `memory_compact` after it rather than
9
+ * beside `memory_save` where a reader would look for it, `bantamkit_read` after that and
10
+ * `skill_audit` after that, for the same reason the reference appends all four: registration
11
+ * order IS the served order, and appending is the only edit that leaves the other ten where
12
+ * every existing declaration says they are.
13
+ */
14
+ export declare const MCP_TOOLS: readonly ["memory_save", "memory_recall", "validate_json", "shiftwork_clock_in", "shiftwork_clock_out", "shiftwork_status", "build_identity", "bantamkit_status", "memory_compact", "bantamkit_read", "skill_audit"];
15
+ /**
16
+ * The advertisement for one tool, out of its manifest, with the surface checked.
17
+ *
18
+ * Key order is `description, inputSchema, name, outputSchema` because that is the order the
19
+ * reference's pydantic `Tool` model dumps in, and the nested schemas keep the manifest's own
20
+ * key order because both runtimes preserve insertion order in a JSON object.
21
+ *
22
+ * `output_schema` is advertised VERBATIM and is not derived from anything this file knows.
23
+ * That includes `build_identity.json`'s `"title": "build_identity_toolDictOutput"`, which
24
+ * leaks the Python closure name `build_identity_tool` into a Node server's advertisement.
25
+ * IT IS REPRODUCED, NOT FIXED. The manifest is an asset, `build_identity` hashes every asset
26
+ * byte, and changing it would move `assets_digest` — the one field that is supposed to be
27
+ * comparable across the two runtimes. A cosmetic title is not worth breaking the instrument
28
+ * that detects drift. Registered here, not fixed here; `runtime-py` is the reference.
29
+ */
30
+ export declare function fromManifest(name: string): Record<string, unknown>;
31
+ /**
32
+ * Assemble the MCP server around one Memory instance (the per-person state).
33
+ *
34
+ * `wire` is the transport, and it is a parameter rather than something this function creates
35
+ * because the handlers need it twice: to read the RAW request line (so `5.0` is still a float
36
+ * when it reaches `shiftwork.clockOut`) and to register the EXACT result text (so it is still
37
+ * a float on the way back out).
38
+ *
39
+ * `log` is the event-log sink (`docs/eventlog.md`), resolved from `BANTAMKIT_EVENT_LOG` when
40
+ * the caller does not supply one and DISABLED unless that variable asks for it. It is a
41
+ * parameter and not only an environment read so that a test can inject a fixed clock and a
42
+ * scratch path without setting a process-wide variable — the same seam `runtime-py`'s
43
+ * `build_server(memory, log=None)` offers.
44
+ */
45
+ export declare function buildServer(memory: Memory, wire: RawStdioTransport, version: string, log?: EventLog): Server;