@frockbot/plugin-tools 0.3.8 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-tools",
3
- "version": "0.3.8",
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.8",
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: { content: `Unknown tool: ${call.name}`, isError: true },
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