Skip to content

Commit cef59ff

Browse files
Merge branch 'master' into fix/tts-audio-content-type
2 parents 54aef30 + eec3e73 commit cef59ff

12 files changed

Lines changed: 320 additions & 124 deletions

File tree

frontend/src/components/WorkspaceChat/ChatContainer/ChatTooltips/index.jsx

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { Tooltip } from "react-tooltip";
22
import { createPortal } from "react-dom";
3-
import { useTranslation } from "react-i18next";
43

54
/**
65
* Set the tooltips for the chat container in bulk.
@@ -17,8 +16,6 @@ import { useTranslation } from "react-i18next";
1716
* @returns
1817
*/
1918
export function ChatTooltips() {
20-
const { t } = useTranslation();
21-
2219
return (
2320
<>
2421
<Tooltip
@@ -105,13 +102,6 @@ export function ChatTooltips() {
105102
delayShow={300}
106103
className="tooltip !text-xs"
107104
/>
108-
<Tooltip
109-
id="agent-skill-disabled-tooltip"
110-
place="top"
111-
delayShow={300}
112-
className="tooltip !text-xs z-99"
113-
content={t("chat_window.agent_skills_disabled_in_session")}
114-
/>
115105
<DocumentLevelTooltip />
116106
</>
117107
);

frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/SkillRow/index.jsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ export default function SkillRow({
2626
type="button"
2727
className={classNames}
2828
onClick={() => !disabled && onToggle()}
29-
data-tooltip-id={disabled ? "agent-skill-disabled-tooltip" : undefined}
3029
>
3130
<span className="text-xs text-white light:text-slate-900">{name}</span>
3231
<div className="pointer-events-none" aria-hidden="true">

frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/index.jsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ export default function AgentSkillsTab({
170170
highlightedIndex,
171171
onSelect: (item) => {
172172
if (item.type === "header") return item.onToggle();
173-
if (!agentSessionActive) item.onToggle();
173+
item.onToggle();
174174
},
175175
registerItemCount,
176176
});
@@ -209,7 +209,6 @@ export default function AgentSkillsTab({
209209
enabled={item.enabled}
210210
onToggle={item.onToggle}
211211
highlighted={highlightedIndex === flatIndexMap[item.id]}
212-
disabled={agentSessionActive}
213212
/>
214213
{item.hasSubSkills && item.subSkills && item.enabled && (
215214
<SkillSection
@@ -232,7 +231,7 @@ export default function AgentSkillsTab({
232231
highlighted={
233232
highlightedIndex === flatIndexMap[subItem.id]
234233
}
235-
disabled={agentSessionActive || !subItem.parentEnabled}
234+
disabled={!subItem.parentEnabled}
236235
/>
237236
))}
238237
</SkillSection>

frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/useAgentSkillsState.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import AgentFlows from "@/models/agentFlows";
66
import MCPServers from "@/models/mcpServers";
77
import { getSubSkillPreferenceKeys } from "./skillRegistry";
88
import useSubSkillPreferences from "./useSubSkillPreferences";
9+
import { toggleAgentSessionTool } from "@/utils/chat/agent";
910

1011
/**
1112
* Core hook for managing all agent skill state.
@@ -91,6 +92,7 @@ export default function useAgentSkillsState(defaultSkills) {
9192
async (key) => {
9293
const toggleItem = (arr, item) =>
9394
arr.includes(item) ? arr.filter((s) => s !== item) : [...arr, item];
95+
const newEnabled = !isSkillEnabled(key);
9496

9597
if (key in defaultSkills) {
9698
const updated = toggleItem(disabledDefaults, key);
@@ -99,6 +101,7 @@ export default function useAgentSkillsState(defaultSkills) {
99101
disabled_agent_skills: updated.join(","),
100102
default_agent_skills: enabledConfigurable.join(","),
101103
});
104+
toggleAgentSessionTool(key, newEnabled);
102105
return;
103106
}
104107

@@ -108,8 +111,9 @@ export default function useAgentSkillsState(defaultSkills) {
108111
disabled_agent_skills: disabledDefaults.join(","),
109112
default_agent_skills: updated.join(","),
110113
});
114+
toggleAgentSessionTool(key, newEnabled);
111115
},
112-
[defaultSkills, disabledDefaults, enabledConfigurable]
116+
[defaultSkills, disabledDefaults, enabledConfigurable, isSkillEnabled]
113117
);
114118

115119
const toggleImportedSkill = useCallback(async (skill) => {
@@ -120,6 +124,7 @@ export default function useAgentSkillsState(defaultSkills) {
120124
)
121125
);
122126
await AgentPlugins.toggleFeature(skill.hubId, newActive);
127+
toggleAgentSessionTool(skill.hubId, newActive);
123128
}, []);
124129

125130
const toggleFlow = useCallback(async (flow) => {
@@ -128,6 +133,7 @@ export default function useAgentSkillsState(defaultSkills) {
128133
prev.map((f) => (f.uuid === flow.uuid ? { ...f, active: newActive } : f))
129134
);
130135
await AgentFlows.toggleFlow(flow.uuid, newActive);
136+
toggleAgentSessionTool(`@@flow_${flow.uuid}`, newActive);
131137
}, []);
132138

133139
const toggleMcpTool = useCallback(
@@ -154,6 +160,11 @@ export default function useAgentSkillsState(defaultSkills) {
154160
});
155161
});
156162
await MCPServers.toggleTool(serverName, toolName, newEnabled);
163+
toggleAgentSessionTool(
164+
`${serverName}-${toolName}`,
165+
newEnabled,
166+
serverName
167+
);
157168
},
158169
[]
159170
);

frontend/src/components/WorkspaceChat/ChatContainer/PromptInput/ToolsMenu/Tabs/AgentSkills/useSubSkillPreferences.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useState, useCallback } from "react";
22
import Admin from "@/models/admin";
33
import { SUB_SKILL_REGISTRY, getPreferenceKeyForSkill } from "./skillRegistry";
4+
import { toggleAgentSessionTool } from "@/utils/chat/agent";
45

56
/**
67
* Hook to manage sub-skill preferences for all skills in the registry.
@@ -65,6 +66,7 @@ export default function useSubSkillPreferences() {
6566
await Admin.updateSystemPreferences({
6667
[prefKey]: updated.join(","),
6768
});
69+
toggleAgentSessionTool(subSkillName, !updated.includes(subSkillName));
6870
},
6971
[disabledSubSkills]
7072
);

frontend/src/components/WorkspaceChat/ChatContainer/index.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import handleSocketResponse, {
1616
AGENT_SESSION_END,
1717
AGENT_SESSION_START,
1818
setAgentSessionActive,
19+
setAgentSessionSocket,
1920
} from "@/utils/chat/agent";
2021
import DnDFileUploaderWrapper from "./DnDWrapper";
2122
import SpeechRecognition, {
@@ -360,6 +361,7 @@ export default function ChatContainer({
360361

361362
window.addEventListener(ABORT_STREAM_EVENT, () => {
362363
setAgentSessionActive(false);
364+
setAgentSessionSocket(null);
363365
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
364366
socket?.close();
365367
});
@@ -379,6 +381,7 @@ export default function ChatContainer({
379381

380382
socket.addEventListener("close", (_event) => {
381383
setAgentSessionActive(false);
384+
setAgentSessionSocket(null);
382385
window.dispatchEvent(new CustomEvent(AGENT_SESSION_END));
383386
// When the close was triggered by /reset, skip the "Agent session
384387
// complete." status - the pending /reset flow will clear history.
@@ -406,6 +409,7 @@ export default function ChatContainer({
406409
});
407410
setWebsocket(socket);
408411
setAgentSessionActive(true);
412+
setAgentSessionSocket(socket);
409413
window.dispatchEvent(new CustomEvent(AGENT_SESSION_START));
410414
window.dispatchEvent(new CustomEvent(CLEAR_ATTACHMENTS_EVENT));
411415
} catch (e) {

frontend/src/utils/chat/agent.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,26 @@ export function getAgentSessionActive() {
408408
return _agentSessionActive;
409409
}
410410

411+
// Live agent-session websocket, used to toggle tools available to the agent mid-session.
412+
let _agentSessionSocket = null;
413+
export function setAgentSessionSocket(socket) {
414+
_agentSessionSocket = socket;
415+
}
416+
417+
/**
418+
* Toggle a tool/skill on or off for the active agent session over the websocket.
419+
* No-op when there is no open agent session.
420+
* @param {string} skill - Skill key, `@@flow_<uuid>`, MCP `<server>-<tool>`, hubId, or sub-skill name.
421+
* @param {boolean} enabled - Whether the tool should be enabled.
422+
* @param {string|null} [serverName] - MCP server name; required to enable an MCP tool mid-session.
423+
*/
424+
export function toggleAgentSessionTool(skill, enabled, serverName = null) {
425+
if (_agentSessionSocket?.readyState !== WebSocket.OPEN) return;
426+
_agentSessionSocket.send(
427+
JSON.stringify({ type: "agentToolToggle", skill, enabled, serverName })
428+
);
429+
}
430+
411431
export function useIsAgentSessionActive() {
412432
const [activeSession, setActiveSession] = useState(
413433
() => !!getAgentSessionActive()

server/endpoints/agentWebsocket.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ const { safeJsonParse } = require("../utils/http");
1010

1111
// Setup listener for incoming messages to relay to socket so it can be handled by agent plugin.
1212
function relayToSocket(message) {
13+
// Tool toggles can arrive while the agent is paused awaiting feedback/approval,
14+
// so handle them first. The handler ignores (returns false for) any other message.
15+
if (this.handleToolToggle?.(message)) return;
1316
if (this.handleFeedback) return this?.handleFeedback?.(message);
1417
if (this.handleToolApproval) return this?.handleToolApproval?.(message);
1518
if (this.handleClarificationResponse)

server/utils/agents/aibitat/index.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1456,6 +1456,17 @@ https://docs.anythingllm.com/agent/intelligent-tool-selection
14561456
this.functions.set(functionConfig.name, functionConfig);
14571457
return this;
14581458
}
1459+
1460+
/**
1461+
* Remove a registered function so the agent can no longer call it on its next
1462+
* turn. Used to disable a tool mid-session; restore it by re-running its plugin
1463+
* via aibitat.use().
1464+
* @param {string} functionName - The registered name of the function to remove.
1465+
*/
1466+
removeFunction(functionName) {
1467+
this.functions.delete(functionName);
1468+
return this;
1469+
}
14591470
}
14601471

14611472
module.exports = AIbitat;

server/utils/agents/aibitat/plugins/websocket.js

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,25 @@ const { Telemetry } = require("../../../../models/telemetry");
33
const { v4: uuidv4 } = require("uuid");
44
const { safeJsonParse } = require("../../../http");
55
const { skillIsAutoApproved } = require("../../../helpers/agents");
6+
const { ROLES } = require("../../../middleware/multiUserProtected");
7+
8+
/**
9+
* Toggling an agent's tools mid-session is an admin-only action, mirroring the
10+
* Agent Skills settings which only admins can manage. In multi-user mode the
11+
* requesting user must be an admin; single-user mode (no userId) is allowed.
12+
* @param {number|null} userId - User id from the agent invocation.
13+
* @returns {Promise<boolean>}
14+
*/
15+
async function userCanToggleTools(userId = null) {
16+
const { SystemSettings } = require("../../../../models/systemSettings");
17+
if (!(await SystemSettings.isMultiUserMode())) return true;
18+
if (!userId) return false;
19+
20+
const { User } = require("../../../../models/user");
21+
const user = await User.get({ id: Number(userId) });
22+
return user?.role === ROLES.admin;
23+
}
24+
625
const SOCKET_TIMEOUT_MS = 300 * 1_000; // 5 mins
726
const TOOL_APPROVAL_TIMEOUT_MS = 120 * 1_000; // 2 mins for tool approval
827
const CLARIFICATION_DEFAULT_TIMEOUT_MS = 120 * 1_000; // 2 mins for clarifying questions
@@ -87,6 +106,29 @@ const websocket = {
87106
},
88107
};
89108

109+
// Toggle a tool/skill on or off for the running agent mid-session. The
110+
// change applies on the agent's next turn. Returns true once handled so
111+
// the socket message router stops further dispatch. Toggling is an
112+
// admin-only action, so the message is claimed but only applied once
113+
// the requesting user is authorized.
114+
socket.handleToolToggle = (message) => {
115+
const data = safeJsonParse(message, {});
116+
if (data?.type !== "agentToolToggle") return false;
117+
118+
userCanToggleTools(userId).then((authorized) => {
119+
if (!authorized)
120+
return console.log(
121+
chalk.yellow("Ignoring agentToolToggle from a non-admin user.")
122+
);
123+
aibitat.toggleAgentTool?.({
124+
skill: data.skill,
125+
enabled: data.enabled,
126+
serverName: data.serverName || null,
127+
});
128+
});
129+
return true;
130+
};
131+
90132
/**
91133
* Request user approval before executing a tool/skill.
92134
* This sends a request to the frontend and blocks until the user responds.

0 commit comments

Comments
 (0)