@meantjs/tools 0.0.0-stage → 0.1.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 +40 -0
- package/LICENSE +202 -0
- package/README.md +75 -2
- package/package.json +51 -4
- package/src/index.d.mts +164 -0
- package/src/index.mjs +6 -0
- package/src/input.mjs +612 -0
- package/src/layer.mjs +449 -0
- package/src/plan.mjs +459 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
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.1.0 — 2026-10-28
|
|
6
|
+
|
|
7
|
+
First published version.
|
|
8
|
+
|
|
9
|
+
Published under the `@meantjs` scope (D-051). Before any release, this package was `@candid-core/profile-tools`.
|
|
10
|
+
|
|
11
|
+
- 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
|
|
12
|
+
a value. The layer reads the set from the canister when the call is prepared (once per intent), resolves the name
|
|
13
|
+
by `resolveName` (`E_NAME_NOT_FOUND`, `E_NAME_AMBIGUOUS`, `E_NAMES_UNREAD`), treats a name and the value it stands
|
|
14
|
+
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
|
|
15
|
+
that lists it, as the layer does.
|
|
16
|
+
- Agent tools from any resolved form: one tool per query method, `prepare_<method>` per update method and `commit`,
|
|
17
|
+
with MCP tool definitions (`toolDefinitions`) and exact input reading in human units (`readToolInput`).
|
|
18
|
+
- `createToolLayer`: a confirmation hook awaited before every send, an injected encoder, call and clock, idempotent
|
|
19
|
+
commits, classified outcomes and an optional reconcile hook after an unknown outcome.
|
|
20
|
+
- No Node built-in: runs in browsers. `displayValue` shows bytes from another realm as it shows this realm's.
|
|
21
|
+
- A `principal` position with no value tag is an input in its textual form (checksum checked, canonical text out),
|
|
22
|
+
so a method that takes a principal gets a tool instead of being skipped.
|
|
23
|
+
- Interfaces beyond the ICRC packs (B2 of `docs/proposals/w9-b3wallet-gaps.md`; the packs' tools do not change): an
|
|
24
|
+
integer position with a `describe` and no value tag is an input of decimal digits; a read-only update
|
|
25
|
+
(`replicated: true`) is a read tool sent as an update call; an update without `idempotency` gets a prepare tool and
|
|
26
|
+
is never sent again after an unknown outcome; outcome keys may pass through record fields; a method with more than
|
|
27
|
+
one argument takes each as an input (`arg0`, `arg1`, ...).
|
|
28
|
+
- `encode` gets a third argument and `call` a field, `args`: the whole argument tuple. Encode it; `value` stays the
|
|
29
|
+
single argument (for a method with more than one, the tuple).
|
|
30
|
+
- A query or read-only update with no answer, or refused, says that nothing was read (it said to commit a preview).
|
|
31
|
+
- More readers (D-050, proposal `docs/proposals/w6-adoption.md` items 2 and 3): a `text` position is an input of the
|
|
32
|
+
text as given, within its `text` tag (`max_len`, `pattern`, `format`) and `range` lengths, and at most 1024
|
|
33
|
+
characters without a tag; a pattern the layer will not run (over 256 characters, not valid with the `u` flag, a
|
|
34
|
+
back reference, a lookaround, a repeated repeating group) skips the method with that reason. A `bool` position is
|
|
35
|
+
`"true"` or `"false"`. A record, variant or `vec` that no value tag reads is one JSON input whose leaves are read by
|
|
36
|
+
their own rules (`{ "tag": …, "value": … }` for a variant case; at most 256 elements in a `vec` and 4096 values in
|
|
37
|
+
one input), with issue paths into it (`$args[0].command.Follow.followees[1].id`) and a matching JSON Schema. A lone
|
|
38
|
+
`null` or `reserved` argument takes no input. An `outcome` with `default` alone classifies every reply of a method
|
|
39
|
+
with any result. Methods skipped before may now get tools; the packs' tools do not change.
|
|
40
|
+
- 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,76 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @meantjs/tools
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
|
|
50
|
+
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.
|
|
51
|
+
- **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.
|
|
52
|
+
- **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.
|
|
53
|
+
- **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.
|
|
54
|
+
- **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`.
|
|
55
|
+
|
|
56
|
+
## Outcomes
|
|
57
|
+
|
|
58
|
+
| Outcome | When | The intent |
|
|
59
|
+
|---|---|---|
|
|
60
|
+
| `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 |
|
|
61
|
+
| `success`, `already_done` | the class of the reply | done: a later commit answers `already_done` without sending |
|
|
62
|
+
| `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 |
|
|
63
|
+
| `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) |
|
|
64
|
+
| `fix_input`, `fail` | the class of the reply | settled |
|
|
65
|
+
| `not_executed` | reject code 1, 2 or 3 | unchanged |
|
|
66
|
+
| `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 |
|
|
67
|
+
| `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 |
|
|
68
|
+
|
|
69
|
+
## Tests
|
|
70
|
+
|
|
71
|
+
`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.
|
|
72
|
+
|
|
73
|
+
## Not here
|
|
74
|
+
|
|
75
|
+
- 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.
|
|
76
|
+
- 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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.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.1.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@candid-core/schema": "0.5.0",
|
|
51
|
+
"esbuild": "0.28.2"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/index.d.mts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
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
|
+
export interface ToolDefinition {
|
|
30
|
+
name: string;
|
|
31
|
+
description: string;
|
|
32
|
+
inputSchema: {
|
|
33
|
+
$schema: string;
|
|
34
|
+
type: "object";
|
|
35
|
+
properties: Record<string, JsonSchemaString>;
|
|
36
|
+
required: string[];
|
|
37
|
+
additionalProperties: false;
|
|
38
|
+
};
|
|
39
|
+
annotations: { destructiveHint: boolean; openWorldHint: boolean; readOnlyHint: boolean; title: string };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ToolParam {
|
|
43
|
+
name: string;
|
|
44
|
+
node: unknown;
|
|
45
|
+
optional: boolean;
|
|
46
|
+
/** `$args[0].amount`, or `$args[0]` for a whole argument (`$args[1]` for the second of several). */
|
|
47
|
+
path: string;
|
|
48
|
+
}
|
|
49
|
+
// A param whose node is a record, variant or vec with no value tag is one JSON input (D-050): an object, a
|
|
50
|
+
// `{ tag, value }` object, or an array, whose leaves are text read by their own rules.
|
|
51
|
+
|
|
52
|
+
export interface ToolPlan {
|
|
53
|
+
name: string;
|
|
54
|
+
kind: "read" | "prepare";
|
|
55
|
+
method: string;
|
|
56
|
+
/** The method's mode in the resolved form; a read-only update is `"update"` with `kind: "read"`. */
|
|
57
|
+
mode: string;
|
|
58
|
+
/** A read sent as an update call, so that its answer is certified (`effect.replicated`, D-038). */
|
|
59
|
+
replicated: boolean;
|
|
60
|
+
title: string;
|
|
61
|
+
effect: { kind: string; irreversible: boolean; replicated?: boolean };
|
|
62
|
+
/** The method's arguments, in order. */
|
|
63
|
+
argNodes: unknown[];
|
|
64
|
+
params: ToolParam[];
|
|
65
|
+
/** The argument record's fields, or with more than one argument the arguments (`arg0`, `arg1`, ...). */
|
|
66
|
+
/** `none`: a `null` or `reserved` position, filled with null and never asked for. */
|
|
67
|
+
fields: { name: string; node: unknown; role: "param" | "auto" | "stamp" | "hidden" | "none"; optional: boolean }[];
|
|
68
|
+
[key: string]: unknown;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface InputIssue {
|
|
72
|
+
param: string;
|
|
73
|
+
path: string;
|
|
74
|
+
/** The format library's codes, plus E_INPUT, E_UNKNOWN_PARAM, E_MISSING and E_TYPE. Inside a JSON input, `path`
|
|
75
|
+
* names the place (`$args[0].command.Follow.followees[1].id`). */
|
|
76
|
+
code: string;
|
|
77
|
+
message: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export const COMMIT_TOOL: "commit";
|
|
81
|
+
export function prepareToolName(method: string): string;
|
|
82
|
+
export function resolvedMethods(resolved: ResolvedForm | ResolvedForm[]): [string, unknown][];
|
|
83
|
+
export function buildToolPlans(resolved: ResolvedForm | ResolvedForm[]): { plans: Map<string, ToolPlan>; skipped: { method: string; reason: string }[] };
|
|
84
|
+
export function toolDefinitions(resolved: ResolvedForm | ResolvedForm[], options?: { canisters?: Canister[] }): ToolDefinition[];
|
|
85
|
+
|
|
86
|
+
export function readToolInput(
|
|
87
|
+
plan: ToolPlan,
|
|
88
|
+
input: unknown,
|
|
89
|
+
context?: { refs?: Record<string, unknown>; now?: bigint; names?: Record<string, NameEntry[]> },
|
|
90
|
+
): { value: unknown; identity: [string, unknown][] } | { issues: InputIssue[] };
|
|
91
|
+
export function toCandid(node: unknown, value: unknown): unknown;
|
|
92
|
+
export function displayValue(node: unknown, value: unknown, refs?: Record<string, unknown>, names?: Record<string, NameEntry[]>): unknown;
|
|
93
|
+
/** 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. */
|
|
94
|
+
export function nameEntries(resolved: unknown, set: string, reply: unknown): NameEntry[] | undefined;
|
|
95
|
+
export function classifyReply(plan: ToolPlan, reply: unknown): { cls: string; path: string[]; node: unknown; value: unknown };
|
|
96
|
+
|
|
97
|
+
export type CallResult =
|
|
98
|
+
| { status: "replied"; reply: unknown }
|
|
99
|
+
| { status: "rejected"; reject_code: number; message: string }
|
|
100
|
+
| { status: "unknown"; message: string };
|
|
101
|
+
|
|
102
|
+
export interface Preview {
|
|
103
|
+
preview_id: string;
|
|
104
|
+
method: string;
|
|
105
|
+
canister: string;
|
|
106
|
+
title: string;
|
|
107
|
+
effect: { kind: string; irreversible: boolean; replicated?: boolean };
|
|
108
|
+
/** The arguments as `displayValue` shows them; an array for a method with more than one. */
|
|
109
|
+
arguments: unknown;
|
|
110
|
+
arg_hex: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export type ReconcileFinding =
|
|
114
|
+
| { status: "found"; block?: bigint | string }
|
|
115
|
+
| { status: "absent" | "not_yet" }
|
|
116
|
+
| { status: "unreadable"; reason?: string };
|
|
117
|
+
|
|
118
|
+
export type Outcome =
|
|
119
|
+
| "declined"
|
|
120
|
+
| "success"
|
|
121
|
+
| "already_done"
|
|
122
|
+
| "retry_same"
|
|
123
|
+
| "retry_new"
|
|
124
|
+
| "fix_input"
|
|
125
|
+
| "fail"
|
|
126
|
+
| "not_executed"
|
|
127
|
+
| "unknown"
|
|
128
|
+
| "reconcile";
|
|
129
|
+
|
|
130
|
+
export interface ToolResult {
|
|
131
|
+
content: { type: "text"; text: string }[];
|
|
132
|
+
/** `preview` is there when a prepare tool answers `prepared` (and when an intent is handed back). */
|
|
133
|
+
structuredContent: { status?: string; outcome?: Outcome; earlier_outcome?: "unknown"; preview?: Preview; [key: string]: unknown };
|
|
134
|
+
isError?: true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ToolLayerConfig {
|
|
138
|
+
resolved: ResolvedForm | ResolvedForm[];
|
|
139
|
+
canisters: Canister[];
|
|
140
|
+
/**
|
|
141
|
+
* The method's arguments, in candid-core domain shapes, to the bytes sent: candid-core's `encodeArgs` of `args`, the
|
|
142
|
+
* whole argument tuple. `value` is the single argument (for a method with more than one, the tuple again).
|
|
143
|
+
*/
|
|
144
|
+
encode(method: string, value: unknown, args: unknown[]): Uint8Array | Promise<Uint8Array>;
|
|
145
|
+
/**
|
|
146
|
+
* Sends `arg` exactly, as a query or an update call (`mode`; a read-only update is `"update"`); `reply` is the
|
|
147
|
+
* decoded first result value in candid-core's domain shape (a variant is `{ tag, value }`).
|
|
148
|
+
*/
|
|
149
|
+
call(req: { canister: string; method: string; mode: string; arg: Uint8Array; value: unknown; args: unknown[] }): Promise<CallResult>;
|
|
150
|
+
/** The clock of the canister named, ns since the Unix epoch (canisters on different subnets keep different times). */
|
|
151
|
+
now(o: { canister: string }): Promise<bigint>;
|
|
152
|
+
/** Awaited before every send. Required when the form has an update method. */
|
|
153
|
+
confirm?(preview: Preview): Promise<boolean | { approved: boolean }>;
|
|
154
|
+
/** Consumer rule 1: look for the original in the canister's history. */
|
|
155
|
+
reconcile?(o: { canister: string; method: string; value: unknown; stamp: bigint; preview: Preview }): Promise<ReconcileFinding>;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface ToolLayer {
|
|
159
|
+
listTools(): ToolDefinition[];
|
|
160
|
+
skipped: { method: string; reason: string }[];
|
|
161
|
+
callTool(name: string, input: unknown): Promise<ToolResult>;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
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, prepareToolName, resolvedMethods, toolDefinitions } from "./plan.mjs";
|
|
5
|
+
export { displayValue, readToolInput, toCandid } from "./input.mjs";
|
|
6
|
+
export { classifyReply, createToolLayer, nameEntries } from "./layer.mjs";
|