@meantjs/tools 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +1 -0
- package/package.json +2 -2
- package/src/index.d.mts +42 -1
- package/src/index.mjs +1 -1
- package/src/input.mjs +11 -5
- package/src/plan.mjs +125 -54
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
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
4
|
|
|
5
|
+
## 0.3.0 — 2026-10-29
|
|
6
|
+
|
|
7
|
+
No changes in this package.
|
|
8
|
+
|
|
9
|
+
## 0.2.0 — 2026-10-28
|
|
10
|
+
|
|
11
|
+
- One tool per case (D-053): a method whose single argument is a variant with an `intent` or an `effect` on any case
|
|
12
|
+
gets `<method>_<Case>` (a read) or `prepare_<method>_<Case>` for each case, instead of one tool for the method. The
|
|
13
|
+
plan carries `case`; `caseToolName` is exported. Resolved forms without case tags give the same tools as before.
|
|
14
|
+
- An EVM contract's resolved form (`"chain": "evm"`, spec §15) is refused with a `TypeError`: its tools need nonces
|
|
15
|
+
and receipts, a later phase.
|
|
16
|
+
- Types: `ToolDefinition.inputSchema.properties` is `Record<string, JsonSchemaInput>`, not strings only. Since
|
|
17
|
+
0.1.0 (D-050) a record, variant or vec input is one JSON input whose schema is an object, a `oneOf` of
|
|
18
|
+
`{ tag, value }` cases or an array; the new exported types `JsonSchemaObject`, `JsonSchemaVariant`,
|
|
19
|
+
`JsonSchemaCase`, `JsonSchemaArray` and `JsonSchemaNull` say so. The generated schemas do not change.
|
|
20
|
+
|
|
5
21
|
## 0.1.0 — 2026-10-28
|
|
6
22
|
|
|
7
23
|
First published version.
|
package/README.md
CHANGED
|
@@ -46,6 +46,7 @@ await layer.callTool("commit", { preview_id: "p1" });
|
|
|
46
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
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
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.
|
|
49
50
|
|
|
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.
|
|
51
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meantjs/tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
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
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"test": "node --test \"test/*.test.mjs\""
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@meantjs/format": "0.
|
|
47
|
+
"@meantjs/format": "0.3.0"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@candid-core/schema": "0.5.0",
|
package/src/index.d.mts
CHANGED
|
@@ -26,13 +26,50 @@ export interface JsonSchemaString {
|
|
|
26
26
|
pattern?: string;
|
|
27
27
|
}
|
|
28
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
|
+
|
|
29
66
|
export interface ToolDefinition {
|
|
30
67
|
name: string;
|
|
31
68
|
description: string;
|
|
32
69
|
inputSchema: {
|
|
33
70
|
$schema: string;
|
|
34
71
|
type: "object";
|
|
35
|
-
properties: Record<string,
|
|
72
|
+
properties: Record<string, JsonSchemaInput>;
|
|
36
73
|
required: string[];
|
|
37
74
|
additionalProperties: false;
|
|
38
75
|
};
|
|
@@ -59,6 +96,8 @@ export interface ToolPlan {
|
|
|
59
96
|
replicated: boolean;
|
|
60
97
|
title: string;
|
|
61
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;
|
|
62
101
|
/** The method's arguments, in order. */
|
|
63
102
|
argNodes: unknown[];
|
|
64
103
|
params: ToolParam[];
|
|
@@ -79,6 +118,8 @@ export interface InputIssue {
|
|
|
79
118
|
|
|
80
119
|
export const COMMIT_TOOL: "commit";
|
|
81
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;
|
|
82
123
|
export function resolvedMethods(resolved: ResolvedForm | ResolvedForm[]): [string, unknown][];
|
|
83
124
|
export function buildToolPlans(resolved: ResolvedForm | ResolvedForm[]): { plans: Map<string, ToolPlan>; skipped: { method: string; reason: string }[] };
|
|
84
125
|
export function toolDefinitions(resolved: ResolvedForm | ResolvedForm[], options?: { canisters?: Canister[] }): ToolDefinition[];
|
package/src/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @meantjs/tools: agent tools from any resolved form, browser-safe (#16).
|
|
2
2
|
// Arm C (packages/mcp-adapters) stays the eval arm with its frozen texts; this is the product tool layer.
|
|
3
3
|
|
|
4
|
-
export { COMMIT_TOOL, buildToolPlans, prepareToolName, resolvedMethods, toolDefinitions } from "./plan.mjs";
|
|
4
|
+
export { COMMIT_TOOL, buildToolPlans, caseToolName, prepareToolName, resolvedMethods, toolDefinitions } from "./plan.mjs";
|
|
5
5
|
export { displayValue, readToolInput, toCandid } from "./input.mjs";
|
|
6
6
|
export { classifyReply, createToolLayer, nameEntries } from "./layer.mjs";
|
package/src/input.mjs
CHANGED
|
@@ -468,7 +468,7 @@ function readInput(node, input, path, param, ctx) {
|
|
|
468
468
|
* input may use (spec 5.3, D-045), read when the call is prepared
|
|
469
469
|
* @returns {{ value: unknown, identity: [string, unknown][] } | { issues: { param: string, path: string, code: string, message: string }[] }}
|
|
470
470
|
* `value` is the method's single argument (undefined for a method without one; the argument tuple, an array, for a
|
|
471
|
-
* method with more than one), with the stamp left null; `identity` is what the inputs add to the intent's
|
|
471
|
+
* method with more than one; for a tool per case, the case `{ tag, value }` around its payload), with the stamp left null; `identity` is what the inputs add to the intent's
|
|
472
472
|
* identity. Issue codes are the format library's, plus E_INPUT (not an object), E_UNKNOWN_PARAM, E_MISSING and
|
|
473
473
|
* E_TYPE (an input that is not text, or JSON of the wrong shape for a record, variant or vec input), and for a
|
|
474
474
|
* `named` input E_NAMES_UNREAD (its set's names were not read), E_NAME_NOT_FOUND and E_NAME_AMBIGUOUS (several
|
|
@@ -510,10 +510,16 @@ export function readToolInput(plan, input, { refs = {}, now, names = {} } = {})
|
|
|
510
510
|
if (issues.length > 0) return { issues };
|
|
511
511
|
if (plan.argNodes?.length > 1) return { value: plan.fields.map((field) => (field.role === "param" ? values.get(field.name) : null)), identity };
|
|
512
512
|
if (plan.argNode === undefined) return { value: undefined, identity };
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
513
|
+
const value = plan.rootParam
|
|
514
|
+
? values.get(plan.params[0].name)
|
|
515
|
+
: plan.rootNull
|
|
516
|
+
? null
|
|
517
|
+
: Object.fromEntries(plan.fields.map((field) => [field.name, field.role === "param" ? values.get(field.name) : null]));
|
|
518
|
+
if (plan.case === undefined) return { value, identity };
|
|
519
|
+
// a tool per case (D-053): the payload goes into its case, a null payload as candid-core's bare { tag }; the case is
|
|
520
|
+
// part of the intent, so two cases with the same inputs are two intents
|
|
521
|
+
const bare = plan.caseNode.candid === "null" && plan.caseNode.optional !== true;
|
|
522
|
+
return { value: bare ? { tag: plan.case } : { tag: plan.case, value }, identity: [[plan.case, identity]] };
|
|
517
523
|
}
|
|
518
524
|
|
|
519
525
|
/**
|
package/src/plan.mjs
CHANGED
|
@@ -26,6 +26,12 @@
|
|
|
26
26
|
// rules (input.mjs, `inputWithoutRule`); a single argument that is a variant or a vec is the input `arg0`;
|
|
27
27
|
// - outcome keys may start with a record field (`result.Ok` over `record { result : opt variant { Ok; Err } }`);
|
|
28
28
|
// - an outcome with `default` alone classes every reply of a method whose result has no variant to select.
|
|
29
|
+
// And one tool per case (B2 rule 3 of proposal w9-b3wallet-gaps.md, D-053): a method whose single argument is a
|
|
30
|
+
// variant with an `intent` or `effect` on any of its cases gets a tool for each case, `<method>_<Case>` for a read and
|
|
31
|
+
// `prepare_<method>_<Case>` otherwise. Its inputs are the case's payload, read as a single argument is; its title is
|
|
32
|
+
// the case's intent, else its `describe`, else `<method> <Case>`; its effect is the case's, else the method's; and the
|
|
33
|
+
// method's `idempotency` and `outcome` apply. A method with an idempotency stamp has no case tools yet (the stamp would
|
|
34
|
+
// sit inside a case), and a tagged variant deeper in the argument stays one JSON input.
|
|
29
35
|
//
|
|
30
36
|
// The input words come from the format library (`Formatter.describe()`), so a form hint and a tool schema read
|
|
31
37
|
// one grammar. Arm C keeps its own frozen words (D-015, D-030, D-031).
|
|
@@ -34,6 +40,8 @@ import { MAX_VEC_ITEMS, inputReader, inputWithoutRule, nestedRole, readAs } from
|
|
|
34
40
|
|
|
35
41
|
export const COMMIT_TOOL = "commit";
|
|
36
42
|
export const prepareToolName = (method) => `prepare_${method}`;
|
|
43
|
+
/** The tool of one case of a method's variant argument (D-053): `<method>_<Case>` for a read, else `prepare_<method>_<Case>`. */
|
|
44
|
+
export const caseToolName = (method, label, read) => (read ? `${method}_${label}` : prepareToolName(`${method}_${label}`));
|
|
37
45
|
const byName = ([a], [b]) => (a < b ? -1 : a > b ? 1 : 0);
|
|
38
46
|
|
|
39
47
|
/** The Candid id of a label: `#n` is n, a name is its idl hash (profile spec 3.2). */
|
|
@@ -68,6 +76,8 @@ export function resolvedMethods(resolved) {
|
|
|
68
76
|
if (doc === null || typeof doc !== "object" || doc.format !== "candid-profile-resolved" || doc.format_version !== 0) {
|
|
69
77
|
throw new TypeError("resolved: not a candid-profile-resolved v0 document");
|
|
70
78
|
}
|
|
79
|
+
// An EVM contract's form (profile spec section 15) has ABI positions, nonces and receipts: its tools are phase 2.
|
|
80
|
+
if (doc.chain !== undefined) throw new TypeError(`resolved: a resolved form of chain ${JSON.stringify(doc.chain)} has no tools yet; this layer calls Internet Computer canisters`);
|
|
71
81
|
for (const [name, method] of Object.entries(doc.methods ?? {})) {
|
|
72
82
|
const seen = methods.get(name);
|
|
73
83
|
if (seen === undefined) methods.set(name, method);
|
|
@@ -128,25 +138,69 @@ function outcomeKeyWithoutRule(classes, result) {
|
|
|
128
138
|
return undefined;
|
|
129
139
|
}
|
|
130
140
|
|
|
131
|
-
|
|
132
|
-
|
|
141
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
142
|
+
const hasCaseTag = (node) => isObject(node?.tags) && (isObject(node.tags.intent) || isObject(node.tags.effect));
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* The cases of a method's single variant argument when any of them has an `intent` or an `effect` (D-053), else
|
|
146
|
+
* undefined: such a method gets one tool per case.
|
|
147
|
+
*/
|
|
148
|
+
function taggedCases(m) {
|
|
149
|
+
const root = Array.isArray(m.args) && m.args.length === 1 ? m.args[0] : undefined;
|
|
150
|
+
if (root?.candid !== "variant" || readAs(root)) return undefined;
|
|
151
|
+
const cases = Object.entries(root.cases ?? {});
|
|
152
|
+
return cases.some(([, node]) => hasCaseTag(node)) ? cases : undefined;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* A method's tools: one, or one per case of its variant argument (D-053).
|
|
157
|
+
* @returns {{ plans: object[], skipped: string[] }} the plans, and why each tool it would have is not made
|
|
158
|
+
*/
|
|
159
|
+
function planTools(name, m) {
|
|
160
|
+
const cases = taggedCases(m);
|
|
161
|
+
if (cases === undefined) {
|
|
162
|
+
const p = planMethod(name, m);
|
|
163
|
+
return p.skip ? { plans: [], skipped: [p.skip] } : { plans: [p], skipped: [] };
|
|
164
|
+
}
|
|
165
|
+
if (m.tags?.idempotency) return { plans: [], skipped: ["tools per case of a method with an idempotency stamp have no rule yet (the stamp would sit inside a case)"] };
|
|
166
|
+
const plans = [];
|
|
167
|
+
const skipped = [];
|
|
168
|
+
for (const kase of cases) {
|
|
169
|
+
const p = /^[A-Za-z_][A-Za-z0-9_]*$/.test(kase[0]) ? planMethod(name, m, kase) : { skip: "a case without a name has no tool name" };
|
|
170
|
+
if (p.skip) skipped.push(`case ${kase[0]}: ${p.skip}`);
|
|
171
|
+
else plans.push(p);
|
|
172
|
+
}
|
|
173
|
+
return { plans, skipped };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @param {[string, object]} [kase] for a tool per case (D-053): the case's label and node in the variant argument
|
|
178
|
+
* @returns {object | { skip: string }}
|
|
179
|
+
*/
|
|
180
|
+
function planMethod(name, m, kase) {
|
|
133
181
|
const tags = m.tags ?? {};
|
|
134
|
-
const
|
|
182
|
+
const own = kase === undefined ? tags : isObject(kase[1].tags) ? kase[1].tags : {}; // the case's tags, for a case tool
|
|
183
|
+
// the call's effect and intent: the case's, else the method's (spec 5.2 and 5.3)
|
|
184
|
+
const effect = own.effect?.args ?? tags.effect?.args;
|
|
185
|
+
const intent = own.intent?.args ?? tags.intent?.args;
|
|
135
186
|
// a read-only update (D-038) is a read for consumers, and must be sent as an update so that its answer is certified
|
|
136
187
|
const replicated = m.mode === "update" && effect?.kind === "read" && effect.irreversible === false && effect.replicated === true;
|
|
137
188
|
// a composite query is a read too (arm C has none to meet: the ICRC-1/2 packs declare only queries and updates)
|
|
138
189
|
const read = m.mode === "query" || m.mode === "composite_query" || replicated;
|
|
139
190
|
const args = Array.isArray(m.args) ? m.args : [];
|
|
140
191
|
const plan = {
|
|
141
|
-
name: read ? name : prepareToolName(name),
|
|
192
|
+
name: kase !== undefined ? caseToolName(name, kase[0], read) : read ? name : prepareToolName(name),
|
|
142
193
|
kind: read ? "read" : "prepare",
|
|
143
194
|
method: name,
|
|
195
|
+
/** for a tool per case (D-053): the case of the variant argument it sends, and that case's node */
|
|
196
|
+
case: kase?.[0],
|
|
197
|
+
caseNode: kase?.[1],
|
|
144
198
|
mode: m.mode,
|
|
145
199
|
/** a read sent as an update call */
|
|
146
200
|
replicated,
|
|
147
201
|
effect: effect ?? (read ? { kind: "read", irreversible: false } : { kind: "write", irreversible: true }),
|
|
148
|
-
describe:
|
|
149
|
-
intent
|
|
202
|
+
describe: own.describe?.args,
|
|
203
|
+
intent,
|
|
150
204
|
idempotency: tags.idempotency?.args,
|
|
151
205
|
outcome: tags.outcome?.args,
|
|
152
206
|
/** the method's single argument, or undefined (none, or more than one) */
|
|
@@ -164,12 +218,13 @@ function planMethod(name, m) {
|
|
|
164
218
|
rootNull: false,
|
|
165
219
|
results: m.results ?? [],
|
|
166
220
|
};
|
|
167
|
-
plan.title = plan.intent?.title ?? plan.describe?.label ?? name;
|
|
221
|
+
plan.title = kase !== undefined ? (own.intent?.args?.title ?? plan.describe?.label ?? `${name} ${kase[0]}`) : (plan.intent?.title ?? plan.describe?.label ?? name);
|
|
168
222
|
if (read && plan.results.length > 1) return { skip: "a query with more than one result has no rule (only the first would be shown)" };
|
|
169
223
|
// the stamp would be an input, or left unset: the stamp rules are for writes
|
|
170
224
|
if (replicated && plan.idempotency) return { skip: "a read-only update with an idempotency stamp has no rule" };
|
|
171
225
|
if (!read) {
|
|
172
|
-
|
|
226
|
+
if (!intent) return { skip: "an update method needs the intent tag" };
|
|
227
|
+
if (!tags.outcome) return { skip: "an update method needs the outcome tag" };
|
|
173
228
|
if (plan.idempotency && plan.idempotency.mode !== "replay_same_payload") return { skip: `idempotency mode ${plan.idempotency.mode} has no rule` };
|
|
174
229
|
if (!plan.idempotency && [...Object.values(plan.outcome.classes ?? {}), plan.outcome.default].includes("retry_new")) {
|
|
175
230
|
return { skip: "the outcome class retry_new renews an idempotency stamp, and this method has none" };
|
|
@@ -192,49 +247,64 @@ function planMethod(name, m) {
|
|
|
192
247
|
if (c.role === "param") plan.params.push({ name: `arg${i}`, node, optional, path: `$args[${i}]` });
|
|
193
248
|
}
|
|
194
249
|
} else if (args.length === 1) {
|
|
195
|
-
|
|
196
|
-
const
|
|
197
|
-
if (
|
|
198
|
-
const why = inputWithoutRule(root, "(0)"); // a text pattern the layer does not run
|
|
199
|
-
if (why) return { skip: why };
|
|
200
|
-
// the stamp is filled by the tool layer, so it must be a field beside the inputs, never the whole argument
|
|
201
|
-
if (plan.idempotency) return { skip: `the stamp ${plan.idempotency.stamp} must name one field of the argument record` };
|
|
202
|
-
plan.rootParam = true;
|
|
203
|
-
plan.params.push({ name: tag, node: root, optional: root.optional === true, path: "$args[0]" });
|
|
204
|
-
} else if (root.candid === "record") {
|
|
205
|
-
let stampId;
|
|
206
|
-
if (plan.idempotency) {
|
|
207
|
-
const ids = relpathIds(plan.idempotency.stamp);
|
|
208
|
-
if (ids === null || ids.length !== 1) return { skip: `the stamp ${plan.idempotency.stamp} must name one field of the argument record` };
|
|
209
|
-
stampId = ids[0];
|
|
210
|
-
}
|
|
211
|
-
for (const [fname, fnode] of Object.entries(root.fields ?? {})) {
|
|
212
|
-
// the canister selector of every tool (toolDefinitions, the tool layer) owns this input name
|
|
213
|
-
if (fname === "canister_id") return { skip: "an argument field named canister_id would collide with the canister selector input" };
|
|
214
|
-
const c = classifyField(fname, fnode, stampId);
|
|
215
|
-
if (c.skip) return c;
|
|
216
|
-
if (c.role === "stamp") plan.stampField = fname;
|
|
217
|
-
const optional = fnode.optional === true;
|
|
218
|
-
plan.fields.push({ name: fname, node: fnode, role: c.role, optional });
|
|
219
|
-
if (c.role === "param") plan.params.push({ name: fname, node: fnode, optional, path: `$args[0].${fname}` });
|
|
220
|
-
}
|
|
221
|
-
if (plan.idempotency && !plan.fields.some((f) => f.role === "stamp")) return { skip: `the stamp field ${plan.idempotency.stamp} is not an argument field` };
|
|
222
|
-
} else if (root.candid === "null" || root.candid === "reserved") {
|
|
223
|
-
// one value only, so nothing to ask for: the tool takes no input and sends null
|
|
224
|
-
if (plan.idempotency) return { skip: `the stamp ${plan.idempotency.stamp} must name one field of the argument record` };
|
|
225
|
-
plan.rootNull = true;
|
|
226
|
-
} else {
|
|
227
|
-
// a variant or a vec (or a record a value tag does not cover) is one JSON input
|
|
228
|
-
const why = inputWithoutRule(root, "(0)");
|
|
229
|
-
if (why) return { skip: why };
|
|
230
|
-
if (plan.idempotency) return { skip: `the stamp ${plan.idempotency.stamp} must name one field of the argument record` };
|
|
231
|
-
plan.rootParam = true;
|
|
232
|
-
plan.params.push({ name: "arg0", node: root, optional: root.optional === true, path: "$args[0]" });
|
|
233
|
-
}
|
|
250
|
+
// a case tool reads its case's payload as a single argument is read (D-053)
|
|
251
|
+
const why = kase !== undefined ? planArgument(plan, kase[1], `(0).${kase[0]}`, `$args[0].${kase[0]}`) : planArgument(plan, args[0], "(0)", "$args[0]");
|
|
252
|
+
if (why) return why;
|
|
234
253
|
}
|
|
235
254
|
return plan;
|
|
236
255
|
}
|
|
237
256
|
|
|
257
|
+
/**
|
|
258
|
+
* The inputs of a single argument, or of a case's payload: a position `readAs` names is one input named by its tag, a
|
|
259
|
+
* record's fields are inputs of their own (the stamp and filled ones aside), null takes none, and any other position
|
|
260
|
+
* is one JSON input. `at` names the position in skip reasons, `path` in input issues.
|
|
261
|
+
* @returns {{ skip: string } | undefined}
|
|
262
|
+
*/
|
|
263
|
+
function planArgument(plan, root, at, path) {
|
|
264
|
+
const tag = readAs(root);
|
|
265
|
+
if (tag) {
|
|
266
|
+
const why = inputWithoutRule(root, at); // a text pattern the layer does not run
|
|
267
|
+
if (why) return { skip: why };
|
|
268
|
+
// the stamp is filled by the tool layer, so it must be a field beside the inputs, never the whole argument
|
|
269
|
+
if (plan.idempotency) return { skip: `the stamp ${plan.idempotency.stamp} must name one field of the argument record` };
|
|
270
|
+
plan.rootParam = true;
|
|
271
|
+
plan.params.push({ name: tag, node: root, optional: root.optional === true, path });
|
|
272
|
+
} else if (root.candid === "record") {
|
|
273
|
+
let stampId;
|
|
274
|
+
if (plan.idempotency) {
|
|
275
|
+
const ids = relpathIds(plan.idempotency.stamp);
|
|
276
|
+
if (ids === null || ids.length !== 1) return { skip: `the stamp ${plan.idempotency.stamp} must name one field of the argument record` };
|
|
277
|
+
stampId = ids[0];
|
|
278
|
+
}
|
|
279
|
+
for (const [fname, fnode] of Object.entries(root.fields ?? {})) {
|
|
280
|
+
// the canister selector of every tool (toolDefinitions, the tool layer) owns this input name
|
|
281
|
+
if (fname === "canister_id") return { skip: "an argument field named canister_id would collide with the canister selector input" };
|
|
282
|
+
const c = classifyField(fname, fnode, stampId);
|
|
283
|
+
if (c.skip) return c;
|
|
284
|
+
if (c.role === "stamp") plan.stampField = fname;
|
|
285
|
+
const optional = fnode.optional === true;
|
|
286
|
+
plan.fields.push({ name: fname, node: fnode, role: c.role, optional });
|
|
287
|
+
if (c.role === "param") plan.params.push({ name: fname, node: fnode, optional, path: `${path}.${fname}` });
|
|
288
|
+
}
|
|
289
|
+
if (plan.idempotency && !plan.fields.some((f) => f.role === "stamp")) return { skip: `the stamp field ${plan.idempotency.stamp} is not an argument field` };
|
|
290
|
+
} else if (root.candid === "null" || root.candid === "reserved") {
|
|
291
|
+
// one value only, so nothing to ask for: the tool takes no input and sends null
|
|
292
|
+
if (plan.idempotency) return { skip: `the stamp ${plan.idempotency.stamp} must name one field of the argument record` };
|
|
293
|
+
plan.rootNull = true;
|
|
294
|
+
} else {
|
|
295
|
+
// a variant or a vec (or a record a value tag does not cover) is one JSON input
|
|
296
|
+
const why = inputWithoutRule(root, at);
|
|
297
|
+
if (why) return { skip: why };
|
|
298
|
+
if (plan.idempotency) return { skip: `the stamp ${plan.idempotency.stamp} must name one field of the argument record` };
|
|
299
|
+
plan.rootParam = true;
|
|
300
|
+
plan.params.push({ name: "arg0", node: root, optional: root.optional === true, path });
|
|
301
|
+
}
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** What a tool calls, for messages: the method, or the method and the case it sends. */
|
|
306
|
+
const toolOf = (p) => (p.case === undefined ? p.method : `${p.method}'s case ${p.case}`);
|
|
307
|
+
|
|
238
308
|
/**
|
|
239
309
|
* @param {object | object[]} resolved
|
|
240
310
|
* @returns {{ plans: Map<string, any>, skipped: { method: string, reason: string }[] }} plans keyed by tool name, sorted
|
|
@@ -244,15 +314,15 @@ export function buildToolPlans(resolved) {
|
|
|
244
314
|
const skipped = [];
|
|
245
315
|
const byTool = new Map();
|
|
246
316
|
for (const [name, m] of resolvedMethods(resolved)) {
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
317
|
+
const tools = planTools(name, m);
|
|
318
|
+
for (const reason of tools.skipped) skipped.push({ method: name, reason });
|
|
319
|
+
for (const p of tools.plans) byTool.set(p.name, [...(byTool.get(p.name) ?? []), p]);
|
|
250
320
|
}
|
|
251
321
|
// Two methods whose tools share a name (a query named prepare_x beside an update x), or one named like the commit
|
|
252
322
|
// tool: neither is dropped silently, and neither is preferred, so all of them are skipped.
|
|
253
323
|
for (const [tool, ps] of byTool) {
|
|
254
324
|
if (tool === COMMIT_TOOL) for (const p of ps) skipped.push({ method: p.method, reason: `its tool name ${tool} is the commit tool's` });
|
|
255
|
-
else if (ps.length > 1) for (const p of ps) skipped.push({ method: p.method, reason: `its tool name ${tool} is also ${ps.filter((q) => q !== p).map(
|
|
325
|
+
else if (ps.length > 1) for (const p of ps) skipped.push({ method: p.method, reason: `its tool name ${tool} is also ${ps.filter((q) => q !== p).map(toolOf).join(", ")}'s` });
|
|
256
326
|
else plans.set(tool, ps[0]);
|
|
257
327
|
}
|
|
258
328
|
return { plans: new Map([...plans.entries()].sort(byName)), skipped };
|
|
@@ -397,12 +467,13 @@ function definitionOf(plan, canisters, refsList, sets) {
|
|
|
397
467
|
const schema = inputSchema(plan, canisters, refsList, sets);
|
|
398
468
|
if (plan.kind === "read") {
|
|
399
469
|
const label = plan.describe?.label;
|
|
400
|
-
const
|
|
470
|
+
const call = `\`${plan.method}\`${plan.case === undefined ? "" : ` with the case \`${plan.case}\``}`;
|
|
471
|
+
const what = plan.replicated ? `Read-only call of ${call}, sent as an update call so that the answer is certified; it changes nothing.` : `Read-only query of ${call}.`;
|
|
401
472
|
return {
|
|
402
473
|
name: plan.name,
|
|
403
474
|
description: `${label ? `${label}. ` : ""}${what} The answer gives each value formatted for people and raw (base units for amounts, the tagged unit for times).`,
|
|
404
475
|
inputSchema: schema,
|
|
405
|
-
annotations: { destructiveHint: false, openWorldHint: true, readOnlyHint: true, title: label ?? plan.method },
|
|
476
|
+
annotations: { destructiveHint: false, openWorldHint: true, readOnlyHint: true, title: label ?? (plan.case === undefined ? plan.method : plan.title) },
|
|
406
477
|
};
|
|
407
478
|
}
|
|
408
479
|
const e = plan.effect;
|
|
@@ -410,7 +481,7 @@ function definitionOf(plan, canisters, refsList, sets) {
|
|
|
410
481
|
return {
|
|
411
482
|
name: plan.name,
|
|
412
483
|
description:
|
|
413
|
-
`Prepare the call "${plan.title}" (\`${plan.method}
|
|
484
|
+
`Prepare the call "${plan.title}" (\`${plan.method}\`${plan.case === undefined ? "" : ` with the case \`${plan.case}\``}, effect: ${e.kind}${e.irreversible ? ", irreversible" : ""}). Nothing is sent yet. ` +
|
|
414
485
|
"Inputs are in human units and are checked exactly (nothing is rounded). " +
|
|
415
486
|
(plan.idempotency ? "" : "This call has no idempotency stamp, so the canister cannot tell a repeat from a new call: once an attempt's outcome is unknown, it is never sent again (commit answers reconcile). ") +
|
|
416
487
|
(filled ? `The tool layer fills in: ${filled}. ` : "") +
|