@orkestrel/tool 0.0.3 → 0.0.4

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/README.md CHANGED
@@ -33,7 +33,7 @@ npm install @orkestrel/tool
33
33
 
34
34
  ## Requirements
35
35
 
36
- - Node.js >= 24
36
+ - Node.js >= 22
37
37
  - Dual ESM + CommonJS builds (`import` and `require` both supported)
38
38
 
39
39
  ## Guide
@@ -425,6 +425,108 @@ var RELATION_TOOL_DESCRIPTION = [
425
425
  var RELATION_TOOL_LIMIT = 1e3;
426
426
  /** The default cap on how many `include` path segments deep a `load` / `find` call may traverse — the relation tool's default include-depth ceiling. */
427
427
  var RELATION_TOOL_DEPTH = 3;
428
+ /**
429
+ * The name {@link import('./factories.js').createInferTool} advertises by default — the key a
430
+ * model calls and the `ToolManagerInterface` (`@orkestrel/agent`) registers under.
431
+ */
432
+ var INFER_TOOL_NAME = "infer";
433
+ /**
434
+ * The lean {@link import('@orkestrel/agent').ToolInterface.summary} {@link import('./factories.js').createInferTool}
435
+ * advertises in place of {@link INFER_TOOL_DESCRIPTION} — a `ToolManagerInterface.definitions()`
436
+ * (`@orkestrel/agent`) advertises `summary ?? description`, so this one-sentence text stands in
437
+ * for the full teaching description; the full text stays retrievable via
438
+ * {@link import('./factories.js').createDescribeTool}.
439
+ */
440
+ var INFER_TOOL_SUMMARY = "Infer a JSON Schema (as advertised tool parameters) from one or more example values. Call describe('infer') for the required fields.";
441
+ var INFER_TOOL_DESCRIPTION = [
442
+ "Infer a JSON Schema from example values, returned in the same shape a tool advertises its parameters.",
443
+ "",
444
+ "Required:",
445
+ " samples - an array of at least one example value to infer the schema from.",
446
+ "Optional:",
447
+ " format - infer string formats (date-time, email, ...) from the samples. Defaults to false.",
448
+ " enum - infer enum constraints from repeated literal values across the samples. Defaults to false.",
449
+ " candidates - values to check against the freshly inferred schema. When present, the result",
450
+ " is wrapped as { parameters, checks } instead of the bare parameters record, one",
451
+ " check per candidate (same index). Every check has the uniform shape",
452
+ " { index, valid, coercible, faults? }. `valid` is a STRICT verdict (no coercion)",
453
+ " — e.g. the number 7 is NOT valid against a string slot. `coercible` answers a",
454
+ " separate question: would the SAME value be accepted by an endpoint tool call,",
455
+ " whose enforcement NORMALIZES args (7 coerces to '7')? So 7 against a string slot",
456
+ " yields { valid: false, coercible: true, faults: [] } — a strict mismatch that",
457
+ " normalization would silently accept, so faults is EMPTY. `faults` only ever",
458
+ " populates for a non-coercible mismatch (a wrong type normalization cannot fix,",
459
+ " a missing required key, an out-of-enum value); checks never throw, regardless of",
460
+ " candidate shape.",
461
+ "Example (no candidates):",
462
+ ` in: ${JSON.stringify({ samples: [{
463
+ id: 1,
464
+ name: "Ada"
465
+ }, {
466
+ id: 2,
467
+ name: "Bob"
468
+ }] })}`,
469
+ ` out: ${JSON.stringify({
470
+ type: "object",
471
+ properties: {
472
+ id: { type: "integer" },
473
+ name: { type: "string" }
474
+ },
475
+ required: ["id", "name"],
476
+ additionalProperties: false
477
+ })}`,
478
+ "Example (with candidates):",
479
+ ` in: ${JSON.stringify({
480
+ samples: [{
481
+ id: 1,
482
+ name: "Ada"
483
+ }],
484
+ candidates: [
485
+ {
486
+ id: 3,
487
+ name: "Cy"
488
+ },
489
+ {
490
+ id: "x",
491
+ name: "Cy"
492
+ },
493
+ {
494
+ id: 1,
495
+ name: 7
496
+ }
497
+ ]
498
+ })}`,
499
+ ` out: ${JSON.stringify({
500
+ parameters: {
501
+ type: "object",
502
+ properties: {
503
+ id: { type: "integer" },
504
+ name: { type: "string" }
505
+ },
506
+ required: ["id", "name"],
507
+ additionalProperties: false
508
+ },
509
+ checks: [
510
+ {
511
+ index: 0,
512
+ valid: true,
513
+ coercible: true
514
+ },
515
+ {
516
+ index: 1,
517
+ valid: false,
518
+ coercible: false,
519
+ faults: "<structured faults>"
520
+ },
521
+ {
522
+ index: 2,
523
+ valid: false,
524
+ coercible: true,
525
+ faults: []
526
+ }
527
+ ]
528
+ })}`
529
+ ].join("\n");
428
530
  //#endregion
429
531
  //#region src/core/errors.ts
430
532
  /**
@@ -1106,6 +1208,28 @@ var relationToolShape = (0, _orkestrel_contract.unionShape)((0, _orkestrel_contr
1106
1208
  description: "The \"through\" relation name."
1107
1209
  })
1108
1210
  }));
1211
+ /**
1212
+ * The shape of {@link import('./factories.js').createInferTool}'s call arguments — one or more
1213
+ * example `samples` to infer a JSON Schema from, plus per-call `format` / `enum` toggles and an
1214
+ * optional `candidates` array to check against the inferred schema.
1215
+ *
1216
+ * @remarks
1217
+ * `samples` requires at least one element (`min: 1`) — an empty array parses to `undefined`,
1218
+ * surfaced by the handler as a typed `TOOL` {@link import('./errors.js').AgentToolError}. When
1219
+ * `candidates` is present (any array, including empty), the handler compiles a contract from the
1220
+ * freshly inferred schema and checks each candidate against it with a STRICT guard (`.is`, no
1221
+ * coercion) — the opposite of {@link import('./factories.js').createEndpointTool}'s NORMALIZING
1222
+ * `.parse` enforcement.
1223
+ */
1224
+ var inferToolShape = (0, _orkestrel_contract.objectShape)({
1225
+ samples: (0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.jsonShape)(), {
1226
+ min: 1,
1227
+ description: "The example values to infer a JSON Schema from (at least one)."
1228
+ }),
1229
+ format: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Infer string formats (date-time, email, ...) from the samples. Defaults to false." })),
1230
+ enum: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.booleanShape)({ description: "Infer enum constraints from repeated literal values. Defaults to false." })),
1231
+ candidates: (0, _orkestrel_contract.optionalShape)((0, _orkestrel_contract.arrayShape)((0, _orkestrel_contract.jsonShape)(), { description: "Optional values to check against the freshly inferred schema. When present, the tool returns a per-candidate verdict (strict — no coercion) alongside the inferred parameters." }))
1232
+ });
1109
1233
  //#endregion
1110
1234
  //#region src/core/helpers.ts
1111
1235
  /**
@@ -2820,6 +2944,216 @@ function createRelationTool(options) {
2820
2944
  }
2821
2945
  });
2822
2946
  }
2947
+ /**
2948
+ * Build a standalone LLM-callable tool that infers a JSON Schema from example values — the
2949
+ * utility half of the "existing API/DB → MCP tool" bridge (the other half,
2950
+ * {@link createEndpointTool}, wraps one CONCRETE endpoint).
2951
+ *
2952
+ * @remarks
2953
+ * The universal tool-handler contract (AGENTS §14): validates the call args against
2954
+ * {@link import('./shapers.js').inferToolShape} (`samples` non-empty, `format` / `enum` optional
2955
+ * booleans, `candidates` an optional array), infers a schema via `@orkestrel/contract`'s
2956
+ * `samplesToSchema`, wraps a non-object root as `{ value: <schema> }` via `schemaToObject` (mirrors
2957
+ * the tool-parameters convention every other `create*Tool` factory advertises), and RETURNS the
2958
+ * resulting parameters record. An empty `samples` array fails `inferToolShape`'s `min: 1` bound —
2959
+ * `contract.parse` returns `undefined` and the handler throws a typed `TOOL`
2960
+ * {@link import('./errors.js').AgentToolError}.
2961
+ *
2962
+ * When `candidates` is ABSENT, the return is the bare parameters record — unchanged from before
2963
+ * this array existed. When `candidates` is PRESENT (any array, including empty), the handler
2964
+ * compiles a SEPARATE per-call contract from the RAW inferred schema (via `@orkestrel/contract`'s
2965
+ * `schemaToShape`, NOT the `schemaToObject`-wrapped parameters — a bare-value sample checks a
2966
+ * bare-value candidate) and returns `{ parameters, checks }`, one check per candidate at the same
2967
+ * index. Every entry has a UNIFORM shape — `{ index, valid, coercible }`, with `faults` added ONLY
2968
+ * when `valid` is `false`: `valid` is the STRICT guard verdict (`checker.is(candidate)`), the
2969
+ * OPPOSITE of {@link createEndpointTool}'s enforcement, which coerces (`7` becomes `'7'` for a
2970
+ * string slot) — here a conformance report answers "does this value conform AS-IS": `7` against a
2971
+ * string slot is `valid: false`, full stop. `coercible` answers a SEPARATE question — "would the
2972
+ * NORMALIZING parse accept this value", i.e. would {@link createEndpointTool}'s default enforcement
2973
+ * admit it (`checker.parse(candidate) !== undefined`) — computed for every candidate regardless of
2974
+ * `valid`; by the house parse/guard round-trip guarantee (AGENTS §14), a `valid: true` entry is
2975
+ * ALWAYS also `coercible: true`. `@orkestrel/contract` 0.0.7's `explain` mirrors the normalizing
2976
+ * `parse`'s leniency, not `is`'s strictness — so a strictly-invalid but coercible candidate (`7`
2977
+ * against a string slot) yields `{ valid: false, coercible: true, faults: [] }`: EMPTY faults, since
2978
+ * the mismatch the normalizing parse would silently fix is not one `explain` reports. `faults`
2979
+ * therefore only ever populates for a NON-coercible mismatch — a wrong type the parse can't coerce
2980
+ * (a boolean in a string slot), a missing required key, or an out-of-enum value — where
2981
+ * `coercible: false`. `checker.is` / `.parse` / `.explain` are all total over JSON-safe input — a
2982
+ * JSON-safe hostile candidate (a `__proto__`-carrying object, deeply nested data) reaches all three
2983
+ * and yields a bounded, non-throwing per-candidate verdict; a NON-JSON-safe candidate (e.g. a
2984
+ * throwing-getter `Proxy`) never reaches the checker at all — it fails the OUTER `args` parse
2985
+ * against {@link import('./shapers.js').inferToolShape} and rejects the WHOLE call with the same
2986
+ * `TOOL` {@link import('./errors.js').AgentToolError} a malformed `samples`/`format`/`enum` throws,
2987
+ * with no per-candidate verdict produced.
2988
+ *
2989
+ * @param options - Advertised `name` / `description` overrides (see
2990
+ * {@link import('./types.js').InferToolOptions})
2991
+ * @returns A `ToolInterface` (named {@link import('./constants.js').INFER_TOOL_NAME} by default)
2992
+ *
2993
+ * @example
2994
+ * ```ts
2995
+ * import { createInferTool } from '@src/core'
2996
+ * import { createToolManager } from '@orkestrel/agent'
2997
+ *
2998
+ * const tool = createInferTool()
2999
+ * const tools = createToolManager()
3000
+ * tools.add(tool)
3001
+ *
3002
+ * const result = await tools.execute({
3003
+ * id: 'call-1',
3004
+ * name: 'infer',
3005
+ * arguments: { samples: [{ id: 1, name: 'Ada' }, { id: 2, name: 'Bob' }] },
3006
+ * })
3007
+ * // result.value -> { type: 'object', properties: { id: {...}, name: {...} }, ... }
3008
+ *
3009
+ * // with candidates, the result is wrapped with per-candidate verdicts
3010
+ * const checked = await tools.execute({
3011
+ * id: 'call-2',
3012
+ * name: 'infer',
3013
+ * arguments: {
3014
+ * samples: [{ id: 1, name: 'Ada' }],
3015
+ * candidates: [{ id: 2, name: 'Bob' }, { id: 'x', name: 'Cy' }],
3016
+ * },
3017
+ * })
3018
+ * // checked.value -> { parameters: {...}, checks: [
3019
+ * // { index: 0, valid: true, coercible: true },
3020
+ * // { index: 1, valid: false, coercible: false, faults: [...] },
3021
+ * // ] }
3022
+ * ```
3023
+ */
3024
+ function createInferTool(options) {
3025
+ const contract = (0, _orkestrel_contract.createContract)(inferToolShape);
3026
+ const parameters = (0, _orkestrel_contract.schemaToParameters)(contract.schema);
3027
+ return (0, _orkestrel_agent.createTool)({
3028
+ name: options?.name ?? "infer",
3029
+ description: options?.description ?? INFER_TOOL_DESCRIPTION,
3030
+ summary: INFER_TOOL_SUMMARY,
3031
+ parameters,
3032
+ execute: async (args) => {
3033
+ const parsed = contract.parse(args);
3034
+ if (parsed === void 0) throw new AgentToolError("TOOL", "malformed infer arguments", { args });
3035
+ const schema = (0, _orkestrel_contract.samplesToSchema)(parsed.samples, {
3036
+ format: parsed.format ?? false,
3037
+ enum: parsed.enum ?? false
3038
+ });
3039
+ const result = (0, _orkestrel_contract.schemaToParameters)((0, _orkestrel_contract.schemaToObject)(schema));
3040
+ if (result === void 0) throw new AgentToolError("TOOL", "could not infer a schema", { args });
3041
+ if (parsed.candidates === void 0) return result;
3042
+ const checker = (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.schemaToShape)(schema));
3043
+ return {
3044
+ parameters: result,
3045
+ checks: parsed.candidates.map((candidate, index) => {
3046
+ const valid = checker.is(candidate);
3047
+ const coercible = checker.parse(candidate) !== void 0;
3048
+ return valid ? {
3049
+ index,
3050
+ valid,
3051
+ coercible
3052
+ } : {
3053
+ index,
3054
+ valid,
3055
+ coercible,
3056
+ faults: checker.explain(candidate)
3057
+ };
3058
+ })
3059
+ };
3060
+ }
3061
+ });
3062
+ }
3063
+ /**
3064
+ * Wrap one CONCRETE endpoint ({@link import('./types.js').EndpointDefinition}) as an LLM-callable
3065
+ * `ToolInterface` — the endpoint half of the "existing API/DB → MCP tool" bridge (the other half,
3066
+ * {@link createInferTool}, is a standalone inference utility).
3067
+ *
3068
+ * @remarks
3069
+ * `parameters` is inferred ONCE at construction from `definition.samples` via
3070
+ * `@orkestrel/contract`'s `samplesToSchema` (tuned by {@link import('./types.js').EndpointToolOptions}'s
3071
+ * `format` / `enum`), wrapping a non-object root as `{ value: <schema> }` via `schemaToObject` —
3072
+ * the SAME object-rooted schema is both the ADVERTISED `parameters` and, by default
3073
+ * ({@link import('./types.js').EndpointToolOptions.validate} `true`), the ENFORCED contract:
3074
+ * `@orkestrel/contract` 0.0.7's `schemaToShape` compiles it ONCE (via `createContract`) into a
3075
+ * `ContractInterface` whose `.parse` runs on every call's `args` before `definition.invoke` — a
3076
+ * NORMALIZING parse, not a strict type check: a scalar is COERCED to its inferred type where the
3077
+ * house parsers coerce (a number to/from a numeric string, a boolean from `'1'`/`'0'`/`'true'`/
3078
+ * `'false'`/`1`/`0`), so `definition.invoke` receives the COERCED value (e.g. `7` sent for a
3079
+ * string slot arrives as `'7'`), not the raw call value. A call whose `args` fails to parse into
3080
+ * a record — a required key missing, or a value not coercible to its slot's type — THROWS a
3081
+ * typed `TOOL` {@link import('./errors.js').AgentToolError} carrying the compiled contract's
3082
+ * structured `explain` faults, and `definition.invoke` is never called. `format` annotations are
3083
+ * NEVER asserted, and a key outside the closed inferred schema is SILENTLY DROPPED rather than
3084
+ * rejected (see {@link import('./types.js').EndpointToolOptions.validate}). With
3085
+ * `validate: false`, `execute` PASSES THROUGH the model-supplied `args` to `definition.invoke`
3086
+ * WITHOUT re-validation — the pre-0.0.7 behavior, preserved as an explicit opt-out. Either way,
3087
+ * `invoke`'s return flows back as the tool call's plain result; a throw PROPAGATES uncaught,
3088
+ * isolated by the `ToolManagerInterface` (`@orkestrel/agent`) into the canonical error envelope
3089
+ * (AGENTS §14) — never caught or re-wrapped here.
3090
+ *
3091
+ * @param definition - The endpoint's identity, non-empty samples, and local handler (see
3092
+ * {@link import('./types.js').EndpointDefinition})
3093
+ * @param options - Construction-time inference tuning + the validate opt-out (see
3094
+ * {@link import('./types.js').EndpointToolOptions})
3095
+ * @returns A `ToolInterface` named `definition.name`
3096
+ *
3097
+ * @example
3098
+ * ```ts
3099
+ * import { createEndpointTool } from '@src/core'
3100
+ * import { createToolManager } from '@orkestrel/agent'
3101
+ *
3102
+ * const tool = createEndpointTool({
3103
+ * name: 'lookupUser',
3104
+ * description: 'Look up a user by id.',
3105
+ * samples: [{ id: '1', name: 'Ada' }, { id: '2', name: 'Bob' }],
3106
+ * invoke: (args) => ({ id: args.id, name: 'Ada' }),
3107
+ * })
3108
+ * const tools = createToolManager()
3109
+ * tools.add(tool)
3110
+ *
3111
+ * // conforming args (all required keys present) parse and reach `invoke`
3112
+ * const result = await tools.execute({
3113
+ * id: 'call-1',
3114
+ * name: 'lookupUser',
3115
+ * arguments: { id: '1', name: 'Ada' },
3116
+ * })
3117
+ * // result.value -> { id: '1', name: 'Ada' }
3118
+ *
3119
+ * // a nonconforming call (id is not coercible to the required string) is rejected before
3120
+ * // `invoke` runs
3121
+ * const rejected = await tools.execute({
3122
+ * id: 'call-2',
3123
+ * name: 'lookupUser',
3124
+ * arguments: { id: true, name: 'Ada' },
3125
+ * })
3126
+ * // rejected.error -> the TOOL AgentToolError message
3127
+ * ```
3128
+ */
3129
+ function createEndpointTool(definition, options) {
3130
+ if (definition.samples.length === 0) throw new AgentToolError("TOOL", "endpoint requires at least one sample", { name: definition.name });
3131
+ const objectSchema = (0, _orkestrel_contract.schemaToObject)((0, _orkestrel_contract.samplesToSchema)(definition.samples, {
3132
+ format: options?.format ?? false,
3133
+ enum: options?.enum ?? false
3134
+ }));
3135
+ const parameters = (0, _orkestrel_contract.schemaToParameters)(objectSchema);
3136
+ if (!(options?.validate ?? true)) return (0, _orkestrel_agent.createTool)({
3137
+ name: definition.name,
3138
+ description: definition.description,
3139
+ parameters,
3140
+ execute: (args) => definition.invoke(args)
3141
+ });
3142
+ const contract = (0, _orkestrel_contract.createContract)((0, _orkestrel_contract.schemaToShape)(objectSchema));
3143
+ return (0, _orkestrel_agent.createTool)({
3144
+ name: definition.name,
3145
+ description: definition.description,
3146
+ parameters,
3147
+ execute: (args) => {
3148
+ const parsed = contract.parse(args);
3149
+ if (parsed === void 0 || !(0, _orkestrel_contract.isRecord)(parsed)) throw new AgentToolError("TOOL", "malformed endpoint call arguments", {
3150
+ name: definition.name,
3151
+ faults: contract.explain(args)
3152
+ });
3153
+ return definition.invoke(parsed);
3154
+ }
3155
+ });
3156
+ }
2823
3157
  //#endregion
2824
3158
  exports.AGENT_TOOL_DEPTH = AGENT_TOOL_DEPTH;
2825
3159
  exports.AGENT_TOOL_DESCRIPTION = AGENT_TOOL_DESCRIPTION;
@@ -2838,6 +3172,9 @@ exports.DESCRIBE_TOOL_DESCRIPTION = DESCRIBE_TOOL_DESCRIPTION;
2838
3172
  exports.DESCRIBE_TOOL_NAME = DESCRIBE_TOOL_NAME;
2839
3173
  exports.DESCRIBE_TOOL_SUMMARY = DESCRIBE_TOOL_SUMMARY;
2840
3174
  exports.DatabaseDefinitionStore = DatabaseDefinitionStore;
3175
+ exports.INFER_TOOL_DESCRIPTION = INFER_TOOL_DESCRIPTION;
3176
+ exports.INFER_TOOL_NAME = INFER_TOOL_NAME;
3177
+ exports.INFER_TOOL_SUMMARY = INFER_TOOL_SUMMARY;
2841
3178
  exports.MAX_WORKFLOW_DEPTH = MAX_WORKFLOW_DEPTH;
2842
3179
  exports.MemoryDefinitionStore = MemoryDefinitionStore;
2843
3180
  exports.PROMPT_TOOL_DESCRIPTION = PROMPT_TOOL_DESCRIPTION;
@@ -2876,6 +3213,8 @@ exports.createAnswerTool = createAnswerTool;
2876
3213
  exports.createDatabaseDefinitionStore = createDatabaseDefinitionStore;
2877
3214
  exports.createDatabaseTool = createDatabaseTool;
2878
3215
  exports.createDescribeTool = createDescribeTool;
3216
+ exports.createEndpointTool = createEndpointTool;
3217
+ exports.createInferTool = createInferTool;
2879
3218
  exports.createMemoryDefinitionStore = createMemoryDefinitionStore;
2880
3219
  exports.createPromptTool = createPromptTool;
2881
3220
  exports.createRelationTool = createRelationTool;
@@ -2892,6 +3231,7 @@ exports.expandInclude = expandInclude;
2892
3231
  exports.expandSteps = expandSteps;
2893
3232
  exports.expandTables = expandTables;
2894
3233
  exports.includeShape = includeShape;
3234
+ exports.inferToolShape = inferToolShape;
2895
3235
  exports.isAgentToolError = isAgentToolError;
2896
3236
  exports.isColumnKind = isColumnKind;
2897
3237
  exports.isColumnSpec = isColumnSpec;