diff --git a/AGENTS.md b/AGENTS.md index 946bf18..7ec5c83 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -134,11 +134,11 @@ Server push only. `pushBotNotice` (`server:bot-message`, `server:bot-updated`), ### Schedules (routines in the UI) -`cron_jobs` per user and bot (`schedule` is a `Bun.cron` expression in UTC, `max_runs`, `next_run_at`, `last_error`). Helpers in `src/functions/backend/db/cron-jobs/` (ownership-checked). `nextCronRun` validates and enforces `AppData.CronJobMinIntervalMs`; max `AppData.CronJobMaxPerBot` per bot. `fireCronJob` claims a run atomically, skips while the last job is open, queues a task. A missed run runs once. Runs act on their own (`ScheduledRunNote`); `report_run_failed` retries with doubling backoff. UI: `/admin/routines` and each conversation's Schedules modal (shared `RoutineForm`/`RoutineRow`/`RoutineEditModal`). +`cron_jobs` per user and bot (`schedule` is a `Bun.cron` expression in UTC, `max_runs`, `next_run_at`, `last_error`). Helpers in `src/functions/backend/db/cron-jobs/` (ownership-checked). `nextCronRun` validates and enforces `AppData.CronJobMinIntervalMs`; max `AppData.CronJobMaxPerBot` per bot. `fireCronJob` claims a run atomically, skips while the last job is open, queues a task. A missed run runs once. Runs act on their own (`ScheduledRunNote`); `report_run_failed` retries with doubling backoff. `handOverCronJob` (tool `hand_over_schedule`, UI Hand over) moves a routine to another bot: a workspace routine stays in its workspace (`handOverPlace`), its computer is kept only if the new bot reaches it, refused while a run is open. UI: `/admin/routines` and each conversation's Schedules modal (shared `RoutineForm`/`RoutineRow`/`RoutineEditModal`, `HandOverForm`). ### Tasks (the to-do tracker) -`tasks` (one collection per bot or per workspace; exactly one of `bot_id`/`workspace_id`, enforced in `taskScopeValues` since SQLite unique treats NULLs as distinct), `task_notes`, `task_media` (deliverable|reference). Not the same as `bot_tasks`/`bots_queue` (those record jobs). Visibility SQL in `src/utils/task-access/`. Helpers in `src/functions/backend/db/tasks/`; tools `list_tasks`/`create_task`/`update_task`; API `api/admin/tasks/` (`userAuth` -> `isReadOnlyUser` -> `assertUserReachesTask` -> `safeErrorMessage`). UI in the workspace dashboard and the header's Tasks modal (shared components in `src/components/general/`). **Not built yet:** task steps, `server:task-changed` pushes, a `/admin/tasks` page. +`tasks` (one collection per bot or per workspace; exactly one of `bot_id`/`workspace_id`, enforced in `taskScopeValues` since SQLite unique treats NULLs as distinct), `task_notes`, `task_media` (deliverable|reference). Not the same as `bot_tasks`/`bots_queue` (those record jobs). Visibility SQL in `src/utils/task-access/`. Helpers in `src/functions/backend/db/tasks/`; tools `list_tasks`/`create_task`/`update_task`; API `api/admin/tasks/` (`userAuth` -> `isReadOnlyUser` -> `assertUserReachesTask` -> `safeErrorMessage`). UI in the workspace dashboard and the header's Tasks modal (shared components in `src/components/general/`). **Hand-over:** `handOverTask` (bot: `delegate_task` with `task_id`; UI: Hand over, crew managers) moves an open task to the new bot's list (a workspace's stays on its list and is worked there), sets it doing and queues a job with `params.tracker_task_id`; `noteTrackerTask` notes the job's end on it and blocks it on failure. **Not built yet:** task steps, `server:task-changed` pushes, a `/admin/tasks` page. ### Envs @@ -174,7 +174,7 @@ Read-only browser of every volume (`src/utils/storage-volumes.ts`: bots, workspa - **Catalog:** `harness_models`, synced hourly. OpenRouter "free" means `:free` or `openrouter/free`, never zero prices. Auto picks: `rank-auto-models`. - **Calling:** `resolveHarnessCandidates` -> `withCandidates` with process-wide cooldowns; errors are `HarnessError`. Entry points under `text/`, `media/`, `audio/`, `image/`, `embeddings/`. `run-text-loop.ts` sticks to one model, caps steps (`grabHarnessMaxToolSteps`, `grabReplyMaxToolSteps`), trims old tool output to fit context. Models that can't take an image/audio part get it read by vision/transcription first. - **Per-bot models:** workspace row > bot row > default; the coordinator always uses the defaults. Every entry point takes `bot_id` and passes it on. Shared form `settings/models/(partials)/harness-settings-form.tsx`. -- **Tools:** `HarnessTool` built per run; own tools validate with zod. `run-tool-call.ts` reports errors back to the model and caps each call (`AppData.HarnessToolTimeoutMs`). Bot tools in `src/harness/tools/bot/` (memory, instructions, tasks, schedules, `start_task`, coordinator's crew management and `delegate_task` with results relayed back by `relayJobToRequester`). +- **Tools:** `HarnessTool` built per run; own tools validate with zod. `run-tool-call.ts` reports errors back to the model and caps each call (`AppData.HarnessToolTimeoutMs`). Bot tools in `src/harness/tools/bot/` (memory, instructions, tasks, schedules, `start_task`, coordinator's crew management, `delegate_task` and `hand_over_schedule`, with results relayed back by `relayJobToRequester`). - **Extensions:** MCP servers (`mcp_servers`, remote or stdio), custom tools (`custom_tools`: http, script, prompt) and skills (`skills`, runbooks loaded with `load_skill`). Given per bot / per workspace (`botExtensionsSql`); `all_bots` only decides what new bots get. Templates `{{args.x}}`/`{{env.KEY}}` via `fillTemplate`; secrets redacted from results. stdio servers run in a per-process pool. Skill body limits are app settings (`grabSkillBodyMaxChars`, `grabSkillBodyShownChars`). - **Bots never add or repoint extensions or set envs.** The coordinator may only change what can't alter where data is sent (name, description, on/off, access). New skills, servers, tools, or changes to a URL, headers, kind, config or a skill's runbook go through `propose_*` -> `extension_proposals` -> `[proposal:ID]` card, which the app describes from the config itself (never from what the bot says) and the super admin confirms (`checkProposalData` re-checks). Stdio servers are never proposed by bots (they run in the app container, which holds the docker socket). - **Check before and after acting** (`ActionCheckNote`): look for an existing record before creating one, verify every action took effect. Tools enforce it (read-back on memory saves, duplicate checks on bots, schedules, tasks, workspaces). diff --git a/src/components/general/hand-over-form.tsx b/src/components/general/hand-over-form.tsx new file mode 100644 index 0000000..520a3d7 --- /dev/null +++ b/src/components/general/hand-over-form.tsx @@ -0,0 +1,128 @@ +import { useEffect, useState } from "react"; +import Form from "@moduletrace/twui/form/Form"; +import Select from "@moduletrace/twui/form/Select"; +import Textarea from "@moduletrace/twui/form/Textarea"; +import Span from "@moduletrace/twui/layout/Span"; +import Stack from "@moduletrace/twui/layout/Stack"; +import postAdminApi from "@/src/layouts/bots/(functions)/post-admin-api"; +import AddFormActions from "@/src/layouts/bots/(partials)/add-form-actions"; +import type { HandOverTarget } from "@/src/types"; + +type Props = { + /** + * What is handed over, for the confirm ("the task "Draft the post""). + */ + what: string; + /** + * Under `/api/admin/`: who it can go to, and the hand-over itself. + * Both get `body`; the hand-over also the picked `bot_id`. + */ + targets_path: string; + submit_path: string; + body: Record; + /** + * Offer a note added to the brief (a task's hand-over). + */ + with_note?: boolean; + onHandedOver: (params: { saved: T; msg: string }) => void; + onCancel: () => void; +}; + +/** + * Picks the bot a task or routine goes to (the ones already where it runs, + * loaded when the form opens) and hands it over. Shared by the task panel + * and both routine lists. + */ +export default function HandOverForm({ + what, + targets_path, + submit_path, + body, + with_note, + onHandedOver, + onCancel, +}: Props) { + const [targets, setTargets] = useState(); + const [bot_id, setBotId] = useState(); + const [note, setNote] = useState(""); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + + useEffect(() => { + postAdminApi({ path: targets_path, body }).then((res) => { + if (!res.success) { + setError(res.msg || "Couldn't load the bots."); + setTargets([]); + return; + } + + const list = (res.payload || []) as HandOverTarget[]; + + setTargets(list); + setBotId(list[0]?.id); + }); + }, [targets_path]); + + const submit = async () => { + if (loading) return; + + const target = targets?.find((it) => it.id == bot_id); + + if (!target) { + setError("Pick a bot."); + return; + } + + if (!window.confirm(`Hand ${what} over to ${target.name}?`)) return; + + setLoading(true); + setError(undefined); + + const res = await postAdminApi({ + path: submit_path, + body: { ...body, bot_id: target.id, instructions: with_note && note.trim() ? note : undefined }, + }); + + setLoading(false); + + if (!res.success || !res.singleRes) { + setError(res.msg || "Couldn't hand it over."); + return; + } + + onHandedOver({ saved: res.singleRes, msg: res.msg || `Handed over to ${target.name}.` }); + }; + + return ( +
+ + {!targets ? ( + Loading the bots... + ) : targets.length ? ( +