@molecule/api-ai-tools 1.0.8 → 1.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/README.md +241 -1
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -0
- package/dist/input-normalizer.d.ts +92 -0
- package/dist/input-normalizer.d.ts.map +1 -0
- package/dist/input-normalizer.js +191 -0
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +69 -4
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +450 -88
- package/dist/utilities.d.ts +79 -0
- package/dist/utilities.d.ts.map +1 -1
- package/dist/utilities.js +130 -2
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@ AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
|
3
3
|
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
4
|
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
5
|
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
-
Generated: 2026-09-
|
|
6
|
+
Generated: 2026-09-22T18:02:23.461Z
|
|
7
7
|
-->
|
|
8
8
|
|
|
9
9
|
# @molecule/api-ai-tools
|
|
@@ -133,6 +133,50 @@ interface FileDiffEvent {
|
|
|
133
133
|
}
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
+
#### `GuardOptions`
|
|
137
|
+
|
|
138
|
+
Options shared by {@link guardToolExecute} and {@link guardAITool}.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
interface GuardOptions {
|
|
142
|
+
/**
|
|
143
|
+
* Redactor applied to a THROWN error's message before it is returned to the
|
|
144
|
+
* model. Defaults to this package's `redactSecrets`.
|
|
145
|
+
*/
|
|
146
|
+
redactError?: (message: string) => string
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
#### `NormalizableSchema`
|
|
151
|
+
|
|
152
|
+
The JSON-Schema-ish shape every tool in this package declares.
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
interface NormalizableSchema {
|
|
156
|
+
name: string
|
|
157
|
+
parameters?: {
|
|
158
|
+
type?: string
|
|
159
|
+
properties?: Record<string, { type?: string; description?: string }>
|
|
160
|
+
required?: string[]
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
#### `NormalizedInput`
|
|
166
|
+
|
|
167
|
+
The result of normalizing one tool call's input.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
interface NormalizedInput {
|
|
171
|
+
/** The input to hand the handler, with aliases moved onto the declared names. */
|
|
172
|
+
input: Record<string, unknown>
|
|
173
|
+
/** Declared-required parameters still absent after aliasing. */
|
|
174
|
+
missing: string[]
|
|
175
|
+
/** `alias→declared` pairs that were moved, for logging. */
|
|
176
|
+
aliased: string[]
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
136
180
|
#### `PromptContext`
|
|
137
181
|
|
|
138
182
|
Context for building a composable system prompt.
|
|
@@ -355,6 +399,21 @@ function createSandboxBackend(sandbox: SandboxLike, projectRoot?: string): Execu
|
|
|
355
399
|
|
|
356
400
|
**Returns:** A backend that proxies all filesystem calls into the sandbox.
|
|
357
401
|
|
|
402
|
+
#### `declaredBudgetSeconds(command, timeoutParam)`
|
|
403
|
+
|
|
404
|
+
The time budget a COMMAND declares for itself — a `timeout <seconds>` prefix
|
|
405
|
+
anywhere in it, or the tool call's own `timeout` (ms) parameter, whichever is
|
|
406
|
+
larger. Zero when it declares none.
|
|
407
|
+
|
|
408
|
+
```typescript
|
|
409
|
+
function declaredBudgetSeconds(command: string, timeoutParam: unknown): number
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
- `command` — The shell command.
|
|
413
|
+
- `timeoutParam` — The tool call's `timeout` input, in milliseconds.
|
|
414
|
+
|
|
415
|
+
**Returns:** The declared budget in seconds, or 0.
|
|
416
|
+
|
|
358
417
|
#### `directoryReadHint(message, path)`
|
|
359
418
|
|
|
360
419
|
Detect a "read/edit targeted a directory, not a file" failure from a backend
|
|
@@ -386,6 +445,50 @@ function discoverSkills(backend: ExecutionBackend): Promise<SkillEntry[]>
|
|
|
386
445
|
|
|
387
446
|
**Returns:** Array of discovered skills with name, description, and path
|
|
388
447
|
|
|
448
|
+
#### `guardAITool(tool)`
|
|
449
|
+
|
|
450
|
+
Apply {@link guardToolExecute} to a ready-made tool object (the
|
|
451
|
+
`@molecule/api-ai` `AITool` shape: `{ name, description, parameters,
|
|
452
|
+
execute }`), returning a copy whose `execute` is guarded.
|
|
453
|
+
|
|
454
|
+
Use it at the point a consumer hands its full tool list to the model, so
|
|
455
|
+
tools built outside this package get the same treatment as the ones built
|
|
456
|
+
inside it.
|
|
457
|
+
|
|
458
|
+
```typescript
|
|
459
|
+
function guardAITool(tool: T, options?: GuardOptions): T
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
- `tool` — The tool to guard.
|
|
463
|
+
|
|
464
|
+
**Returns:** A copy with a guarded `execute`.
|
|
465
|
+
|
|
466
|
+
#### `guardToolExecute(schema, impl)`
|
|
467
|
+
|
|
468
|
+
Wrap a tool handler so every call is normalized first and a handler that
|
|
469
|
+
THROWS returns something the model can act on.
|
|
470
|
+
|
|
471
|
+
The guard deliberately does NOT pre-empt the handler when a required
|
|
472
|
+
parameter looks absent: several handlers accept a documented older shape
|
|
473
|
+
(`edit_file`'s top-level `old_string`/`new_string`) or answer a missing
|
|
474
|
+
parameter with far better guidance than a generic message could (`edit_file`
|
|
475
|
+
telling the model to re-read the file). It steps in only where the handler
|
|
476
|
+
would otherwise crash — and when it does, a still-missing required parameter
|
|
477
|
+
is the accurate diagnosis for that crash, so it is reported as one.
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
function guardToolExecute(
|
|
481
|
+
schema: NormalizableSchema,
|
|
482
|
+
impl: (input: Record<string, unknown>, ...rest: never[]) => Promise<unknown>,
|
|
483
|
+
options?: GuardOptions,
|
|
484
|
+
): (input: unknown, ...rest: never[]) => Promise<unknown>
|
|
485
|
+
```
|
|
486
|
+
|
|
487
|
+
- `schema` — The tool's declared schema.
|
|
488
|
+
- `impl` — The raw handler.
|
|
489
|
+
|
|
490
|
+
**Returns:** A handler with the same signature, guarded.
|
|
491
|
+
|
|
389
492
|
#### `isEnvFilePath(path)`
|
|
390
493
|
|
|
391
494
|
Whether a path is an env file, for which {@link redactSecrets}' full env-dump
|
|
@@ -422,6 +525,62 @@ function isValidGlob(pattern: string): boolean
|
|
|
422
525
|
|
|
423
526
|
**Returns:** `true` when the pattern contains only allowed characters.
|
|
424
527
|
|
|
528
|
+
#### `missingParamError(schema, input, missing)`
|
|
529
|
+
|
|
530
|
+
The error a tool returns when a required parameter is still absent after
|
|
531
|
+
aliasing. Model-facing, so it names the right parameter, what was actually
|
|
532
|
+
sent, and the shape of a correct call — everything needed to fix it in one
|
|
533
|
+
turn instead of concluding the tool is broken.
|
|
534
|
+
|
|
535
|
+
```typescript
|
|
536
|
+
function missingParamError(
|
|
537
|
+
schema: NormalizableSchema,
|
|
538
|
+
input: Record<string, unknown>,
|
|
539
|
+
missing: string[],
|
|
540
|
+
): string
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
- `schema` — The tool's declared schema.
|
|
544
|
+
- `input` — The normalized input (used to list the keys that were sent).
|
|
545
|
+
- `missing` — The required parameters still absent.
|
|
546
|
+
|
|
547
|
+
**Returns:** A single-sentence-per-clause error string.
|
|
548
|
+
|
|
549
|
+
#### `normalizeToolInput(schema, raw)`
|
|
550
|
+
|
|
551
|
+
Move known aliases onto the declared parameter names and report what is still
|
|
552
|
+
missing. Unknown keys are left in place — handlers ignore them, and dropping
|
|
553
|
+
them would hide a model's intent from anything reading the call log.
|
|
554
|
+
|
|
555
|
+
```typescript
|
|
556
|
+
function normalizeToolInput(schema: NormalizableSchema, raw: unknown): NormalizedInput
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
- `schema` — The tool's declared schema.
|
|
560
|
+
- `raw` — Whatever the model sent as the tool input.
|
|
561
|
+
|
|
562
|
+
**Returns:** The normalized input plus the missing-required and aliased lists.
|
|
563
|
+
|
|
564
|
+
#### `outputIsWithheldUntilExit(command)`
|
|
565
|
+
|
|
566
|
+
Whether a command's output reaches the tool only when the whole pipeline
|
|
567
|
+
ends — a pipe into `tail`, `grep`, `sort`, `wc` and friends, which
|
|
568
|
+
block-buffer when stdout is not a terminal.
|
|
569
|
+
|
|
570
|
+
It matters only in combination with an overrun: a command stopped at the
|
|
571
|
+
tool's ceiling normally hands back everything it printed, but behind one of
|
|
572
|
+
these it printed nothing, so the whole budget is spent for no information.
|
|
573
|
+
Measured across six real agent runs: ten such commands, every one of them
|
|
574
|
+
piped, 49 minutes returning a few hundred bytes each.
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
function outputIsWithheldUntilExit(command: string): boolean
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
- `command` — The shell command.
|
|
581
|
+
|
|
582
|
+
**Returns:** True when a kill would return no useful output.
|
|
583
|
+
|
|
425
584
|
#### `pathArgError(path, tool)`
|
|
426
585
|
|
|
427
586
|
Validate a file tool's `path` argument is a non-empty string. A weak model
|
|
@@ -512,6 +671,28 @@ function shellQuote(s: string): string
|
|
|
512
671
|
|
|
513
672
|
**Returns:** A single-quoted shell literal representing `s`.
|
|
514
673
|
|
|
674
|
+
#### `sliceLines(content, window)`
|
|
675
|
+
|
|
676
|
+
Return a line window of `content`, or null when no window was asked for.
|
|
677
|
+
|
|
678
|
+
`read_file` returned whole files only, so an executor that wanted part of one
|
|
679
|
+
shelled out: across six real agent runs, 260 of 377 shell-inspection commands
|
|
680
|
+
(69%) were `head`/`tail`/`sed -n` windowing a file the tool could have
|
|
681
|
+
windowed itself. `offset` is 1-based, matching the line numbers every other
|
|
682
|
+
tool reports and what `grep -n` prints.
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
function sliceLines(
|
|
686
|
+
content: string,
|
|
687
|
+
window?: { offset?: unknown; limit?: unknown },
|
|
688
|
+
): { text: string; offset: number; lines: number; totalLines: number; truncated: boolean } | null
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
- `content` — The file's full text.
|
|
692
|
+
- `window` — The requested `offset` (1-based) and `limit` (line count).
|
|
693
|
+
|
|
694
|
+
**Returns:** The window plus its position, or null when neither was given.
|
|
695
|
+
|
|
515
696
|
#### `stripControlChars(s)`
|
|
516
697
|
|
|
517
698
|
Strip C0 control chars (except tab, newline, CR) that break PostgreSQL JSONB
|
|
@@ -614,6 +795,25 @@ const DEFAULT_SEARCH_EXCLUDED_DIRS: readonly [
|
|
|
614
795
|
]
|
|
615
796
|
```
|
|
616
797
|
|
|
798
|
+
#### `MAX_BATCH_READ_BYTES`
|
|
799
|
+
|
|
800
|
+
Total content bytes one batched `read_file` call returns, across all files.
|
|
801
|
+
The per-file {@link MAX_READ_SIZE} still applies to each one.
|
|
802
|
+
|
|
803
|
+
```typescript
|
|
804
|
+
const MAX_BATCH_READ_BYTES: number
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
#### `MAX_BATCH_READ_FILES`
|
|
808
|
+
|
|
809
|
+
Most files one batched `read_file` call returns. The cap exists so a survey
|
|
810
|
+
of a large tree cannot swallow the turn's context in one result; the tool
|
|
811
|
+
reports where it stopped so the model continues from there.
|
|
812
|
+
|
|
813
|
+
```typescript
|
|
814
|
+
const MAX_BATCH_READ_FILES: 25
|
|
815
|
+
```
|
|
816
|
+
|
|
617
817
|
#### `MAX_FIND_RESULTS`
|
|
618
818
|
|
|
619
819
|
Max find results.
|
|
@@ -630,6 +830,23 @@ Max command output size (100KB per stream).
|
|
|
630
830
|
const MAX_OUTPUT_SIZE: number
|
|
631
831
|
```
|
|
632
832
|
|
|
833
|
+
#### `MAX_READ_RETURN_CHARS`
|
|
834
|
+
|
|
835
|
+
The most one read_file call returns for ONE file when no window was asked
|
|
836
|
+
for, in chars. Larger files come back as their first window with a note
|
|
837
|
+
saying how to read the rest.
|
|
838
|
+
|
|
839
|
+
MAX_READ_SIZE (5 MB) is a bound on what the tool will open, not on what it
|
|
840
|
+
should hand a model: a single batched read that returned a 1.2 MB README
|
|
841
|
+
put the next three provider calls at 366k tokens (~330k of them fresh)
|
|
842
|
+
against a 120k-token context cap (X0 run x5, call 26). ~80 KB is ~20–25k
|
|
843
|
+
tokens — a fifth of that cap, and more than any one file read usually
|
|
844
|
+
needs.
|
|
845
|
+
|
|
846
|
+
```typescript
|
|
847
|
+
const MAX_READ_RETURN_CHARS: number
|
|
848
|
+
```
|
|
849
|
+
|
|
633
850
|
#### `MAX_READ_SIZE`
|
|
634
851
|
|
|
635
852
|
Max file size for read_file (5MB).
|
|
@@ -638,6 +855,16 @@ Max file size for read_file (5MB).
|
|
|
638
855
|
const MAX_READ_SIZE: number
|
|
639
856
|
```
|
|
640
857
|
|
|
858
|
+
#### `MAX_SEARCH_CONTEXT_LINES`
|
|
859
|
+
|
|
860
|
+
Most lines of surrounding context `search_files` will return per match.
|
|
861
|
+
Bounded so a broad pattern with a generous context cannot pull a whole tree
|
|
862
|
+
into one reply.
|
|
863
|
+
|
|
864
|
+
```typescript
|
|
865
|
+
const MAX_SEARCH_CONTEXT_LINES: 40
|
|
866
|
+
```
|
|
867
|
+
|
|
641
868
|
#### `MAX_SEARCH_RESULTS`
|
|
642
869
|
|
|
643
870
|
Max search results.
|
|
@@ -685,6 +912,19 @@ Peer dependencies:
|
|
|
685
912
|
rules via `blockCommand` (return an actionable error string to refuse — it is
|
|
686
913
|
shown to the model verbatim) and enable `blockDangerousCommands` (default
|
|
687
914
|
FALSE) where appropriate.
|
|
915
|
+
- **`read_file` takes `paths` as well as `path`.** An agent that issues one
|
|
916
|
+
tool call per model round-trip pays a full round-trip per file, so a project
|
|
917
|
+
survey costs minutes; batched, the same reads cost one call each 25 files
|
|
918
|
+
(`MAX_BATCH_READ_FILES` / `MAX_BATCH_READ_BYTES`, which report where they
|
|
919
|
+
stopped). Each entry carries its own content or its own error, so one bad
|
|
920
|
+
path never fails the batch.
|
|
921
|
+
- **A parameter under another name is not a broken tool.** Every tool's input
|
|
922
|
+
is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
|
|
923
|
+
snake_case), and a handler that throws returns an error naming the missing
|
|
924
|
+
parameter rather than a bare internal message — which a weak executor reads
|
|
925
|
+
as a broken tool and routes around, taking its verification with it. Use
|
|
926
|
+
`guardAITool` to give tools built elsewhere the same treatment, passing
|
|
927
|
+
`redactError` so a thrown message is masked by your own redactor.
|
|
688
928
|
- **Scope the tool set per agent** with `include`/`exclude` — a read-only agent
|
|
689
929
|
should not receive `write_file`/`exec_command`.
|
|
690
930
|
- Outputs are size-capped (`MAX_READ_SIZE`, `MAX_OUTPUT_SIZE`, …) and searches
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,19 @@
|
|
|
23
23
|
* rules via `blockCommand` (return an actionable error string to refuse — it is
|
|
24
24
|
* shown to the model verbatim) and enable `blockDangerousCommands` (default
|
|
25
25
|
* FALSE) where appropriate.
|
|
26
|
+
* - **`read_file` takes `paths` as well as `path`.** An agent that issues one
|
|
27
|
+
* tool call per model round-trip pays a full round-trip per file, so a project
|
|
28
|
+
* survey costs minutes; batched, the same reads cost one call each 25 files
|
|
29
|
+
* (`MAX_BATCH_READ_FILES` / `MAX_BATCH_READ_BYTES`, which report where they
|
|
30
|
+
* stopped). Each entry carries its own content or its own error, so one bad
|
|
31
|
+
* path never fails the batch.
|
|
32
|
+
* - **A parameter under another name is not a broken tool.** Every tool's input
|
|
33
|
+
* is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
|
|
34
|
+
* snake_case), and a handler that throws returns an error naming the missing
|
|
35
|
+
* parameter rather than a bare internal message — which a weak executor reads
|
|
36
|
+
* as a broken tool and routes around, taking its verification with it. Use
|
|
37
|
+
* `guardAITool` to give tools built elsewhere the same treatment, passing
|
|
38
|
+
* `redactError` so a thrown message is masked by your own redactor.
|
|
26
39
|
* - **Scope the tool set per agent** with `include`/`exclude` — a read-only agent
|
|
27
40
|
* should not receive `write_file`/`exec_command`.
|
|
28
41
|
* - Outputs are size-capped (`MAX_READ_SIZE`, `MAX_OUTPUT_SIZE`, …) and searches
|
|
@@ -85,6 +98,7 @@
|
|
|
85
98
|
export * from './backends/local.js';
|
|
86
99
|
export * from './backends/sandbox.js';
|
|
87
100
|
export * from './browser-guard.js';
|
|
101
|
+
export * from './input-normalizer.js';
|
|
88
102
|
export * from './schemas.js';
|
|
89
103
|
export * from './system-prompt.js';
|
|
90
104
|
export * from './tools.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgGG;AAEH,cAAc,qBAAqB,CAAA;AACnC,cAAc,uBAAuB,CAAA;AACrC,cAAc,oBAAoB,CAAA;AAClC,cAAc,uBAAuB,CAAA;AACrC,cAAc,cAAc,CAAA;AAC5B,cAAc,oBAAoB,CAAA;AAClC,cAAc,YAAY,CAAA;AAC1B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,19 @@
|
|
|
23
23
|
* rules via `blockCommand` (return an actionable error string to refuse — it is
|
|
24
24
|
* shown to the model verbatim) and enable `blockDangerousCommands` (default
|
|
25
25
|
* FALSE) where appropriate.
|
|
26
|
+
* - **`read_file` takes `paths` as well as `path`.** An agent that issues one
|
|
27
|
+
* tool call per model round-trip pays a full round-trip per file, so a project
|
|
28
|
+
* survey costs minutes; batched, the same reads cost one call each 25 files
|
|
29
|
+
* (`MAX_BATCH_READ_FILES` / `MAX_BATCH_READ_BYTES`, which report where they
|
|
30
|
+
* stopped). Each entry carries its own content or its own error, so one bad
|
|
31
|
+
* path never fails the batch.
|
|
32
|
+
* - **A parameter under another name is not a broken tool.** Every tool's input
|
|
33
|
+
* is normalized first (`cmd` → `command`, `file` → `path`, camelCase ⇄
|
|
34
|
+
* snake_case), and a handler that throws returns an error naming the missing
|
|
35
|
+
* parameter rather than a bare internal message — which a weak executor reads
|
|
36
|
+
* as a broken tool and routes around, taking its verification with it. Use
|
|
37
|
+
* `guardAITool` to give tools built elsewhere the same treatment, passing
|
|
38
|
+
* `redactError` so a thrown message is masked by your own redactor.
|
|
26
39
|
* - **Scope the tool set per agent** with `include`/`exclude` — a read-only agent
|
|
27
40
|
* should not receive `write_file`/`exec_command`.
|
|
28
41
|
* - Outputs are size-capped (`MAX_READ_SIZE`, `MAX_OUTPUT_SIZE`, …) and searches
|
|
@@ -85,6 +98,7 @@
|
|
|
85
98
|
export * from './backends/local.js';
|
|
86
99
|
export * from './backends/sandbox.js';
|
|
87
100
|
export * from './browser-guard.js';
|
|
101
|
+
export * from './input-normalizer.js';
|
|
88
102
|
export * from './schemas.js';
|
|
89
103
|
export * from './system-prompt.js';
|
|
90
104
|
export * from './tools.js';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-input normalization: accept the parameter names a weak executor
|
|
3
|
+
* actually sends, and never let a missing one reach a handler as `undefined`.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
/** The JSON-Schema-ish shape every tool in this package declares. */
|
|
8
|
+
export interface NormalizableSchema {
|
|
9
|
+
name: string;
|
|
10
|
+
parameters?: {
|
|
11
|
+
type?: string;
|
|
12
|
+
properties?: Record<string, {
|
|
13
|
+
type?: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
}>;
|
|
16
|
+
required?: string[];
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** The result of normalizing one tool call's input. */
|
|
20
|
+
export interface NormalizedInput {
|
|
21
|
+
/** The input to hand the handler, with aliases moved onto the declared names. */
|
|
22
|
+
input: Record<string, unknown>;
|
|
23
|
+
/** Declared-required parameters still absent after aliasing. */
|
|
24
|
+
missing: string[];
|
|
25
|
+
/** `alias→declared` pairs that were moved, for logging. */
|
|
26
|
+
aliased: string[];
|
|
27
|
+
}
|
|
28
|
+
/** Options shared by {@link guardToolExecute} and {@link guardAITool}. */
|
|
29
|
+
export interface GuardOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Redactor applied to a THROWN error's message before it is returned to the
|
|
32
|
+
* model. Defaults to this package's `redactSecrets`.
|
|
33
|
+
*/
|
|
34
|
+
redactError?: (message: string) => string;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Move known aliases onto the declared parameter names and report what is still
|
|
38
|
+
* missing. Unknown keys are left in place — handlers ignore them, and dropping
|
|
39
|
+
* them would hide a model's intent from anything reading the call log.
|
|
40
|
+
*
|
|
41
|
+
* @param schema - The tool's declared schema.
|
|
42
|
+
* @param raw - Whatever the model sent as the tool input.
|
|
43
|
+
* @returns The normalized input plus the missing-required and aliased lists.
|
|
44
|
+
*/
|
|
45
|
+
export declare function normalizeToolInput(schema: NormalizableSchema, raw: unknown): NormalizedInput;
|
|
46
|
+
/**
|
|
47
|
+
* The error a tool returns when a required parameter is still absent after
|
|
48
|
+
* aliasing. Model-facing, so it names the right parameter, what was actually
|
|
49
|
+
* sent, and the shape of a correct call — everything needed to fix it in one
|
|
50
|
+
* turn instead of concluding the tool is broken.
|
|
51
|
+
*
|
|
52
|
+
* @param schema - The tool's declared schema.
|
|
53
|
+
* @param input - The normalized input (used to list the keys that were sent).
|
|
54
|
+
* @param missing - The required parameters still absent.
|
|
55
|
+
* @returns A single-sentence-per-clause error string.
|
|
56
|
+
*/
|
|
57
|
+
export declare function missingParamError(schema: NormalizableSchema, input: Record<string, unknown>, missing: string[]): string;
|
|
58
|
+
/**
|
|
59
|
+
* Wrap a tool handler so every call is normalized first and a handler that
|
|
60
|
+
* THROWS returns something the model can act on.
|
|
61
|
+
*
|
|
62
|
+
* The guard deliberately does NOT pre-empt the handler when a required
|
|
63
|
+
* parameter looks absent: several handlers accept a documented older shape
|
|
64
|
+
* (`edit_file`'s top-level `old_string`/`new_string`) or answer a missing
|
|
65
|
+
* parameter with far better guidance than a generic message could (`edit_file`
|
|
66
|
+
* telling the model to re-read the file). It steps in only where the handler
|
|
67
|
+
* would otherwise crash — and when it does, a still-missing required parameter
|
|
68
|
+
* is the accurate diagnosis for that crash, so it is reported as one.
|
|
69
|
+
*
|
|
70
|
+
* @param schema - The tool's declared schema.
|
|
71
|
+
* @param impl - The raw handler.
|
|
72
|
+
* @returns A handler with the same signature, guarded.
|
|
73
|
+
*/
|
|
74
|
+
export declare function guardToolExecute(schema: NormalizableSchema, impl: (input: Record<string, unknown>, ...rest: never[]) => Promise<unknown>, options?: GuardOptions): (input: unknown, ...rest: never[]) => Promise<unknown>;
|
|
75
|
+
/**
|
|
76
|
+
* Apply {@link guardToolExecute} to a ready-made tool object (the
|
|
77
|
+
* `@molecule/api-ai` `AITool` shape: `{ name, description, parameters,
|
|
78
|
+
* execute }`), returning a copy whose `execute` is guarded.
|
|
79
|
+
*
|
|
80
|
+
* Use it at the point a consumer hands its full tool list to the model, so
|
|
81
|
+
* tools built outside this package get the same treatment as the ones built
|
|
82
|
+
* inside it.
|
|
83
|
+
*
|
|
84
|
+
* @param tool - The tool to guard.
|
|
85
|
+
* @returns A copy with a guarded `execute`.
|
|
86
|
+
*/
|
|
87
|
+
export declare function guardAITool<T extends {
|
|
88
|
+
name: string;
|
|
89
|
+
parameters?: unknown;
|
|
90
|
+
execute: (input: unknown, ...rest: never[]) => Promise<unknown>;
|
|
91
|
+
}>(tool: T, options?: GuardOptions): T;
|
|
92
|
+
//# sourceMappingURL=input-normalizer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"input-normalizer.d.ts","sourceRoot":"","sources":["../src/input-normalizer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,qEAAqE;AACrE,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE;QACX,IAAI,CAAC,EAAE,MAAM,CAAA;QACb,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE;YAAE,IAAI,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;QACpE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAA;KACpB,CAAA;CACF;AAED,uDAAuD;AACvD,MAAM,WAAW,eAAe;IAC9B,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;IAC9B,gEAAgE;IAChE,OAAO,EAAE,MAAM,EAAE,CAAA;IACjB,2DAA2D;IAC3D,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAiDD,0EAA0E;AAC1E,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAA;CAC1C;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,kBAAkB,EAAE,GAAG,EAAE,OAAO,GAAG,eAAe,CAiD5F;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,kBAAkB,EAC1B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,EAAE,GAChB,MAAM,CAaR;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,EAC5E,OAAO,GAAE,YAAiB,GACzB,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAsBxD;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,WAAW,CACzB,CAAC,SAAS;IACR,IAAI,EAAE,MAAM,CAAA;IACZ,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;CAChE,EACD,IAAI,EAAE,CAAC,EAAE,OAAO,GAAE,YAAiB,GAAG,CAAC,CAaxC"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-input normalization: accept the parameter names a weak executor
|
|
3
|
+
* actually sends, and never let a missing one reach a handler as `undefined`.
|
|
4
|
+
*
|
|
5
|
+
* @module
|
|
6
|
+
*/
|
|
7
|
+
import { redactSecrets } from './utilities.js';
|
|
8
|
+
/**
|
|
9
|
+
* Other names a model sends for a declared parameter, most likely first.
|
|
10
|
+
*
|
|
11
|
+
* Every entry here is a name a real run actually produced or a direct synonym
|
|
12
|
+
* of one. `cmd` is the one that cost the most: `exec_command` read only
|
|
13
|
+
* `input.command`, so a call sent as `{ cmd, timeout }` reached
|
|
14
|
+
* `checkBlockedCommand(undefined)` and died with `Cannot read properties of
|
|
15
|
+
* undefined (reading 'match')` — an internal TypeError, not a usable error. The
|
|
16
|
+
* executor read that as "the shell tool is failing consistently now", stopped
|
|
17
|
+
* verifying, and ended the turn reporting "All checks pass" while seven
|
|
18
|
+
* acceptance checks failed (X0 rehearsal 83).
|
|
19
|
+
*/
|
|
20
|
+
const PARAM_ALIASES = {
|
|
21
|
+
command: ['cmd', 'shell', 'script', 'bash', 'commandLine', 'command_line', 'run'],
|
|
22
|
+
path: ['file', 'filePath', 'file_path', 'filename', 'file_name', 'filepath', 'target'],
|
|
23
|
+
content: [
|
|
24
|
+
'contents',
|
|
25
|
+
'body',
|
|
26
|
+
'text',
|
|
27
|
+
'data',
|
|
28
|
+
'source',
|
|
29
|
+
'markdown',
|
|
30
|
+
'fileContent',
|
|
31
|
+
'file_content',
|
|
32
|
+
],
|
|
33
|
+
pattern: ['query', 'regex', 'search', 'searchPattern', 'search_pattern', 'text'],
|
|
34
|
+
old_path: ['from', 'src', 'source', 'sourcePath', 'source_path', 'oldPath', 'old'],
|
|
35
|
+
new_path: ['to', 'dest', 'destination', 'destinationPath', 'destination_path', 'newPath', 'new'],
|
|
36
|
+
name: ['skill', 'skillName', 'skill_name', 'title', 'planName', 'plan_name'],
|
|
37
|
+
cwd: ['dir', 'directory', 'workdir', 'workingDirectory', 'working_directory'],
|
|
38
|
+
include: ['glob', 'filePattern', 'file_pattern'],
|
|
39
|
+
};
|
|
40
|
+
/** `old_path` ⇄ `oldPath`, so a casing slip is never a missing parameter. */
|
|
41
|
+
function caseVariants(key) {
|
|
42
|
+
const camel = key.replace(/_([a-z])/g, (_m, c) => c.toUpperCase());
|
|
43
|
+
const snake = key.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
|
|
44
|
+
return [camel, snake].filter((v) => v !== key);
|
|
45
|
+
}
|
|
46
|
+
/** Present means the key is there with a value that is not null/undefined. */
|
|
47
|
+
function present(obj, key) {
|
|
48
|
+
return Object.hasOwn(obj, key) && obj[key] !== undefined && obj[key] !== null;
|
|
49
|
+
}
|
|
50
|
+
const warned = new Set();
|
|
51
|
+
/**
|
|
52
|
+
* Move known aliases onto the declared parameter names and report what is still
|
|
53
|
+
* missing. Unknown keys are left in place — handlers ignore them, and dropping
|
|
54
|
+
* them would hide a model's intent from anything reading the call log.
|
|
55
|
+
*
|
|
56
|
+
* @param schema - The tool's declared schema.
|
|
57
|
+
* @param raw - Whatever the model sent as the tool input.
|
|
58
|
+
* @returns The normalized input plus the missing-required and aliased lists.
|
|
59
|
+
*/
|
|
60
|
+
export function normalizeToolInput(schema, raw) {
|
|
61
|
+
const input = typeof raw === 'object' && raw !== null && !Array.isArray(raw)
|
|
62
|
+
? { ...raw }
|
|
63
|
+
: {};
|
|
64
|
+
const properties = schema.parameters?.properties ?? {};
|
|
65
|
+
const declared = Object.keys(properties);
|
|
66
|
+
const declaredSet = new Set(declared);
|
|
67
|
+
const aliased = [];
|
|
68
|
+
for (const key of declared) {
|
|
69
|
+
if (present(input, key))
|
|
70
|
+
continue;
|
|
71
|
+
const candidates = [...caseVariants(key), ...(PARAM_ALIASES[key] ?? [])];
|
|
72
|
+
for (const alias of candidates) {
|
|
73
|
+
// Never steal a name this same schema declares — `path` on rename_file
|
|
74
|
+
// would be ambiguous between old_path and new_path, so it stays unread.
|
|
75
|
+
if (declaredSet.has(alias))
|
|
76
|
+
continue;
|
|
77
|
+
if (!present(input, alias))
|
|
78
|
+
continue;
|
|
79
|
+
input[key] = input[alias];
|
|
80
|
+
aliased.push(`${alias}→${key}`);
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// A declared string that arrived as a number/boolean is stringified rather
|
|
85
|
+
// than handed to a handler that will call .match()/.split() on it.
|
|
86
|
+
for (const key of declared) {
|
|
87
|
+
if (properties[key]?.type !== 'string')
|
|
88
|
+
continue;
|
|
89
|
+
const value = input[key];
|
|
90
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
91
|
+
input[key] = String(value);
|
|
92
|
+
}
|
|
93
|
+
const required = schema.parameters?.required ?? [];
|
|
94
|
+
const missing = required.filter((key) => {
|
|
95
|
+
if (!present(input, key))
|
|
96
|
+
return true;
|
|
97
|
+
const value = input[key];
|
|
98
|
+
return typeof value === 'string' && value.trim() === '' && key !== 'content';
|
|
99
|
+
});
|
|
100
|
+
if (aliased.length > 0) {
|
|
101
|
+
const signature = `${schema.name}:${aliased.join(',')}`;
|
|
102
|
+
if (!warned.has(signature)) {
|
|
103
|
+
warned.add(signature);
|
|
104
|
+
console.warn(`[ai-tools] ${schema.name} input normalized (${aliased.join(', ')})`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { input, missing, aliased };
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The error a tool returns when a required parameter is still absent after
|
|
111
|
+
* aliasing. Model-facing, so it names the right parameter, what was actually
|
|
112
|
+
* sent, and the shape of a correct call — everything needed to fix it in one
|
|
113
|
+
* turn instead of concluding the tool is broken.
|
|
114
|
+
*
|
|
115
|
+
* @param schema - The tool's declared schema.
|
|
116
|
+
* @param input - The normalized input (used to list the keys that were sent).
|
|
117
|
+
* @param missing - The required parameters still absent.
|
|
118
|
+
* @returns A single-sentence-per-clause error string.
|
|
119
|
+
*/
|
|
120
|
+
export function missingParamError(schema, input, missing) {
|
|
121
|
+
const properties = schema.parameters?.properties ?? {};
|
|
122
|
+
const sent = Object.keys(input);
|
|
123
|
+
const names = missing.map((k) => `"${k}"`).join(', ');
|
|
124
|
+
const example = Object.fromEntries((schema.parameters?.required ?? []).map((k) => [k, `<${properties[k]?.type ?? 'value'}>`]));
|
|
125
|
+
return (`${schema.name} needs ${missing.length === 1 ? 'the parameter' : 'the parameters'} ${names}. ` +
|
|
126
|
+
(sent.length > 0 ? `You sent: ${sent.join(', ')}. ` : 'You sent no parameters. ') +
|
|
127
|
+
`Call it as ${JSON.stringify(example)}. This is a parameter-name problem, not a broken tool — ` +
|
|
128
|
+
'retry the same call with the names above.');
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Wrap a tool handler so every call is normalized first and a handler that
|
|
132
|
+
* THROWS returns something the model can act on.
|
|
133
|
+
*
|
|
134
|
+
* The guard deliberately does NOT pre-empt the handler when a required
|
|
135
|
+
* parameter looks absent: several handlers accept a documented older shape
|
|
136
|
+
* (`edit_file`'s top-level `old_string`/`new_string`) or answer a missing
|
|
137
|
+
* parameter with far better guidance than a generic message could (`edit_file`
|
|
138
|
+
* telling the model to re-read the file). It steps in only where the handler
|
|
139
|
+
* would otherwise crash — and when it does, a still-missing required parameter
|
|
140
|
+
* is the accurate diagnosis for that crash, so it is reported as one.
|
|
141
|
+
*
|
|
142
|
+
* @param schema - The tool's declared schema.
|
|
143
|
+
* @param impl - The raw handler.
|
|
144
|
+
* @returns A handler with the same signature, guarded.
|
|
145
|
+
*/
|
|
146
|
+
export function guardToolExecute(schema, impl, options = {}) {
|
|
147
|
+
// A thrown message is untrusted text (a connection string in a driver error,
|
|
148
|
+
// a key echoed by a failing command). The guard turns a throw into a RETURNED
|
|
149
|
+
// result, which leaves the consumer's thrown-error redaction path unused — so
|
|
150
|
+
// the message is redacted here, before it can reach the model, the SSE stream
|
|
151
|
+
// or the stored transcript. Defaults to this package's name-keyword redactor;
|
|
152
|
+
// pass a stronger one where the consumer has it.
|
|
153
|
+
const redactError = options.redactError ?? redactSecrets;
|
|
154
|
+
return async (raw, ...rest) => {
|
|
155
|
+
const { input, missing } = normalizeToolInput(schema, raw);
|
|
156
|
+
try {
|
|
157
|
+
return await impl(input, ...rest);
|
|
158
|
+
}
|
|
159
|
+
catch (e) {
|
|
160
|
+
if (missing.length > 0)
|
|
161
|
+
return { error: missingParamError(schema, input, missing) };
|
|
162
|
+
const message = redactError(e instanceof Error ? e.message : String(e));
|
|
163
|
+
return {
|
|
164
|
+
error: `${schema.name} failed: ${message}. Parameters sent: ${Object.keys(input).join(', ') || '(none)'}. ` +
|
|
165
|
+
`This tool expects ${JSON.stringify(schema.parameters?.required ?? [])} — check them and retry.`,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Apply {@link guardToolExecute} to a ready-made tool object (the
|
|
172
|
+
* `@molecule/api-ai` `AITool` shape: `{ name, description, parameters,
|
|
173
|
+
* execute }`), returning a copy whose `execute` is guarded.
|
|
174
|
+
*
|
|
175
|
+
* Use it at the point a consumer hands its full tool list to the model, so
|
|
176
|
+
* tools built outside this package get the same treatment as the ones built
|
|
177
|
+
* inside it.
|
|
178
|
+
*
|
|
179
|
+
* @param tool - The tool to guard.
|
|
180
|
+
* @returns A copy with a guarded `execute`.
|
|
181
|
+
*/
|
|
182
|
+
export function guardAITool(tool, options = {}) {
|
|
183
|
+
const schema = {
|
|
184
|
+
name: tool.name,
|
|
185
|
+
parameters: tool.parameters,
|
|
186
|
+
};
|
|
187
|
+
return {
|
|
188
|
+
...tool,
|
|
189
|
+
execute: guardToolExecute(schema, tool.execute, options),
|
|
190
|
+
};
|
|
191
|
+
}
|
package/dist/schemas.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../src/schemas.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAElD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,UAAU,CAAA;CACvB;AAED;;GAEG;AACH,eAAO,MAAM,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAqT1C,CAAA"}
|