diff --git a/AGENTS.md b/AGENTS.md index 4f94032..1a6a8c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -154,7 +154,7 @@ Envs are set in Settings, under Environment (super admin only), not in `.env`. T - **Per-bot models:** in a workspace, its `workspace_harness_settings` row wins (`source: "workspace"`). Otherwise a worker bot inherits the defaults per harness unless it has a `bot_harness_settings` row (same fields as `harness_settings`, unique per bot and harness). `grabHarnessSetting({ harness, bot_id })` returns the bot's row (`source: "bot"`) or the default; the coordinator always gets the default. Every entry point takes `bot_id` and passes it on (text loop -> media reading/transcription), so a bot's run uses its own models throughout. Saved by `saveBotHarnessSetting` (`inherit` deletes the row); the page data is `grabBotHarnessOverview`. A bot's chat header has a ghost model icon (`(partials)/header-icon-button.tsx`; the text model and provider show under the bot's name) that opens a modal with every harness (`(partials)/bot-models-modal.tsx`); the modal and `/admin/bots/[bot_id]/models` both use `(partials)/bot-harness-form.tsx`. On the coordinator the modal edits the defaults themselves (`api/admin/harness/save-settings`, the same settings as Settings, under Models, without the providers): super admins only, the icon is disabled for everyone else. Every one of these wraps `settings/models/(partials)/harness-settings-form.tsx`: per harness a provider select first (`harness-provider-select.tsx`: any provider, one provider, or on a bot "Same as the default"), then the model select narrowed to it. Removing a provider (`deleteHarnessProvider`) puts default and bot settings that picked it or its models back on any provider (`save` prop; "Same as the default" shows when the overview item has `default`). - **Tools:** `HarnessTool` (built per run with its bot bound): own tools validate with a zod `schema`; tools described elsewhere pass a JSON schema as `parameters` and validate themselves. `run-tool-call.ts` reports bad calls and failures back to the model as `Error: ...`, and caps every tool call at `AppData.HarnessToolTimeoutMs` (aborted, its result handed back as an error the model can work around). Bot tools in `src/harness/tools/bot/`: `update_memory`, `update_instructions` (`grabBotTools`, every run), `start_task` and, for the coordinator with workers, `delegate_task` (`grabBotChatTools`, chat replies only; the job's `requested_by_bot_id` is the coordinator, and `relayJobToRequester` posts the worker's result or failure in its conversation, pushed as `relay` by `pushBotNotice`), `ask_user_to_take_over` (see Take-over), plus for the coordinator `create_workspace` and `update_workspace` (a new workspace with its shared context, or changing one; both also in its tasks, so a task that gathers material can write it into the workspace), `create_bot` (add a worker with its name, role and instructions, and join it to a workspace straight away) and, in its own chat, `add_bot_to_workspace` (put an existing worker in one of the user's workspaces) and `remove_bot_from_workspace` (take it out, keeping its conversation there), `assign_computer` (grant or revoke a worker's direct access, `add/remove-bot-to/from-computer`; access through a workspace stays and the result says so), `update_bot` (change a worker's name, role or core instructions, `src/functions/backend/db/bots/update-bot.ts`), `find_bot` and `find_workspace` (look the crew, or the live workspaces, up by id, name, role/description or instructions/context), `delete_bot` (a worker with its conversations, tasks, schedules and workspace places; the coordinator can't go) and `delete_workspace` (its conversations, tasks, schedules and links; its bots and computers stay). `search_computers` (`src/harness/tools/computer/search-computers-tool.ts`, chat replies and tasks) runs one read-only command on all of a bot's computers at once over the sandbox REST API (`run-computer-command.ts`), each output under its `[computer:ID]`, secrets redacted, capped by `AppData.ComputerSearch*`. - **MCP:** `src/harness/mcp/`: `connectMcpServer` (Streamable HTTP, SSE fallback), `grabMcpTools` (each server tool becomes a `HarnessTool` with its JSON schema), `mcpResultToText` (text as is, images read by the vision harness, `isError` thrown). Computers are reached this way: `openComputerTools` (`src/harness/tools/computer/`) connects to the computer's AIO sandbox at `grabComputerMcpUrl` (the `MAIN_COMPUTER_URL` app setting for the main computer, for a dev server outside docker, e.g. `http://localhost:8170`, else `http://:/mcp`, or its IP address when the hostname can't be reached), leaves out `ComputerMcpExcludedTools`, and sets the computer's envs (see Envs). -- **MCPs and custom tools (extensions):** installs add their own MCP servers (`mcp_servers`, Settings, under MCPs) and tools (`custom_tools`, Settings, under Tools). A bot gets the ones given to it (`bot_mcp_servers`, `bot_custom_tools`) and, in a workspace, that workspace's (`workspace_mcp_servers`, `workspace_custom_tools`); `botExtensionsSql` (`src/utils/bot-extensions-sql.ts`, tested) resolves a run's, `grabBotExtensions` reads them. `all_bots` ("every bot by default") only decides what bots are given: turning it on gives it to every bot (`attachToAllBots`, the coordinator included) and every new bot gets it (`attachDefaultExtensions`, from `createBot` and `ensureCoordinatorBot`); turning it off takes it from no one, and a bot that drops one keeps it dropped (`src/scripts/attach-every-bot-extensions.ts`, safe to rerun, moved installs from the old "reaches every bot" meaning). Super admins define them. Super admins and admins (the super admin alone for the coordinator, `assertBotExtensionManager`) give and take them per bot on the bot's settings page and under its chat's composer (`composer-tools.tsx`: chips for everything it can use there, a workspace's not removable, and an Add modal; before them `composer-computer-button.tsx` opens the bot's current computer in the viewer), and per workspace on the workspace's settings page; the settings pages share `settings/(partials)/extensions-group.tsx`, all through `useExtensions` (`src/hooks/`), `grabBotExtensionsOverview` / `grabWorkspaceExtensionsOverview` (`bot_extensions` / `workspace_extensions` page props, both on `grabExtensionsOverview`) and `api/admin/bots/` and `api/admin/workspaces/` `add-extension`, `remove-extension` (`setBotExtension`, `setWorkspaceExtension`); the MCPs and Tools pages' Access modal sets bots and workspaces in bulk (`setExtensionAccess`, an admin never touches the coordinator's or `all_bots`). Helpers in `src/functions/backend/db/extensions/`, API in `api/admin/mcps/` and `api/admin/tools/`, zod in `src/schemas/extension-schema.ts`. `openBotExtensions` (`src/harness/tools/extensions/`) turns them into harness tools for chat replies and tasks (built-ins win a name clash; `close()` in the runners' `finally`; `note` tells the bot about a server it can't use). Templates (`fillTemplate`, `src/utils/fill-template.ts`): `{{args.x}}` and `{{env.KEY}}`, envs from `grabRunEnvs` (workspace beats general, app settings never, no need to be set on computers); secret values are redacted from every result. +- **MCPs and custom tools (extensions):** installs add their own MCP servers (`mcp_servers`, Settings, under MCPs) and tools (`custom_tools`, Settings, under Tools). A bot gets the ones given to it (`bot_mcp_servers`, `bot_custom_tools`) and, in a workspace, that workspace's (`workspace_mcp_servers`, `workspace_custom_tools`); `botExtensionsSql` (`src/utils/bot-extensions-sql.ts`, tested) resolves a run's, `grabBotExtensions` reads them. `all_bots` ("every bot by default") only decides what bots are given: turning it on gives it to every bot (`attachToAllBots`, the coordinator included) and every new bot gets it (`attachDefaultExtensions`, from `createBot` and `ensureCoordinatorBot`); turning it off takes it from no one, and a bot that drops one keeps it dropped (`src/scripts/attach-every-bot-extensions.ts`, safe to rerun, moved installs from the old "reaches every bot" meaning). Super admins define them. Super admins and admins (the super admin alone for the coordinator, `assertBotExtensionManager`) give and take them per bot on the bot's settings page and under its chat's composer (`composer-tools.tsx`: one "N tools" button (`composer-tools-modal.tsx`) counting everything it can use there, which opens a modal listing them (a workspace's not removable) with the add picker, so ten of them don't fill the composer; beside it `composer-computer-button.tsx` opens the bot's current computer in the viewer), and per workspace on the workspace's settings page; the settings pages share `settings/(partials)/extensions-group.tsx`, all through `useExtensions` (`src/hooks/`), `grabBotExtensionsOverview` / `grabWorkspaceExtensionsOverview` (`bot_extensions` / `workspace_extensions` page props, both on `grabExtensionsOverview`) and `api/admin/bots/` and `api/admin/workspaces/` `add-extension`, `remove-extension` (`setBotExtension`, `setWorkspaceExtension`); the MCPs and Tools pages' Access modal sets bots and workspaces in bulk (`setExtensionAccess`, an admin never touches the coordinator's or `all_bots`). Helpers in `src/functions/backend/db/extensions/`, API in `api/admin/mcps/` and `api/admin/tools/`, zod in `src/schemas/extension-schema.ts`. `openBotExtensions` (`src/harness/tools/extensions/`) turns them into harness tools for chat replies and tasks (built-ins win a name clash; `close()` in the runners' `finally`; `note` tells the bot about a server it can't use). Templates (`fillTemplate`, `src/utils/fill-template.ts`): `{{args.x}}` and `{{env.KEY}}`, envs from `grabRunEnvs` (workspace beats general, app settings never, no need to be set on computers); secret values are redacted from every result. - **Custom tools** (`CustomToolKinds` in `src/dict/custom-tool-dict.ts`): a name (`checkCustomToolName`: snake_case, never a built-in's, `ReservedToolNames`), a description, a JSON-schema `parameters` and a `config` per kind, built by `buildCustomTool` (`src/harness/tools/custom/`): `http` (`runHttpTool`: the app fetches, arguments URL-encoded in the URL, no body template sends the arguments as JSON), `script` (`runScriptTool`: bash/python/javascript on the run's computer, the task's or in chat the bot's current one, over the sandbox REST API in a subshell, computer envs set, arguments as JSON in `COBBLE_ARGS` and on stdin; left out without a computer), `prompt` (`runPromptTool`: one `generateText` turn with the bot's models). Super admins try one from the page (`testCustomTool`, the main computer for scripts): a saved one from its row (Try), or the form as it is, saved or not (`draft`, nothing recorded), from the Test panel in its modal (`custom-tool-test-panel.tsx`). Every run (a bot's call or a try; not bad arguments or a cancel) is saved as the tool's `status`, `last_error` and `last_checked_at` (`recordCustomToolRun`); editing resets it to `untested`. - **MCP servers** (`McpTransports` in `src/dict/mcp-dict.ts`): `remote` (URL plus templated headers, `connectMcpServer`) or `stdio` (a command run with the host's shell, `hostShell`, in `DATA_DIR/mcp/` (`grabMcpDir`), after its `setup_command`, which `runMcpSetup` reruns whenever it changes (marker file) or on Reinstall; its env is only its own `env` map plus the shell basics, `grabMcpStdioEnv`, never the app's). Their tools are listed into `tools_cache` (`syncMcpServer`: on save, Refresh, and hourly for remote ones by the `sync-mcp-servers` cron) and named `__` (`mcpToolName`); the modal's Test connection checks the form as it is, saved or not, outside the pool (`testMcpDraft`, `testMcpServer`, `api/admin/mcps/test-mcp`, a new stdio server's setup runs in `mcp/0`); a run that can't connect, or connects again after failing, updates its `status` too (`recordMcpServerConnection`); runs build tools from the cache (`mcpHarnessTool`) and connect on the first call (`openMcpServer`). stdio servers live in a per-process pool (`src/harness/mcp/mcp-stdio-pool.ts`), stopped after `AppData.McpStdioIdleMs` idle, on save or delete. - **Check before and after acting:** every reply and task gets `ActionCheckNote` (`src/data/harness-prompts.ts`): look for an existing record before creating one (update it instead), and verify every action took effect before saying it's done. The tools back it: `update_memory` and `update_instructions` read the save back (`checkSavedText`, `grabBotSavedText`) and fail if it didn't take; `create_bot` refuses a name already on the crew, `create_schedule` a twin of one of the bot's schedules (`findDuplicateCronJob`: same place and same title, or same timing and brief), and `start_task`/`delegate_task` a task with the same title still open in that session (`grabOpenBotTask`); `create_workspace` already refuses a name in use. diff --git a/src/pages/admin/bots/[bot_id]/(partials)/composer-add-tool-modal.tsx b/src/pages/admin/bots/[bot_id]/(partials)/composer-add-tool-modal.tsx deleted file mode 100644 index a8adeb2..0000000 --- a/src/pages/admin/bots/[bot_id]/(partials)/composer-add-tool-modal.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useState } from "react"; -import Modal from "@moduletrace/twui/elements/Modal"; -import Button from "@moduletrace/twui/layout/Button"; -import H2 from "@moduletrace/twui/layout/H2"; -import Span from "@moduletrace/twui/layout/Span"; -import Stack from "@moduletrace/twui/layout/Stack"; -import { Plus } from "lucide-react"; -import ExtensionAddSelect from "@/src/components/general/extension-add-select"; -import type { BotExtensionsPayload } from "@/src/types"; - -type Props = { - bot_name: string; - addable: BotExtensionsPayload["addable"]; - loading?: boolean; - onAdd: (params: { kind: "mcp" | "tool"; id: number }) => Promise; -}; - -/** - * The "Add" chip under the composer: picks an MCP server or tool to give - * the bot, everywhere it works. - */ -export default function ComposerAddToolModal({ - bot_name, - addable, - loading, - onAdd, -}: Props) { - const [open, setOpen] = useState(false); - - if (!addable.length) return null; - - return ( - } - className="gap-1! px-2! py-0.5! text-[12px] leading-4" - > - Add - - } - targetWrapperProps={{ className: "shrink-0" }} - open={open} - setOpen={setOpen} - className="max-w-lg bg-paper p-6" - > - - -

- Add a tool -

- - {`${bot_name} can use it everywhere it works, for everyone it works for.`} - -
- { - if (await onAdd(picked)) setOpen(false); - }} - /> -
-
- ); -} diff --git a/src/pages/admin/bots/[bot_id]/(partials)/composer-tool-chip.tsx b/src/pages/admin/bots/[bot_id]/(partials)/composer-tool-chip.tsx deleted file mode 100644 index 91cb3f4..0000000 --- a/src/pages/admin/bots/[bot_id]/(partials)/composer-tool-chip.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import Button from "@moduletrace/twui/layout/Button"; -import Row from "@moduletrace/twui/layout/Row"; -import Span from "@moduletrace/twui/layout/Span"; -import { CircleAlert, X } from "lucide-react"; -import extensionIcon from "@/src/components/general/extension-icon"; -import type { BotExtension } from "@/src/types"; - -type Props = { - item: BotExtension; - busy?: boolean; - /** - * Takes it from the bot; not given for a workspace's own or when the - * user can't. - */ - onRemove?: () => void; -}; - -/** - * One MCP server or tool the bot can use here, under the composer. One - * that isn't working shows an alert icon, one that's off is struck out. - */ -export default function ComposerToolChip({ item, busy, onRemove }: Props) { - const Icon = item.state == "error" ? CircleAlert : extensionIcon({ kind: item.kind }); - const notes = [ - item.detail, - item.source == "workspace" ? "From this workspace" : "", - !item.enabled ? "Off for every bot" : item.state == "error" ? "Not working" : "", - ].filter(Boolean); - - return ( - - - {item.name} - {onRemove ? ( - - ) : null} - - ); -} diff --git a/src/pages/admin/bots/[bot_id]/(partials)/composer-tool-row.tsx b/src/pages/admin/bots/[bot_id]/(partials)/composer-tool-row.tsx new file mode 100644 index 0000000..7b05caa --- /dev/null +++ b/src/pages/admin/bots/[bot_id]/(partials)/composer-tool-row.tsx @@ -0,0 +1,74 @@ +import Button from "@moduletrace/twui/layout/Button"; +import Row from "@moduletrace/twui/layout/Row"; +import Span from "@moduletrace/twui/layout/Span"; +import Stack from "@moduletrace/twui/layout/Stack"; +import { X } from "lucide-react"; +import extensionIcon from "@/src/components/general/extension-icon"; +import type { BotExtension } from "@/src/types"; + +type Props = { + item: BotExtension; + busy?: boolean; + /** + * Takes it from the bot; not given for a workspace's own or when the + * user can't. + */ + onRemove?: () => void; +}; + +/** + * One MCP server or tool the bot can use here, in the composer's tools + * modal. One that isn't working says so, one that's off is struck out. + */ +export default function ComposerToolRow({ item, busy, onRemove }: Props) { + const Icon = extensionIcon({ kind: item.kind }); + const notes = [ + item.detail, + item.source == "workspace" ? "From this workspace" : "", + !item.enabled + ? "Off for every bot" + : item.state == "error" + ? "Not working" + : item.state == "untested" + ? "Not checked yet" + : "", + ].filter(Boolean); + + return ( + + + + + {item.name} + + {notes.length ? ( + + {notes.join(" · ")} + + ) : null} + + {onRemove ? ( + + ) : null} + + ); +} diff --git a/src/pages/admin/bots/[bot_id]/(partials)/composer-tools-modal.tsx b/src/pages/admin/bots/[bot_id]/(partials)/composer-tools-modal.tsx new file mode 100644 index 0000000..5d0fc46 --- /dev/null +++ b/src/pages/admin/bots/[bot_id]/(partials)/composer-tools-modal.tsx @@ -0,0 +1,123 @@ +import { useState } from "react"; +import Modal from "@moduletrace/twui/elements/Modal"; +import Button from "@moduletrace/twui/layout/Button"; +import H2 from "@moduletrace/twui/layout/H2"; +import Span from "@moduletrace/twui/layout/Span"; +import Stack from "@moduletrace/twui/layout/Stack"; +import { CircleAlert, Wrench } from "lucide-react"; +import ExtensionAddSelect from "@/src/components/general/extension-add-select"; +import type { BotExtension, BotExtensionsPayload } from "@/src/types"; +import ComposerToolRow from "./composer-tool-row"; + +type Props = { + bot_name: string; + attached: BotExtension[]; + addable: BotExtensionsPayload["addable"]; + can_manage: boolean; + /** + * The extension being changed, as `kind:id`. + */ + busy?: string; + onAdd: (params: { kind: "mcp" | "tool"; id: number }) => Promise; + onRemove: (params: { + kind: "mcp" | "tool"; + id: number; + name: string; + }) => Promise; +}; + +/** + * The composer's tools button: how many MCP servers and tools the bot can + * use here, opening them in a modal rather than filling the composer with + * chips. Super admins and admins remove any it was given (even one every + * bot started with) and add more; a workspace's own stay. + */ +export default function ComposerToolsModal({ + bot_name, + attached, + addable, + can_manage, + busy, + onAdd, + onRemove, +}: Props) { + const [open, setOpen] = useState(false); + const count = attached.length; + const has_problem = attached.some( + (item) => !item.enabled || item.state == "error", + ); + const TriggerIcon = has_problem ? CircleAlert : Wrench; + const label = count ? `${count} ${count == 1 ? "tool" : "tools"}` : "Add tools"; + + return ( + } + className="shrink-0 gap-1! px-2! py-0.5! text-[12px] leading-4 text-ink-secondary" + > + {label} + + } + targetWrapperProps={{ className: "shrink-0" }} + open={open} + setOpen={setOpen} + className="max-w-lg bg-paper p-6" + > + +

+ {`Tools ${bot_name} can use here`} +

+ + {count + ? "MCP servers and tools it was given, plus this workspace's. Remove one to take it away, or add another below." + : "It has none yet. Give it an MCP server or tool below, or make one under Settings."} + +
+ + {count ? ( + + {attached.map((item) => ( + + onRemove({ + kind: item.kind, + id: item.id, + name: item.name, + }) + : undefined + } + /> + ))} + + ) : null} + + {can_manage && addable.length ? ( + + + {`Give ${bot_name} another one. It can use it everywhere it works, for everyone it works for.`} + + + + ) : null} +
+ ); +} diff --git a/src/pages/admin/bots/[bot_id]/(partials)/composer-tools.tsx b/src/pages/admin/bots/[bot_id]/(partials)/composer-tools.tsx index 307c026..26cbe2c 100644 --- a/src/pages/admin/bots/[bot_id]/(partials)/composer-tools.tsx +++ b/src/pages/admin/bots/[bot_id]/(partials)/composer-tools.tsx @@ -4,9 +4,8 @@ import Span from "@moduletrace/twui/layout/Span"; import useExtensions from "@/src/hooks/use-extensions"; import type { BotExtensionsPayload, BotWithLastMessage } from "@/src/types"; import { BotComputersContext } from "../(data)/bot-computers-context"; -import ComposerAddToolModal from "./composer-add-tool-modal"; import ComposerComputerButton from "./composer-computer-button"; -import ComposerToolChip from "./composer-tool-chip"; +import ComposerToolsModal from "./composer-tools-modal"; import { Dot } from "lucide-react"; type Props = { @@ -16,10 +15,9 @@ type Props = { }; /** - * The MCP servers and tools the bot can use here, under the composer. - * Super admins and admins remove any it was given (even one every bot - * started with) and add more; a workspace's own stay. The computer the - * bot is on sits before them, opening it in the viewer. + * Under the composer: one button counting the MCP servers and tools the + * bot can use here (they open in a modal, so ten of them don't fill the + * composer), then the computer it's on, opening it in the viewer. */ export default function ComposerTools({ bot, workspace_id, initial }: Props) { const extensions = useExtensions({ @@ -40,34 +38,14 @@ export default function ComposerTools({ bot, workspace_id, initial }: Props) { return ( {has_tools ? ( - - Tools - - ) : null} - {extensions.attached.map((item) => ( - - extensions.remove({ - kind: item.kind, - id: item.id, - name: item.name, - }) - : undefined - } - /> - ))} - - {extensions.can_manage ? ( - ) : null}