@frockbot/plugin-tools 0.3.9 → 0.3.10
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/package.json +2 -2
- package/src/dynamic-tools.test.ts +42 -0
- package/src/tools.ts +28 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-tools",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
20
|
+
"@frockbot/kernel-contracts": "0.3.10",
|
|
21
21
|
"cordis": "4.0.0-rc.8"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
@@ -590,3 +590,45 @@ describe("progressive tool disclosure", () => {
|
|
|
590
590
|
);
|
|
591
591
|
});
|
|
592
592
|
});
|
|
593
|
+
|
|
594
|
+
describe("a dynamic tool called by its bare name", () => {
|
|
595
|
+
test("is answered with the envelope, not a dead-end Unknown tool", async () => {
|
|
596
|
+
// `applet_list` is listed to the model by bare name in
|
|
597
|
+
// `<dynamic_tool_namespaces>`, so the model reaches for it that way. The
|
|
598
|
+
// refusal used to be `Unknown tool: applet_list` and nothing else — a dead
|
|
599
|
+
// end for a tool that was right there.
|
|
600
|
+
const root = await rootWithTools();
|
|
601
|
+
root.tools.register(dynamicTool("applets", "applet_list"));
|
|
602
|
+
|
|
603
|
+
const result = await invoke(root, "applet_list", {});
|
|
604
|
+
|
|
605
|
+
expect(result.isError).toBe(true);
|
|
606
|
+
expect(result.content).toContain("applet_list");
|
|
607
|
+
expect(result.content).toContain(CALL_DYNAMIC_TOOL_NAME);
|
|
608
|
+
expect(result.content).toContain(
|
|
609
|
+
'{"namespace":"applets","toolName":"applet_list"',
|
|
610
|
+
);
|
|
611
|
+
expect(result.content).toContain(GET_DYNAMIC_TOOLS_NAME);
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
test("names every namespace the bare name is in", async () => {
|
|
615
|
+
const root = await rootWithTools();
|
|
616
|
+
root.tools.register(dynamicTool("mail-one", "search"));
|
|
617
|
+
root.tools.register(dynamicTool("mail-two", "search"));
|
|
618
|
+
|
|
619
|
+
const result = await invoke(root, "search", {});
|
|
620
|
+
|
|
621
|
+
expect(result.isError).toBe(true);
|
|
622
|
+
expect(result.content).toContain("mail-one");
|
|
623
|
+
expect(result.content).toContain("mail-two");
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("a name in no namespace still says only that it is unknown", async () => {
|
|
627
|
+
const root = await rootWithTools();
|
|
628
|
+
root.tools.register(dynamicTool("applets", "applet_list"));
|
|
629
|
+
|
|
630
|
+
const result = await invoke(root, "not_a_tool", {});
|
|
631
|
+
|
|
632
|
+
expect(result.content).toBe("Unknown tool: not_a_tool");
|
|
633
|
+
});
|
|
634
|
+
});
|
package/src/tools.ts
CHANGED
|
@@ -552,6 +552,30 @@ export class ToolRegistry extends Service implements ToolExecution {
|
|
|
552
552
|
return { kind: "denied", call, result: { content, isError: true } };
|
|
553
553
|
}
|
|
554
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Why a name is not callable — and, when it *is* callable through the
|
|
557
|
+
* envelope, how.
|
|
558
|
+
*
|
|
559
|
+
* A dynamic tool is listed to the model by bare name in
|
|
560
|
+
* `<dynamic_tool_namespaces>`, so the model reaches for it by that name and
|
|
561
|
+
* used to get `Unknown tool: applet_list` and nothing else: a dead end for a
|
|
562
|
+
* tool that was right there, costing a step every time. The refusal now
|
|
563
|
+
* hands back the exact envelope for the namespace the name is actually in.
|
|
564
|
+
*/
|
|
565
|
+
private unknownToolRefusal(name: string): string {
|
|
566
|
+
const namespaces = [...this.dynamicDefinitions]
|
|
567
|
+
.filter(([, definitions]) => definitions.has(name))
|
|
568
|
+
.map(([namespace]) => namespace)
|
|
569
|
+
.sort();
|
|
570
|
+
const first = namespaces[0];
|
|
571
|
+
if (first === undefined) return `Unknown tool: ${name}`;
|
|
572
|
+
const where =
|
|
573
|
+
namespaces.length === 1
|
|
574
|
+
? `namespace "${first}"`
|
|
575
|
+
: `namespaces ${listOrNone(namespaces)} — pick one`;
|
|
576
|
+
return `Unknown tool: ${name}. It is a dynamic tool in ${where}; call it through ${CALL_DYNAMIC_TOOL_NAME} as {"namespace":"${first}","toolName":"${name}","arguments":{ … the tool's own input … }}. Call ${GET_DYNAMIC_TOOLS_NAME}({"namespace":"${first}","toolName":"${name}"}) first if you do not have its schema.`;
|
|
577
|
+
}
|
|
578
|
+
|
|
555
579
|
private async prepareRegistered(
|
|
556
580
|
call: ToolCall,
|
|
557
581
|
registered: RegisteredTool | undefined,
|
|
@@ -566,7 +590,10 @@ export class ToolRegistry extends Service implements ToolExecution {
|
|
|
566
590
|
return {
|
|
567
591
|
kind: "denied",
|
|
568
592
|
call,
|
|
569
|
-
result: {
|
|
593
|
+
result: {
|
|
594
|
+
content: this.unknownToolRefusal(call.name),
|
|
595
|
+
isError: true,
|
|
596
|
+
},
|
|
570
597
|
};
|
|
571
598
|
}
|
|
572
599
|
// Defence in depth: the catalog was already trimmed, so a call that
|