diff --git a/agent-service/src/agent/texera-agent.spec.ts b/agent-service/src/agent/texera-agent.spec.ts index d173fe7ae0b..6b8f95509e9 100644 --- a/agent-service/src/agent/texera-agent.spec.ts +++ b/agent-service/src/agent/texera-agent.spec.ts @@ -535,6 +535,58 @@ describe("sendMessage", () => { ]); }); + test("leaves disabled tools out of the model call but still lists them", async () => { + const model = textModel("ok"); + const agent = makeAgentWith(model); + agent.updateSettings({ disabledTools: new Set(["modifyOperator"]) }); + await agent.sendMessage("go"); + const call = (model as any).doGenerateCalls[0]; + expect(call.tools.map((t: any) => t.name)).toEqual(["deleteOperator", "addOperator"]); + expect(agent.getSystemInfo().tools.map(t => [t.name, t.enabled])).toEqual([ + ["deleteOperator", true], + ["addOperator", true], + ["modifyOperator", false], + ]); + }); + + test("a disabled tool does not run when the model calls it anyway", async () => { + let n = 0; + const model = new MockLanguageModelV4({ + doGenerate: async () => { + n++; + if (n === 1) + return { + content: [ + { + type: "tool-call", + toolCallId: "c1", + toolName: "deleteOperator", + input: JSON.stringify({ operatorId: "op1" }), + }, + ], + finishReason: finish("tool-calls"), + usage: usage(1, 1), + warnings: [], + } as any; + return { + content: [{ type: "text", text: "x" }], + finishReason: finish("stop"), + usage: usage(1, 1), + warnings: [], + } as any; + }, + }); + const agent = makeAgentWith(model); + agent.updateSettings({ disabledTools: new Set(["deleteOperator"]) }); + agent.getWorkflowState().addOperator(srcOp("op1") as any); + fetchSpy.mockImplementation((async (u: any) => { + urls.push(String(u)); + return { ok: true, json: async () => ({ operatorOutputSchemas: {}, operatorErrors: {} }) } as any; + }) as any); + await agent.sendMessage("del"); + expect(agent.getWorkflowState().getOperator("op1")).toBeDefined(); + }); + test("caps the loop at maxSteps", async () => { // The cap is the only thing standing between a looping model and an unbounded run. let n = 0; diff --git a/agent-service/src/agent/texera-agent.ts b/agent-service/src/agent/texera-agent.ts index b98c812b0b0..cc6cec1e916 100644 --- a/agent-service/src/agent/texera-agent.ts +++ b/agent-service/src/agent/texera-agent.ts @@ -233,6 +233,11 @@ export class TexeraAgent { return tools; } + /** The tools the model may call: every tool except those in `settings.disabledTools`. */ + private getEnabledTools(): Record { + return Object.fromEntries(Object.entries(this.tools).filter(([name]) => !this.settings.disabledTools.has(name))); + } + getState(): AgentStateEnum { return this.state; } @@ -546,7 +551,7 @@ export class TexeraAgent { model: this.model, system: this.systemPrompt, messages: currentUserMessage, - tools: this.tools, + tools: this.getEnabledTools(), temperature: 0.2, stopWhen: stepCountIs(this.settings.maxSteps), prepareStep: async ({ stepNumber, messages: currentMessages }) => {