@meantjs/tools 0.0.0-stage → 0.2.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # Changelog: @meantjs/tools
2
+
3
+ The five Candid Profile library packages (`@meantjs/profile`, `/format`, `/consent`, `/tools`, `/reconcile`) release together under one version and pin each other exactly; a version appears here even when this package did not change. Release steps: `docs/releasing.md` in the repository.
4
+
5
+ ## 0.2.0 — 2026-10-28
6
+
7
+ - One tool per case (D-053): a method whose single argument is a variant with an `intent` or an `effect` on any case
8
+ gets `<method>_<Case>` (a read) or `prepare_<method>_<Case>` for each case, instead of one tool for the method. The
9
+ plan carries `case`; `caseToolName` is exported. Resolved forms without case tags give the same tools as before.
10
+ - An EVM contract's resolved form (`"chain": "evm"`, spec §15) is refused with a `TypeError`: its tools need nonces
11
+ and receipts, a later phase.
12
+ - Types: `ToolDefinition.inputSchema.properties` is `Record<string, JsonSchemaInput>`, not strings only. Since
13
+ 0.1.0 (D-050) a record, variant or vec input is one JSON input whose schema is an object, a `oneOf` of
14
+ `{ tag, value }` cases or an array; the new exported types `JsonSchemaObject`, `JsonSchemaVariant`,
15
+ `JsonSchemaCase`, `JsonSchemaArray` and `JsonSchemaNull` say so. The generated schemas do not change.
16
+
17
+ ## 0.1.0 — 2026-10-28
18
+
19
+ First published version.
20
+
21
+ Published under the `@meantjs` scope (D-051). Before any release, this package was `@candid-core/profile-tools`.
22
+
23
+ - Named inputs (spec §5.3, D-045): where a profile tags a position `named`, a tool takes a name of that set as well as
24
+ a value. The layer reads the set from the canister when the call is prepared (once per intent), resolves the name
25
+ by `resolveName` (`E_NAME_NOT_FOUND`, `E_NAME_AMBIGUOUS`, `E_NAMES_UNREAD`), treats a name and the value it stands
26
+ for as one intent, and shows the name beside the value in previews. `nameEntries` reads a set's entries from a reply of the method
27
+ that lists it, as the layer does.
28
+ - Agent tools from any resolved form: one tool per query method, `prepare_<method>` per update method and `commit`,
29
+ with MCP tool definitions (`toolDefinitions`) and exact input reading in human units (`readToolInput`).
30
+ - `createToolLayer`: a confirmation hook awaited before every send, an injected encoder, call and clock, idempotent
31
+ commits, classified outcomes and an optional reconcile hook after an unknown outcome.
32
+ - No Node built-in: runs in browsers. `displayValue` shows bytes from another realm as it shows this realm's.
33
+ - A `principal` position with no value tag is an input in its textual form (checksum checked, canonical text out),
34
+ so a method that takes a principal gets a tool instead of being skipped.
35
+ - Interfaces beyond the ICRC packs (B2 of `docs/proposals/w9-b3wallet-gaps.md`; the packs' tools do not change): an
36
+ integer position with a `describe` and no value tag is an input of decimal digits; a read-only update
37
+ (`replicated: true`) is a read tool sent as an update call; an update without `idempotency` gets a prepare tool and
38
+ is never sent again after an unknown outcome; outcome keys may pass through record fields; a method with more than
39
+ one argument takes each as an input (`arg0`, `arg1`, ...).
40
+ - `encode` gets a third argument and `call` a field, `args`: the whole argument tuple. Encode it; `value` stays the
41
+ single argument (for a method with more than one, the tuple).
42
+ - A query or read-only update with no answer, or refused, says that nothing was read (it said to commit a preview).
43
+ - More readers (D-050, proposal `docs/proposals/w6-adoption.md` items 2 and 3): a `text` position is an input of the
44
+ text as given, within its `text` tag (`max_len`, `pattern`, `format`) and `range` lengths, and at most 1024
45
+ characters without a tag; a pattern the layer will not run (over 256 characters, not valid with the `u` flag, a
46
+ back reference, a lookaround, a repeated repeating group) skips the method with that reason. A `bool` position is
47
+ `"true"` or `"false"`. A record, variant or `vec` that no value tag reads is one JSON input whose leaves are read by
48
+ their own rules (`{ "tag": …, "value": … }` for a variant case; at most 256 elements in a `vec` and 4096 values in
49
+ one input), with issue paths into it (`$args[0].command.Follow.followees[1].id`) and a matching JSON Schema. A lone
50
+ `null` or `reserved` argument takes no input. An `outcome` with `default` alone classifies every reply of a method
51
+ with any result. Methods skipped before may now get tools; the packs' tools do not change.
52
+ - TypeScript declarations.
package/LICENSE ADDED
@@ -0,0 +1,202 @@
1
+
2
+ Apache License
3
+ Version 2.0, January 2004
4
+ http://www.apache.org/licenses/
5
+
6
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
7
+
8
+ 1. Definitions.
9
+
10
+ "License" shall mean the terms and conditions for use, reproduction,
11
+ and distribution as defined by Sections 1 through 9 of this document.
12
+
13
+ "Licensor" shall mean the copyright owner or entity authorized by
14
+ the copyright owner that is granting the License.
15
+
16
+ "Legal Entity" shall mean the union of the acting entity and all
17
+ other entities that control, are controlled by, or are under common
18
+ control with that entity. For the purposes of this definition,
19
+ "control" means (i) the power, direct or indirect, to cause the
20
+ direction or management of such entity, whether by contract or
21
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
22
+ outstanding shares, or (iii) beneficial ownership of such entity.
23
+
24
+ "You" (or "Your") shall mean an individual or Legal Entity
25
+ exercising permissions granted by this License.
26
+
27
+ "Source" form shall mean the preferred form for making modifications,
28
+ including but not limited to software source code, documentation
29
+ source, and configuration files.
30
+
31
+ "Object" form shall mean any form resulting from mechanical
32
+ transformation or translation of a Source form, including but
33
+ not limited to compiled object code, generated documentation,
34
+ and conversions to other media types.
35
+
36
+ "Work" shall mean the work of authorship, whether in Source or
37
+ Object form, made available under the License, as indicated by a
38
+ copyright notice that is included in or attached to the work
39
+ (an example is provided in the Appendix below).
40
+
41
+ "Derivative Works" shall mean any work, whether in Source or Object
42
+ form, that is based on (or derived from) the Work and for which the
43
+ editorial revisions, annotations, elaborations, or other modifications
44
+ represent, as a whole, an original work of authorship. For the purposes
45
+ of this License, Derivative Works shall not include works that remain
46
+ separable from, or merely link (or bind by name) to the interfaces of,
47
+ the Work and Derivative Works thereof.
48
+
49
+ "Contribution" shall mean any work of authorship, including
50
+ the original version of the Work and any modifications or additions
51
+ to that Work or Derivative Works thereof, that is intentionally
52
+ submitted to Licensor for inclusion in the Work by the copyright owner
53
+ or by an individual or Legal Entity authorized to submit on behalf of
54
+ the copyright owner. For the purposes of this definition, "submitted"
55
+ means any form of electronic, verbal, or written communication sent
56
+ to the Licensor or its representatives, including but not limited to
57
+ communication on electronic mailing lists, source code control systems,
58
+ and issue tracking systems that are managed by, or on behalf of, the
59
+ Licensor for the purpose of discussing and improving the Work, but
60
+ excluding communication that is conspicuously marked or otherwise
61
+ designated in writing by the copyright owner as "Not a Contribution."
62
+
63
+ "Contributor" shall mean Licensor and any individual or Legal Entity
64
+ on behalf of whom a Contribution has been received by Licensor and
65
+ subsequently incorporated within the Work.
66
+
67
+ 2. Grant of Copyright License. Subject to the terms and conditions of
68
+ this License, each Contributor hereby grants to You a perpetual,
69
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
70
+ copyright license to reproduce, prepare Derivative Works of,
71
+ publicly display, publicly perform, sublicense, and distribute the
72
+ Work and such Derivative Works in Source or Object form.
73
+
74
+ 3. Grant of Patent License. Subject to the terms and conditions of
75
+ this License, each Contributor hereby grants to You a perpetual,
76
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
77
+ (except as stated in this section) patent license to make, have made,
78
+ use, offer to sell, sell, import, and otherwise transfer the Work,
79
+ where such license applies only to those patent claims licensable
80
+ by such Contributor that are necessarily infringed by their
81
+ Contribution(s) alone or by combination of their Contribution(s)
82
+ with the Work to which such Contribution(s) was submitted. If You
83
+ institute patent litigation against any entity (including a
84
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
85
+ or a Contribution incorporated within the Work constitutes direct
86
+ or contributory patent infringement, then any patent licenses
87
+ granted to You under this License for that Work shall terminate
88
+ as of the date such litigation is filed.
89
+
90
+ 4. Redistribution. You may reproduce and distribute copies of the
91
+ Work or Derivative Works thereof in any medium, with or without
92
+ modifications, and in Source or Object form, provided that You
93
+ meet the following conditions:
94
+
95
+ (a) You must give any other recipients of the Work or
96
+ Derivative Works a copy of this License; and
97
+
98
+ (b) You must cause any modified files to carry prominent notices
99
+ stating that You changed the files; and
100
+
101
+ (c) You must retain, in the Source form of any Derivative Works
102
+ that You distribute, all copyright, patent, trademark, and
103
+ attribution notices from the Source form of the Work,
104
+ excluding those notices that do not pertain to any part of
105
+ the Derivative Works; and
106
+
107
+ (d) If the Work includes a "NOTICE" text file as part of its
108
+ distribution, then any Derivative Works that You distribute must
109
+ include a readable copy of the attribution notices contained
110
+ within such NOTICE file, excluding those notices that do not
111
+ pertain to any part of the Derivative Works, in at least one
112
+ of the following places: within a NOTICE text file distributed
113
+ as part of the Derivative Works; within the Source form or
114
+ documentation, if provided along with the Derivative Works; or,
115
+ within a display generated by the Derivative Works, if and
116
+ wherever such third-party notices normally appear. The contents
117
+ of the NOTICE file are for informational purposes only and
118
+ do not modify the License. You may add Your own attribution
119
+ notices within Derivative Works that You distribute, alongside
120
+ or as an addendum to the NOTICE text from the Work, provided
121
+ that such additional attribution notices cannot be construed
122
+ as modifying the License.
123
+
124
+ You may add Your own copyright statement to Your modifications and
125
+ may provide additional or different license terms and conditions
126
+ for use, reproduction, or distribution of Your modifications, or
127
+ for any such Derivative Works as a whole, provided Your use,
128
+ reproduction, and distribution of the Work otherwise complies with
129
+ the conditions stated in this License.
130
+
131
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
132
+ any Contribution intentionally submitted for inclusion in the Work
133
+ by You to the Licensor shall be under the terms and conditions of
134
+ this License, without any additional terms or conditions.
135
+ Notwithstanding the above, nothing herein shall supersede or modify
136
+ the terms of any separate license agreement you may have executed
137
+ with Licensor regarding such Contributions.
138
+
139
+ 6. Trademarks. This License does not grant permission to use the trade
140
+ names, trademarks, service marks, or product names of the Licensor,
141
+ except as required for reasonable and customary use in describing the
142
+ origin of the Work and reproducing the content of the NOTICE file.
143
+
144
+ 7. Disclaimer of Warranty. Unless required by applicable law or
145
+ agreed to in writing, Licensor provides the Work (and each
146
+ Contributor provides its Contributions) on an "AS IS" BASIS,
147
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
148
+ implied, including, without limitation, any warranties or conditions
149
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
150
+ PARTICULAR PURPOSE. You are solely responsible for determining the
151
+ appropriateness of using or redistributing the Work and assume any
152
+ risks associated with Your exercise of permissions under this License.
153
+
154
+ 8. Limitation of Liability. In no event and under no legal theory,
155
+ whether in tort (including negligence), contract, or otherwise,
156
+ unless required by applicable law (such as deliberate and grossly
157
+ negligent acts) or agreed to in writing, shall any Contributor be
158
+ liable to You for damages, including any direct, indirect, special,
159
+ incidental, or consequential damages of any character arising as a
160
+ result of this License or out of the use or inability to use the
161
+ Work (including but not limited to damages for loss of goodwill,
162
+ work stoppage, computer failure or malfunction, or any and all
163
+ other commercial damages or losses), even if such Contributor
164
+ has been advised of the possibility of such damages.
165
+
166
+ 9. Accepting Warranty or Additional Liability. While redistributing
167
+ the Work or Derivative Works thereof, You may choose to offer,
168
+ and charge a fee for, acceptance of support, warranty, indemnity,
169
+ or other liability obligations and/or rights consistent with this
170
+ License. However, in accepting such obligations, You may act only
171
+ on Your own behalf and on Your sole responsibility, not on behalf
172
+ of any other Contributor, and only if You agree to indemnify,
173
+ defend, and hold each Contributor harmless for any liability
174
+ incurred by, or claims asserted against, such Contributor by reason
175
+ of your accepting any such warranty or additional liability.
176
+
177
+ END OF TERMS AND CONDITIONS
178
+
179
+ APPENDIX: How to apply the Apache License to your work.
180
+
181
+ To apply the Apache License to your work, attach the following
182
+ boilerplate notice, with the fields enclosed by brackets "[]"
183
+ replaced with your own identifying information. (Don't include
184
+ the brackets!) The text should be enclosed in the appropriate
185
+ comment syntax for the file format. We also recommend that a
186
+ file or class name and description of purpose be included on the
187
+ same "printed page" as the copyright notice for easier
188
+ identification within third-party archives.
189
+
190
+ Copyright [yyyy] [name of copyright owner]
191
+
192
+ Licensed under the Apache License, Version 2.0 (the "License");
193
+ you may not use this file except in compliance with the License.
194
+ You may obtain a copy of the License at
195
+
196
+ http://www.apache.org/licenses/LICENSE-2.0
197
+
198
+ Unless required by applicable law or agreed to in writing, software
199
+ distributed under the License is distributed on an "AS IS" BASIS,
200
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201
+ See the License for the specific language governing permissions and
202
+ limitations under the License.
package/README.md CHANGED
@@ -1,3 +1,77 @@
1
- # Temporary Holding Version
1
+ # @meantjs/tools
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Agent tools from any Candid Profile resolved form, for an app or an agent host: MCP tool definitions, input reading in human units, a preview that a person confirms before every send, and the retry and reconcile rules of spec section 7.3. The design and the readings it takes on open questions are in `docs/proposals/w3b-tools-package.md` in the repository; the eval's own tool arm (arm C) is a separate, frozen copy.
4
+
5
+ ```sh
6
+ npm install @meantjs/tools
7
+ ```
8
+
9
+ - **Runs on:** Node 22 or later and browsers: no `node:*` import, no `Buffer`, no `process` (a test bundles it for the
10
+ browser with esbuild).
11
+ - **Module format:** ES modules only, with hand-written TypeScript declarations for every entry.
12
+ - **Spec:** [Candid Profile v0](https://github.com/B3Pay/Meant/blob/main/profile/docs/profile-v0.md). **Licence:** Apache-2.0.
13
+ - **Versions:** the five Candid Profile packages (`@meantjs/profile`, `/format`, `/consent`, `/tools`,
14
+ `/reconcile`) release together under one version and pin each other exactly; in 0.x a minor is breaking,
15
+ and so is any change to generated text ([releasing](https://github.com/B3Pay/Meant/blob/main/profile/docs/releasing.md)).
16
+
17
+ ## API
18
+
19
+ ```js
20
+ import { createToolLayer, toolDefinitions, readToolInput, buildToolPlans } from "@meantjs/tools";
21
+
22
+ const canisters = [{ id: "ryjl3-tyaaa-aaaaa-aaaba-cai", label: "TKA", refs: { "query:icrc1_decimals": 8, "query:icrc1_symbol": "TKA" } }];
23
+
24
+ toolDefinitions([icrc1Resolved, icrc2Resolved], { canisters }); // MCP tool definitions, sorted by name
25
+
26
+ const layer = createToolLayer({
27
+ resolved: [icrc1Resolved, icrc2Resolved],
28
+ canisters,
29
+ encode: (method, value, args) => encodeArgs(methods.get(method).args, args).bytes, // candid-core's encoder
30
+ call: async ({ canister, method, mode, arg, value, args }) => ({ status: "replied", reply }), // or rejected / unknown
31
+ now: async ({ canister }) => canisterTimeNs, // that canister's clock, bigint ns
32
+ confirm: async (preview) => askThePerson(preview), // awaited before every send
33
+ reconcile: async ({ canister, method, value, stamp }) => ({ status: "absent" }), // optional, consumer rule 1
34
+ });
35
+ await layer.callTool("prepare_icrc1_transfer", { canister_id: canisters[0].id, to: "…", amount: "1.5 TKA" });
36
+ await layer.callTool("commit", { preview_id: "p1" });
37
+ ```
38
+
39
+ - **Tools.** One per query method (named as the method), one `prepare_<method>` per update method, and `commit`, with arm C's rules for which positions are inputs (a value-tagged position is a text input in human units; the idempotency stamp, `auto` defaults and `ui.hidden` positions are filled by the layer). On the pack fixtures the tools, their inputs and their required sets equal `armCToolDefinitions()` (tested). Beyond arm C, for interfaces the packs do not cover (the last five are items 1, 2, 4, 5 and 6 of B2 in `docs/proposals/w9-b3wallet-gaps.md`; the packs meet none of them, so their tools do not change):
40
+ - **A principal.** A `principal` position with no value tag is a text input in the principal's textual form. It has exactly one, with a checksum, so it is read exactly without a profile tag (spec 5.3 has none for it): the input is trimmed, its case ignored, its checksum checked (`E_PRINCIPAL`), and the value is the canonical text. Its schema gives the grammar, two examples and a `pattern`, and a `describe` label goes in front as for any input.
41
+ - **An integer the profile describes.** An integer position (`nat`, `int` and their sized forms) with no value tag but a `describe` is a text input of decimal digits, read as they are: trimmed, no unit, separators, exponent, `+` or leading zeros (`E_SYNTAX`), within the Candid type's bounds (`E_RANGE`); a whole argument of this kind is the input `integer`. The words say what the number is (an action id, a page size), so an integer without a `describe` still has no rule.
42
+ - **A read-only update** (`effect: { kind: "read", irreversible: false, replicated: true }`, D-038) is a read tool named as the method, sent once as an update call (`call` gets `mode: "update"`), so that the answer is certified; no preview, no confirmation. With an idempotency stamp it is skipped.
43
+ - **An update without `idempotency`** gets a prepare tool too, whose description says it has no stamp. Its first send is like any other; after an unknown outcome it is never sent again (spec 5.3: without the tag, an update with unknown outcome is not retried automatically), see `reconcile` below. With an outcome class `retry_new`, which renews a stamp, it is skipped.
44
+ - **Outcome keys through record fields.** A key may pass through a record field to a case (`Ok.state.Failed`). The classifier follows the reply's cases and, at a record, the field the keys read there; a key that names no case or field, ends on a field, or reads another field of a record than an earlier key does (one reply could match both) skips the method.
45
+ - **More than one argument.** Each argument is an input of its own, `arg0`, `arg1` and so on, read by the rules above; an idempotency stamp skips the method.
46
+ - **Text and bool** (D-050). A `text` position is a text input taken as given (not trimmed), checked against its `text` tag: `max_len` and `range` lengths in code points (1024 without either), `pattern` (unanchored, with the `u` flag) and `format` (`url`: an http or https URL; `email`); lone surrogates are refused. A pattern the layer will not run skips the method with the reason: over 256 characters, not valid with the `u` flag, a back reference, a lookaround, or a group that repeats inside a group that repeats (exponential backtracking). A `bool` position is `"true"` or `"false"`.
47
+ - **Nested arguments** (D-050). A record, variant or `vec` that no value tag reads (an account record is still one text input) is one input in JSON: an object for a record (unknown keys refused), `{ "tag": "Case", "value": … }` for a variant case (no `value` for a `null` case), an array for a `vec` (`null` for an element that is none), and each leaf read by its own rule above, as text. At most 256 elements in a `vec` and 4096 values in one input. An issue names its place (`$args[0].command.Follow.followees[1].id`). The input's JSON Schema mirrors the shape (`oneOf` per case, `maxItems`) with each leaf's own schema. A `null` or `reserved` field takes no input, and neither does a lone `null` or `reserved` argument. A recursive position, or a field or case named by a number, still skips the method.
48
+ - **An outcome with `default` alone** (D-049): every reply of the method is that class, whatever its result; outcome keys still need a method with one result.
49
+ - **One tool per case** (D-053, rule 3 of B2). A method whose single argument is a variant with an `intent` or an `effect` on any of its cases gets one tool for each case instead of one for the method: `<method>_<Case>` when the case's effect is a read (every case of a query, or a read-only update case), else `prepare_<method>_<Case>`. Its inputs are the case's payload, read as a single argument is (a record's fields, one value-tagged input, none for a `null` case, else one JSON input), and it sends `{ tag: Case, value }` (a bare `{ tag }` for a `null` case). Its title is the case's intent, else its `describe`, else `<method> <Case>`; its effect the case's, else the method's; the method's `outcome` applies, and a prepare tool needs an intent, the case's or the method's. The case is part of the intent's identity, so two cases with the same inputs are two previews. A case without a rule, or without a name (`#3`), is skipped alone, with `case <Case>:` in front of the reason; a method with an idempotency stamp gets no case tools yet (the stamp would sit inside a case). A tagged variant deeper in the argument (inside a record) stays one JSON input.
50
+
51
+ A method with no rule (including a query with more than one result) is skipped and reported in `buildToolPlans(resolved).skipped`, not thrown on; so is every method whose tool name is `commit` or is another method's tool name, every method with an argument field named `canister_id` (the canister selector's input), and every update whose stamp is the whole argument rather than a field (a required stamp field is filled like an optional one). The stamp and the outcome keys are relpaths, matched by the position they resolve to: `(0).Ok`, `.Ok` and `Ok` are one key.
52
+ - **Input words.** Each input's description is the format library's `Formatter.describe()`, with the canisters' decimals and symbol filled in when they agree (and its regular-expression `pattern` for amounts and subaccounts). When the canisters disagree the words give the grammar without the numbers.
53
+ - **Reading.** `readToolInput(plan, input, { refs, now })` returns `{ value, identity }` or `{ issues: [{ param, path, code, message }] }`, every issue at once, with the format library's codes. `value` is candid-core's domain shape (`T | null` opts, `bigint` for nat/int/nat64/int64, `Uint8Array` blobs, principals as text), so `encodeArgs` takes it: the single argument, or for a method with more than one the argument tuple.
54
+ - **Sending.** `encode(method, value, args)` is injected, so the bytes are the host's encoder's (candid-core's `encodeArgs`, as ic-reactor sends; a test checks every send against it). `args` is the whole argument tuple (empty, one argument, or several); encode it. `value` is the single argument, kept for hosts that encode `[value]`; for a method with more than one argument it is the tuple again, so such a host's `encodeArgs(types, [value])` is refused (`invalid_length`, observed with candid-core) rather than encoded wrongly. `call` receives the bytes, `mode` (`"query"`, or `"update"` for a commit and for a read-only update), and the `value` and `args` they encode. Results are `{ content, structuredContent, isError? }`: structured data with a short text beside it.
55
+ - **Confirmation.** `confirm(preview)` is awaited before every send, including a re-send. There is no auto-approve default: a form with an update method needs it. A decline sends nothing and answers `declined`.
56
+
57
+ ## Outcomes
58
+
59
+ | Outcome | When | The intent |
60
+ |---|---|---|
61
+ | `declined` | `confirm` said no | settled if it had no attempt, else unchanged; after an unknown outcome the result also carries `earlier_outcome: "unknown"` (the earlier attempt may have executed). A stamp declined before any send sets no floor for later stamps of the same arguments, and is never handed out again |
62
+ | `success`, `already_done` | the class of the reply | done: a later commit answers `already_done` without sending |
63
+ | `retry_same` | the class of the reply | unchanged: handed back, nothing sleeps here. The third one stops the intent (consumer rule 3): `reconcile` after an unknown outcome, else `fail` with `retries_spent`, settled |
64
+ | `retry_new` | the class of the reply | a new preview with a new stamp, relative times resolved again; the old preview is retired. A `CreatedInFuture` re-stamp is capped by its `ledger_time` (only that case, matched by its name) |
65
+ | `fix_input`, `fail` | the class of the reply | settled |
66
+ | `not_executed` | reject code 1, 2 or 3 | unchanged |
67
+ | `unknown` | for an update with an idempotency stamp: no answer, or any reject code other than 1 to 3 (4, 5, 6 and any unlisted code; or any reject from `aaaaa-aa`); a `call` that throws is answered the same (`message` is the error's) | unknown: commit again sends the same bytes; preparing it again returns the same preview with outcome `unknown`, as does any later preparation of an intent that had one |
68
+ | `reconcile` | after an unknown outcome, any answer but success, already_done or retry_same (retry_same resends the same bytes and stamp, so the ledger's deduplication answers Duplicate within the window and TooOld after it); or a third reject 4/5 of the intent. For an update without an idempotency stamp, at once, in place of `unknown`: the finding is `unreadable` with the reason, and the `reconcile` hook is not called, since there is no stamp to match | held until `reconcile` finds the original (then done) or reports it absent (then settled); without a stamp, held: no commit or preparation of it sends again |
69
+
70
+ ## Tests
71
+
72
+ `npm test -w packages/tools`: definitions against arm C's, input reading against candid-core's encoder, every outcome row above against a scripted ledger, concurrent preparations and commits, a `call` that throws, the hand-written types against the runtime exports, and the browser bundle. **Observed (2026-10-14, while writing them):** each guard in `src/layer.mjs` was removed once and its test failed; the four regression tests from the review fail on the earlier code. `test/interface-rules.test.mjs` runs the B2 rules on a wallet-like form; **observed (2026-10-24):** each of its ten guards (the update call for a read-only update, the hold after an unknown outcome, a reject 4 and the hook's skip without a stamp, the `describe` requirement, the two record-key checks, the argument tuple, the `retry_new` and stamped-read skips) was removed once and a test failed. `test/readers.test.mjs` runs the D-050 readers, and every value they prepare is encoded by candid-core; **observed (2026-10-27):** removing the bare `{ tag }` for a `null` case, the repeat check on patterns, or the lone-surrogate check made a test fail.
73
+
74
+ ## Not here
75
+
76
+ - The demo's agent tools are not switched to this package yet: its tools also read address-book names and choose the calling identity per call (the payer question of #10), and its write engine is the app's.
77
+ - Arm C keeps its own code; rebuilding it on this package before the freeze, with re-pinned hashes, is recommended in the proposal (D-015 reruns nothing while no run has started).
package/package.json CHANGED
@@ -1,6 +1,53 @@
1
1
  {
2
2
  "name": "@meantjs/tools",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "Agent tools from any Candid Profile resolved form: tool definitions, exact input reading, and a tool layer with a confirmation hook, an injected encoder, call and clock. Browser-safe.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/B3Pay/Meant.git",
9
+ "directory": "profile/packages/tools"
10
+ },
11
+ "homepage": "https://github.com/B3Pay/Meant/tree/main/profile/packages/tools#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/B3Pay/Meant/issues"
14
+ },
15
+ "keywords": [
16
+ "candid",
17
+ "internet-computer",
18
+ "icp",
19
+ "agent",
20
+ "mcp",
21
+ "tools",
22
+ "wallet"
23
+ ],
24
+ "type": "module",
25
+ "sideEffects": false,
26
+ "main": "./src/index.mjs",
27
+ "types": "./src/index.d.mts",
28
+ "exports": {
29
+ ".": {
30
+ "types": "./src/index.d.mts",
31
+ "default": "./src/index.mjs"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "src",
37
+ "CHANGELOG.md",
38
+ "LICENSE"
39
+ ],
40
+ "engines": {
41
+ "node": ">=22"
42
+ },
43
+ "scripts": {
44
+ "test": "node --test \"test/*.test.mjs\""
45
+ },
46
+ "dependencies": {
47
+ "@meantjs/format": "0.2.0"
48
+ },
49
+ "devDependencies": {
50
+ "@candid-core/schema": "0.5.0",
51
+ "esbuild": "0.28.2"
52
+ }
53
+ }
@@ -0,0 +1,205 @@
1
+ // Types for @meantjs/tools. Hand-written; the source is plain ES modules (src/*.mjs).
2
+
3
+ import type { NameEntry } from "@meantjs/format";
4
+
5
+ /** A candid-profile-resolved v0 document (docs/profile-v0.md section 10). */
6
+ export interface ResolvedForm {
7
+ format: "candid-profile-resolved";
8
+ format_version: 0;
9
+ methods: Record<string, unknown>;
10
+ [key: string]: unknown;
11
+ }
12
+
13
+ export interface Canister {
14
+ /** Principal text of the canister. */
15
+ id: string;
16
+ /** A short name for people and models, such as the token's symbol. */
17
+ label?: string;
18
+ /** The canister's dynamic tag values, keyed by their `from`: `{ "query:icrc1_decimals": 8 }`. */
19
+ refs?: Record<string, unknown>;
20
+ }
21
+
22
+ export interface JsonSchemaString {
23
+ type: "string";
24
+ description: string;
25
+ enum?: string[];
26
+ pattern?: string;
27
+ }
28
+
29
+ /** The schema of one input: text read by its own rules, or the JSON shape of a record, variant or vec (D-050). */
30
+ export type JsonSchemaInput = JsonSchemaString | JsonSchemaObject | JsonSchemaVariant | JsonSchemaArray;
31
+
32
+ /** A record: one property per field that is an input. */
33
+ export interface JsonSchemaObject {
34
+ type: "object";
35
+ description: string;
36
+ properties: Record<string, JsonSchemaInput>;
37
+ required: string[];
38
+ additionalProperties: false;
39
+ }
40
+
41
+ /** A variant: one `{ tag, value }` object per case; a case without a value has `tag` alone. */
42
+ export interface JsonSchemaVariant {
43
+ oneOf: JsonSchemaCase[];
44
+ description: string;
45
+ }
46
+
47
+ export interface JsonSchemaCase {
48
+ type: "object";
49
+ properties: { tag: { const: string }; value?: JsonSchemaInput };
50
+ required: ["tag"] | ["tag", "value"];
51
+ additionalProperties: false;
52
+ }
53
+
54
+ export interface JsonSchemaNull {
55
+ type: "null";
56
+ }
57
+
58
+ /** A vec: an element that is none is a JSON null, so an opt element is its schema or null. */
59
+ export interface JsonSchemaArray {
60
+ type: "array";
61
+ description: string;
62
+ items: JsonSchemaInput | JsonSchemaNull | { anyOf: [JsonSchemaInput, JsonSchemaNull] };
63
+ maxItems: number;
64
+ }
65
+
66
+ export interface ToolDefinition {
67
+ name: string;
68
+ description: string;
69
+ inputSchema: {
70
+ $schema: string;
71
+ type: "object";
72
+ properties: Record<string, JsonSchemaInput>;
73
+ required: string[];
74
+ additionalProperties: false;
75
+ };
76
+ annotations: { destructiveHint: boolean; openWorldHint: boolean; readOnlyHint: boolean; title: string };
77
+ }
78
+
79
+ export interface ToolParam {
80
+ name: string;
81
+ node: unknown;
82
+ optional: boolean;
83
+ /** `$args[0].amount`, or `$args[0]` for a whole argument (`$args[1]` for the second of several). */
84
+ path: string;
85
+ }
86
+ // A param whose node is a record, variant or vec with no value tag is one JSON input (D-050): an object, a
87
+ // `{ tag, value }` object, or an array, whose leaves are text read by their own rules.
88
+
89
+ export interface ToolPlan {
90
+ name: string;
91
+ kind: "read" | "prepare";
92
+ method: string;
93
+ /** The method's mode in the resolved form; a read-only update is `"update"` with `kind: "read"`. */
94
+ mode: string;
95
+ /** A read sent as an update call, so that its answer is certified (`effect.replicated`, D-038). */
96
+ replicated: boolean;
97
+ title: string;
98
+ effect: { kind: string; irreversible: boolean; replicated?: boolean };
99
+ /** For a tool per case of the method's variant argument (D-053): the case it sends. */
100
+ case?: string;
101
+ /** The method's arguments, in order. */
102
+ argNodes: unknown[];
103
+ params: ToolParam[];
104
+ /** The argument record's fields, or with more than one argument the arguments (`arg0`, `arg1`, ...). */
105
+ /** `none`: a `null` or `reserved` position, filled with null and never asked for. */
106
+ fields: { name: string; node: unknown; role: "param" | "auto" | "stamp" | "hidden" | "none"; optional: boolean }[];
107
+ [key: string]: unknown;
108
+ }
109
+
110
+ export interface InputIssue {
111
+ param: string;
112
+ path: string;
113
+ /** The format library's codes, plus E_INPUT, E_UNKNOWN_PARAM, E_MISSING and E_TYPE. Inside a JSON input, `path`
114
+ * names the place (`$args[0].command.Follow.followees[1].id`). */
115
+ code: string;
116
+ message: string;
117
+ }
118
+
119
+ export const COMMIT_TOOL: "commit";
120
+ export function prepareToolName(method: string): string;
121
+ /** The tool of one case of a method's variant argument (D-053): `<method>_<Case>` for a read, else `prepare_<method>_<Case>`. */
122
+ export function caseToolName(method: string, label: string, read: boolean): string;
123
+ export function resolvedMethods(resolved: ResolvedForm | ResolvedForm[]): [string, unknown][];
124
+ export function buildToolPlans(resolved: ResolvedForm | ResolvedForm[]): { plans: Map<string, ToolPlan>; skipped: { method: string; reason: string }[] };
125
+ export function toolDefinitions(resolved: ResolvedForm | ResolvedForm[], options?: { canisters?: Canister[] }): ToolDefinition[];
126
+
127
+ export function readToolInput(
128
+ plan: ToolPlan,
129
+ input: unknown,
130
+ context?: { refs?: Record<string, unknown>; now?: bigint; names?: Record<string, NameEntry[]> },
131
+ ): { value: unknown; identity: [string, unknown][] } | { issues: InputIssue[] };
132
+ export function toCandid(node: unknown, value: unknown): unknown;
133
+ export function displayValue(node: unknown, value: unknown, refs?: Record<string, unknown>, names?: Record<string, NameEntry[]>): unknown;
134
+ /** The entries of a names set (spec 5.3, D-045) in a reply of the method that lists it; undefined without the set or a list. */
135
+ export function nameEntries(resolved: unknown, set: string, reply: unknown): NameEntry[] | undefined;
136
+ export function classifyReply(plan: ToolPlan, reply: unknown): { cls: string; path: string[]; node: unknown; value: unknown };
137
+
138
+ export type CallResult =
139
+ | { status: "replied"; reply: unknown }
140
+ | { status: "rejected"; reject_code: number; message: string }
141
+ | { status: "unknown"; message: string };
142
+
143
+ export interface Preview {
144
+ preview_id: string;
145
+ method: string;
146
+ canister: string;
147
+ title: string;
148
+ effect: { kind: string; irreversible: boolean; replicated?: boolean };
149
+ /** The arguments as `displayValue` shows them; an array for a method with more than one. */
150
+ arguments: unknown;
151
+ arg_hex: string;
152
+ }
153
+
154
+ export type ReconcileFinding =
155
+ | { status: "found"; block?: bigint | string }
156
+ | { status: "absent" | "not_yet" }
157
+ | { status: "unreadable"; reason?: string };
158
+
159
+ export type Outcome =
160
+ | "declined"
161
+ | "success"
162
+ | "already_done"
163
+ | "retry_same"
164
+ | "retry_new"
165
+ | "fix_input"
166
+ | "fail"
167
+ | "not_executed"
168
+ | "unknown"
169
+ | "reconcile";
170
+
171
+ export interface ToolResult {
172
+ content: { type: "text"; text: string }[];
173
+ /** `preview` is there when a prepare tool answers `prepared` (and when an intent is handed back). */
174
+ structuredContent: { status?: string; outcome?: Outcome; earlier_outcome?: "unknown"; preview?: Preview; [key: string]: unknown };
175
+ isError?: true;
176
+ }
177
+
178
+ export interface ToolLayerConfig {
179
+ resolved: ResolvedForm | ResolvedForm[];
180
+ canisters: Canister[];
181
+ /**
182
+ * The method's arguments, in candid-core domain shapes, to the bytes sent: candid-core's `encodeArgs` of `args`, the
183
+ * whole argument tuple. `value` is the single argument (for a method with more than one, the tuple again).
184
+ */
185
+ encode(method: string, value: unknown, args: unknown[]): Uint8Array | Promise<Uint8Array>;
186
+ /**
187
+ * Sends `arg` exactly, as a query or an update call (`mode`; a read-only update is `"update"`); `reply` is the
188
+ * decoded first result value in candid-core's domain shape (a variant is `{ tag, value }`).
189
+ */
190
+ call(req: { canister: string; method: string; mode: string; arg: Uint8Array; value: unknown; args: unknown[] }): Promise<CallResult>;
191
+ /** The clock of the canister named, ns since the Unix epoch (canisters on different subnets keep different times). */
192
+ now(o: { canister: string }): Promise<bigint>;
193
+ /** Awaited before every send. Required when the form has an update method. */
194
+ confirm?(preview: Preview): Promise<boolean | { approved: boolean }>;
195
+ /** Consumer rule 1: look for the original in the canister's history. */
196
+ reconcile?(o: { canister: string; method: string; value: unknown; stamp: bigint; preview: Preview }): Promise<ReconcileFinding>;
197
+ }
198
+
199
+ export interface ToolLayer {
200
+ listTools(): ToolDefinition[];
201
+ skipped: { method: string; reason: string }[];
202
+ callTool(name: string, input: unknown): Promise<ToolResult>;
203
+ }
204
+
205
+ export function createToolLayer(config: ToolLayerConfig): ToolLayer;
package/src/index.mjs ADDED
@@ -0,0 +1,6 @@
1
+ // @meantjs/tools: agent tools from any resolved form, browser-safe (#16).
2
+ // Arm C (packages/mcp-adapters) stays the eval arm with its frozen texts; this is the product tool layer.
3
+
4
+ export { COMMIT_TOOL, buildToolPlans, caseToolName, prepareToolName, resolvedMethods, toolDefinitions } from "./plan.mjs";
5
+ export { displayValue, readToolInput, toCandid } from "./input.mjs";
6
+ export { classifyReply, createToolLayer, nameEntries } from "./layer.mjs";