@frockbot/plugin-tools 0.3.10 → 0.3.12
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 +69 -0
- package/src/tools.ts +22 -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.12",
|
|
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.12",
|
|
21
21
|
"cordis": "4.0.0-rc.8"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
@@ -591,6 +591,64 @@ describe("progressive tool disclosure", () => {
|
|
|
591
591
|
});
|
|
592
592
|
});
|
|
593
593
|
|
|
594
|
+
describe("the envelope discovery hands back", () => {
|
|
595
|
+
test("is the envelope dispatch accepts, for a non-external namespace", async () => {
|
|
596
|
+
// The Applets Package mounts through the isolate host, whose namespaces
|
|
597
|
+
// used to register `external: true`. The dispatch guard then refused every
|
|
598
|
+
// call without `mcpDetails.description`, while discovery's `callWith`
|
|
599
|
+
// never mentioned the field — so the envelope offered was the envelope
|
|
600
|
+
// refused, and no Applet could be created by chat at all.
|
|
601
|
+
const root = await rootWithTools();
|
|
602
|
+
root.tools.register(dynamicTool("applets", "applet_create"));
|
|
603
|
+
root.tools.registerNamespace({ name: "applets", status: "ready" });
|
|
604
|
+
|
|
605
|
+
const discovered = await invoke(root, GET_DYNAMIC_TOOLS_NAME, {
|
|
606
|
+
namespace: "applets",
|
|
607
|
+
toolName: "applet_create",
|
|
608
|
+
});
|
|
609
|
+
const envelope = JSON.parse(discovered.content as string).callWith as {
|
|
610
|
+
tool: string;
|
|
611
|
+
input: Record<string, unknown>;
|
|
612
|
+
};
|
|
613
|
+
expect(envelope.tool).toBe(CALL_DYNAMIC_TOOL_NAME);
|
|
614
|
+
expect(envelope.input.mcpDetails).toBeUndefined();
|
|
615
|
+
|
|
616
|
+
// The exact envelope, with only the placeholder arguments made real.
|
|
617
|
+
const dispatched = await invoke(root, CALL_DYNAMIC_TOOL_NAME, {
|
|
618
|
+
...envelope.input,
|
|
619
|
+
arguments: { value: "a todo list" },
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
expect(dispatched.isError).toBe(false);
|
|
623
|
+
expect(dispatched.content).toContain("a todo list");
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("carries mcpDetails when the namespace really is external", async () => {
|
|
627
|
+
const root = await rootWithTools();
|
|
628
|
+
root.tools.register(dynamicTool("linear", "issue_create"));
|
|
629
|
+
root.tools.registerNamespace({ name: "linear", external: true });
|
|
630
|
+
|
|
631
|
+
const discovered = await invoke(root, GET_DYNAMIC_TOOLS_NAME, {
|
|
632
|
+
namespace: "linear",
|
|
633
|
+
toolName: "issue_create",
|
|
634
|
+
});
|
|
635
|
+
const envelope = JSON.parse(discovered.content as string).callWith as {
|
|
636
|
+
input: Record<string, unknown>;
|
|
637
|
+
};
|
|
638
|
+
expect(envelope.input.mcpDetails).toEqual({
|
|
639
|
+
description: "<one sentence saying why you are calling this>",
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const dispatched = await invoke(root, CALL_DYNAMIC_TOOL_NAME, {
|
|
643
|
+
...envelope.input,
|
|
644
|
+
arguments: { value: "an issue" },
|
|
645
|
+
mcpDetails: { description: "Filing the bug the User just described." },
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
expect(dispatched.isError).toBe(false);
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
|
|
594
652
|
describe("a dynamic tool called by its bare name", () => {
|
|
595
653
|
test("is answered with the envelope, not a dead-end Unknown tool", async () => {
|
|
596
654
|
// `applet_list` is listed to the model by bare name in
|
|
@@ -623,6 +681,17 @@ describe("a dynamic tool called by its bare name", () => {
|
|
|
623
681
|
expect(result.content).toContain("mail-two");
|
|
624
682
|
});
|
|
625
683
|
|
|
684
|
+
test("routes an external namespace with the mcpDetails its guard demands", async () => {
|
|
685
|
+
const root = await rootWithTools();
|
|
686
|
+
root.tools.register(dynamicTool("linear", "issue_create"));
|
|
687
|
+
root.tools.registerNamespace({ name: "linear", external: true });
|
|
688
|
+
|
|
689
|
+
const result = await invoke(root, "issue_create", {});
|
|
690
|
+
|
|
691
|
+
expect(result.content).toContain("mcpDetails");
|
|
692
|
+
expect(result.content).toContain("description");
|
|
693
|
+
});
|
|
694
|
+
|
|
626
695
|
test("a name in no namespace still says only that it is unknown", async () => {
|
|
627
696
|
const root = await rootWithTools();
|
|
628
697
|
root.tools.register(dynamicTool("applets", "applet_list"));
|
package/src/tools.ts
CHANGED
|
@@ -573,7 +573,14 @@ export class ToolRegistry extends Service implements ToolExecution {
|
|
|
573
573
|
namespaces.length === 1
|
|
574
574
|
? `namespace "${first}"`
|
|
575
575
|
: `namespaces ${listOrNone(namespaces)} — pick one`;
|
|
576
|
-
|
|
576
|
+
// Same rule as the discovery envelope: an external namespace's dispatch
|
|
577
|
+
// guard refuses a call without `mcpDetails.description`, so a route that
|
|
578
|
+
// omits it just moves the dead end one step later.
|
|
579
|
+
const details =
|
|
580
|
+
this.namespaces.get(first)?.external === true
|
|
581
|
+
? `,"mcpDetails":{"description":"<one sentence saying why you are calling this>"}`
|
|
582
|
+
: "";
|
|
583
|
+
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 … }${details}}. Call ${GET_DYNAMIC_TOOLS_NAME}({"namespace":"${first}","toolName":"${name}"}) first if you do not have its schema.`;
|
|
577
584
|
}
|
|
578
585
|
|
|
579
586
|
private async prepareRegistered(
|
|
@@ -990,6 +997,20 @@ export class ToolRegistry extends Service implements ToolExecution {
|
|
|
990
997
|
namespace: namespaceName,
|
|
991
998
|
toolName: registered.definition.name,
|
|
992
999
|
arguments: "<an object matching inputSchema>",
|
|
1000
|
+
// The envelope handed back has to be the envelope accepted. An
|
|
1001
|
+
// external namespace's dispatch guard refuses a call without
|
|
1002
|
+
// `mcpDetails.description`, and a model that copied this
|
|
1003
|
+
// envelope verbatim was denied for a field the envelope never
|
|
1004
|
+
// mentioned.
|
|
1005
|
+
...(this.namespaces.get(registered.definition.namespace ?? "")
|
|
1006
|
+
?.external === true
|
|
1007
|
+
? {
|
|
1008
|
+
mcpDetails: {
|
|
1009
|
+
description:
|
|
1010
|
+
"<one sentence saying why you are calling this>",
|
|
1011
|
+
},
|
|
1012
|
+
}
|
|
1013
|
+
: {}),
|
|
993
1014
|
},
|
|
994
1015
|
},
|
|
995
1016
|
}),
|