diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 53686a6ca..7ba6d8ed5 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -1391,6 +1391,7 @@ export class CopilotClient { overridesBuiltInTool: tool.overridesBuiltInTool, skipPermission: tool.skipPermission, defer: tool.defer, + _meta: tool._meta, })), canvases: config.canvases?.map((canvas) => canvas.declaration), requestCanvasRenderer: config.requestCanvasRenderer, @@ -1591,6 +1592,7 @@ export class CopilotClient { overridesBuiltInTool: tool.overridesBuiltInTool, skipPermission: tool.skipPermission, defer: tool.defer, + _meta: tool._meta, })), canvases: config.canvases?.map((canvas) => canvas.declaration), requestCanvasRenderer: config.requestCanvasRenderer, diff --git a/nodejs/src/types.ts b/nodejs/src/types.ts index e354bd821..9777261a9 100644 --- a/nodejs/src/types.ts +++ b/nodejs/src/types.ts @@ -560,6 +560,15 @@ export interface Tool { * Optional; defaults to `"auto"`. */ defer?: "auto" | "never"; + /** + * Opaque, host-defined metadata associated with the tool definition. + * + * Keys are namespaced and are not part of the stable public API. The SDK + * does not interpret these values; it forwards them verbatim to the runtime, + * which may recognize specific namespaced keys to inform host-specific + * behavior. Unknown keys are preserved and round-tripped untouched. + */ + _meta?: Record; } /** @@ -575,6 +584,7 @@ export function defineTool( overridesBuiltInTool?: boolean; skipPermission?: boolean; defer?: "auto" | "never"; + _meta?: Record; } ): Tool { return { name, ...config }; diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 96d7da30c..fc49a9823 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -188,6 +188,69 @@ describe("CopilotClient", () => { expect(resumePayload.contextTier).toBe("default"); }); + it("forwards tool _meta verbatim in session.create and session.resume", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + if (method === "session.resume") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + const meta = { "github.com/copilot:safeForTelemetry": { name: true, inputsNames: false } }; + const tool = { + name: "my_tool", + description: "a tool", + parameters: { type: "object", properties: {} }, + _meta: meta, + }; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + tools: [tool], + }); + await client.resumeSession(session.sessionId, { + onPermissionRequest: approveAll, + tools: [tool], + }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + const resumePayload = spy.mock.calls.find( + ([method]) => method === "session.resume" + )![1] as any; + expect(createPayload.tools[0]._meta).toEqual(meta); + expect(resumePayload.tools[0]._meta).toEqual(meta); + }); + + it("omits tool _meta from session.create when unset", async () => { + const client = new CopilotClient(); + await client.start(); + onTestFinished(() => client.forceStop()); + + const spy = vi + .spyOn((client as any).connection!, "sendRequest") + .mockImplementation(async (method: string, params: any) => { + if (method === "session.create") return { sessionId: params.sessionId }; + throw new Error(`Unexpected method: ${method}`); + }); + + await client.createSession({ + onPermissionRequest: approveAll, + tools: [{ name: "my_tool", description: "a tool" }], + }); + + const createPayload = spy.mock.calls.find( + ([method]) => method === "session.create" + )![1] as any; + expect(createPayload.tools[0]._meta).toBeUndefined(); + }); + it("forwards expAssignments in session.create and session.resume", async () => { const client = new CopilotClient(); await client.start();