diff --git a/.env.example b/.env.example index 231933bf..89d8a399 100644 --- a/.env.example +++ b/.env.example @@ -128,7 +128,7 @@ AGENT_COMPUTER_ALLOW_PRIVATE_HOSTS=true # EGRESS_PROXY_SALES_BOT=http://sales.proxy.internal:8080 -# The managed teammate AG-UI endpoint. Required: use an HTTP(S) URL. +# The managed coworker AG-UI endpoint. Required: use an HTTP(S) URL. MANAGED_AGENT_AG_UI_URL=http://localhost:4200/ag-ui # The second Bot in the box runs on http://localhost:4201/ag-ui, on a framework rather than diff --git a/README.md b/README.md index c2005c19..710310fe 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # OpenBot -**AI teammates you can hand real work to, and actually trust with the access.** Each gets a computer of its own: a real browser with its own logins, its own files, and only the tools you grant. Every action decided before it happens and recorded after. +**AI coworkers you can hand real work to, and actually trust with the access.** Each gets a computer of its own: a real browser with its own logins, its own files, and only the tools you grant. Every action decided before it happens and recorded after. [**copilotkit.ai/openbot**](https://copilotkit.ai/openbot) · [**Quick start**](#quick-start) · [**Features**](#features) · [**Bring your own agent**](#bring-your-own-agent) · [**Architecture**](#architecture) · [**Docs**](docs/README.md) @@ -18,7 +18,7 @@ https://github.com/user-attachments/assets/535ef7ee-1631-4a69-b839-564c56cf90b4
Bring any AG-UI agent, written on a framework or by hand, and it arrives as a -teammate with a channel of its own. Watch it work on its own screen, take the +coworker with a channel of its own. Watch it work on its own screen, take the wheel when it reaches something it should not do alone, then hand it back. It answers with components rather than only prose, and the whole thing runs on your own machine. @@ -33,7 +33,7 @@ your own machine. An agent platform that runs inside your own infrastructure. Docker Compose brings up every part of it, the data sits in your PostgreSQL, and the model is yours to choose: no model ships in the box, and an administrator supplies the credential, which is encrypted at rest and never logged. -Three teammates ship in the example package, and they are configuration rather than code: **General Assistant** for everyday work, **Knowledge** for company questions, **Risk Analyst** for risk and compliance. Add your own by editing `agents.yaml` or from `/agents` in the UI. +Three coworkers ship in the example package, and they are configuration rather than code: **General Assistant** for everyday work, **Knowledge** for company questions, **Risk Analyst** for risk and compliance. Add your own by editing `agents.yaml` or from `/agents` in the UI. Anything a Bot does to a computer, a file, an MCP server or a component goes through one gateway that decides and records it. That is the difference between an agent that can use your tools and an agent you can let near them. @@ -101,15 +101,15 @@ A Bot is any endpoint speaking [AG-UI](https://github.com/ag-ui-protocol/ag-ui), - Open `/bot` and ask: `Open news.ycombinator.com and tell me the top story.` - Ask the Bot to fill out , then inspect `/admin/audit`. - Open `/admin/boundaries`, add a deny rule or preset, and retry the same browser action. -- Create a teammate from `/agents`, give it a standing role, and start a channel with it. +- Create a coworker from `/agents`, give it a standing role, and start a channel with it. ## Main surfaces | Route | Purpose | | -------------------- | ------------------------------------------------------------------ | | `/` | Start and browse channels. | -| `/agents` | Create, edit, duplicate, hide, delete, and launch teammates. | -| `/channel/:id` | Converse with one teammate and view its live screen/profile panel. | +| `/agents` | Create, edit, duplicate, hide, delete, and launch coworkers. | +| `/channel/:id` | Converse with one coworker and view its live screen/profile panel. | | `/bot` | Direct chat with a Bot; `?agent=` selects one. | | `/skills` | Create and enable personal skills. | | `/settings` | User preferences. | @@ -142,21 +142,21 @@ A Bot is any endpoint speaking [AG-UI](https://github.com/ag-ui-protocol/ag-ui), Any AG-UI endpoint can be a Bot. -From `/agents`, create a teammate with: +From `/agents`, create a coworker with: - name, title, and role description; - private or public visibility; - optional AG-UI endpoint; - optional write-only authorization header. -The server validates agent endpoints with the same target checks used for browser navigation. If no custom endpoint is set, product-created teammates use `MANAGED_AGENT_AG_UI_URL`. +The server validates agent endpoints with the same target checks used for browser navigation. If no custom endpoint is set, product-created coworkers use `MANAGED_AGENT_AG_UI_URL`. Tenant package agents are declared in `agents.yaml` as either: - `built-in`, with a system prompt; or - `remote-ag-ui`, with an endpoint. -See [docs/configuration.md](docs/configuration.md) and [docs/teammates.md](docs/teammates.md). +See [docs/configuration.md](docs/configuration.md) and [docs/coworkers.md](docs/coworkers.md). ## Configuration @@ -191,7 +191,7 @@ Full reference: [docs/configuration.md](docs/configuration.md). | Service | Port | Purpose | | ------------------------ | -------------------------- | ------------------------------------------------------------------------------------------------ | | `app` | 3010 | React/Vite UI. | -| `server` | 3001 | Hono API, CopilotKit runtime, auth, policy, audit, plugins, components, teammates, and channels. | +| `server` | 3001 | Hono API, CopilotKit runtime, auth, policy, audit, plugins, components, coworkers, and channels. | | `agent-computer` | 4100 | Chromium plus `/workspace` and browser profile. | | `agent-bot` | 4200 | Proof-of-concept AG-UI Bot. | | `agent-langgraph` | 4201 | LangGraph AG-UI Bot. | @@ -258,7 +258,7 @@ Use `bash scripts/start.sh` for the whole stack. Use `bun run dev` only when you - [docs/architecture.md](docs/architecture.md) - [docs/configuration.md](docs/configuration.md) - [docs/development.md](docs/development.md) -- [docs/teammates.md](docs/teammates.md) +- [docs/coworkers.md](docs/coworkers.md) ## Contributing diff --git a/app/src/components/agents/abstract-avatar.tsx b/app/src/components/agents/abstract-avatar.tsx index b65b039e..9ca98450 100644 --- a/app/src/components/agents/abstract-avatar.tsx +++ b/app/src/components/agents/abstract-avatar.tsx @@ -16,7 +16,7 @@ export function AbstractAvatar({ className="inline-flex shrink-0 overflow-hidden rounded-full" style={{ height: size, width: size }} > - {/* The drawing carries its own role; hidden so the teammate is announced once, by name. */} + {/* The drawing carries its own role; hidden so the coworker is announced once, by name. */} diff --git a/app/src/components/agents/agent-fields.tsx b/app/src/components/agents/agent-fields.tsx index ccc0b8fd..752a816c 100644 --- a/app/src/components/agents/agent-fields.tsx +++ b/app/src/components/agents/agent-fields.tsx @@ -33,7 +33,7 @@ export function AgentFields({ onCancel, }: { defaultValues: AgentFormValues; - /** Whether this teammate already has a key, so the field can say so without showing it. */ + /** Whether this coworker already has a key, so the field can say so without showing it. */ hasAuth?: boolean; submitLabel: string; onSubmit: (values: AgentFormValues) => Promise; diff --git a/app/src/components/agents/agent-profile.tsx b/app/src/components/agents/agent-profile.tsx index c0b18245..e6fcfe19 100644 --- a/app/src/components/agents/agent-profile.tsx +++ b/app/src/components/agents/agent-profile.tsx @@ -54,7 +54,7 @@ function ProfileSkeleton() { export function AgentProfile({ agentId }: { agentId: string }) { const queryClient = useQueryClient(); const navigate = useNavigate(); - // State is keyed by teammate id because this panel can remain open while its target changes. + // State is keyed by coworker id because this panel can remain open while its target changes. const [editingId, setEditingId] = useState(null); const [confirmingDeleteId, setConfirmingDeleteId] = useState( null, @@ -76,7 +76,7 @@ export function AgentProfile({ agentId }: { agentId: string }) { if (agent.error || !agent.data) { return (

- Could not load this teammate. + Could not load this coworker.

); } diff --git a/app/src/components/agents/new-agent.tsx b/app/src/components/agents/new-agent.tsx index f4c8f222..c99bb222 100644 --- a/app/src/components/agents/new-agent.tsx +++ b/app/src/components/agents/new-agent.tsx @@ -12,9 +12,9 @@ export function NewAgent() { return (
-

New teammate

+

New coworker

- The role you write here applies in every channel this teammate works + The role you write here applies in every channel this coworker works in.

@@ -23,12 +23,12 @@ export function NewAgent() { defaultValues={emptyAgentForm} error={createAgent.error} onSubmit={async (values) => { - // The panel swaps from the form to the new teammate's profile, and the roster behind it + // The panel swaps from the form to the new coworker's profile, and the roster behind it // picks up the new card: the next thing to do is start a channel with it. const agent = await createAgent.mutateAsync(agentInputFrom(values)); await navigate({ search: { agent: agent.id }, to: "/agents" }); }} - submitLabel="Create teammate" + submitLabel="Create coworker" />
); diff --git a/app/src/components/channels/channel-chat.tsx b/app/src/components/channels/channel-chat.tsx index 8f26fa7c..8f4546a2 100644 --- a/app/src/components/channels/channel-chat.tsx +++ b/app/src/components/channels/channel-chat.tsx @@ -27,9 +27,9 @@ import { useSkillCommands } from "@/lib/plugins/skill-commands"; const SEND_WITHOUT_JOIN_AFTER_MS = 1500; /** - * One channel's conversation with one teammate. + * One channel's conversation with one coworker. * - * The local agent id is channel-scoped so two channels with the same teammate keep separate + * The local agent id is channel-scoped so two channels with the same coworker keep separate * durable threads. */ export function ChannelChat({ @@ -132,7 +132,7 @@ export function ChannelChat({ }; }, [copilotkit, agent, isReady, channel.threadId, runtimeAgentId]); - // Tool calls from this conversation act on this teammate's own computer. + // Tool calls from this conversation act on this coworker's own computer. useActiveBot(runtimeAgentId); const skillCommands = useSkillCommands(runtimeAgentId); @@ -301,14 +301,14 @@ export function ChannelChat({ ) : null} {channel.active ? null : (

- This teammate has been deleted. The conversation stays readable, + This coworker has been deleted. The conversation stays readable, but it can no longer reply.

)} } onSubmit={async (draft) => { - // `draft.agentId` carries the @mentioned teammate, but nothing routes on it yet: this + // `draft.agentId` carries the @mentioned coworker, but nothing routes on it yet: this // channel is pinned to one `runtimeAgentId` for the life of its thread, so honouring a // per-message mention is a change to that binding, not to the composer. // diff --git a/app/src/components/channels/compose-state.ts b/app/src/components/channels/compose-state.ts index 1010a2d3..cdf31544 100644 --- a/app/src/components/channels/compose-state.ts +++ b/app/src/components/channels/compose-state.ts @@ -10,13 +10,13 @@ export type Recipient = { }; /** - * One teammate per channel. + * One coworker per channel. * - * Matches the chat screen's current one-teammate render contract. + * Matches the chat screen's current one-coworker render contract. */ export const MAX_RECIPIENTS = 1; -/** Add a teammate, replacing the oldest once the channel recipient cap is reached. */ +/** Add a coworker, replacing the oldest once the channel recipient cap is reached. */ export function addRecipient( current: readonly Recipient[], next: Recipient, diff --git a/app/src/components/channels/composer/composer.tsx b/app/src/components/channels/composer/composer.tsx index 5a1584f0..7ce029b1 100644 --- a/app/src/components/channels/composer/composer.tsx +++ b/app/src/components/channels/composer/composer.tsx @@ -47,7 +47,7 @@ export type ComposerProps = { onStop?: () => void; /** * The conversation cannot take another message at all, which is a property of the conversation - * rather than of the moment: a channel whose teammate was deleted. This is the only thing that + * rather than of the moment: a channel whose coworker was deleted. This is the only thing that * stops a person typing. */ disabled?: boolean; diff --git a/app/src/components/channels/recipient-field.tsx b/app/src/components/channels/recipient-field.tsx index 46a90f00..bb33f113 100644 --- a/app/src/components/channels/recipient-field.tsx +++ b/app/src/components/channels/recipient-field.tsx @@ -19,7 +19,7 @@ import { agentListQueryOptions } from "@/lib/agents/queries"; * Who a conversation that does not exist yet is with. * * Uses the same recipient list model as compose state, even while channels are capped at one - * teammate. + * coworker. */ export function RecipientField({ recipients, @@ -61,9 +61,9 @@ export function RecipientField({ {isFull ? null : ( setSearch(event.target.value)} - placeholder="Choose a teammate…" + placeholder="Choose a coworker…" value={search} /> @@ -96,7 +96,7 @@ export function RecipientField({ ))} {matches.length === 0 ? (
  • - No teammate by that name. + No coworker by that name.
  • ) : null} diff --git a/app/src/lib/agents/form.ts b/app/src/lib/agents/form.ts index fa531b38..b08fa6a7 100644 --- a/app/src/lib/agents/form.ts +++ b/app/src/lib/agents/form.ts @@ -1,7 +1,7 @@ import { z } from "zod"; /** - * Browser-side teammate form contract. Limits match the server parser so validation errors can be + * Browser-side coworker form contract. Limits match the server parser so validation errors can be * shown next to fields before submit. */ export const agentFormSchema = z.object({ @@ -22,7 +22,7 @@ export const agentFormSchema = z.object({ .max(1000, "Role description must be 1000 characters or fewer."), visibility: z.enum(["public", "private"]), /** - * The AG-UI endpoint this teammate runs on. Empty means the Bot in the box. + * The AG-UI endpoint this coworker runs on. Empty means the Bot in the box. * * Only URL shape is checked here; deployment allow/deny rules are server-side. */ diff --git a/app/src/lib/agents/mutations.ts b/app/src/lib/agents/mutations.ts index f71768a3..a8f8eeff 100644 --- a/app/src/lib/agents/mutations.ts +++ b/app/src/lib/agents/mutations.ts @@ -6,7 +6,7 @@ export type AgentInput = { title: string; roleDescription: string; visibility: AgentVisibility; - /** Where this teammate runs. Empty means the Bot in the box. */ + /** Where this coworker runs. Empty means the Bot in the box. */ endpoint?: string; /** Write-only auth value; omitted when the user leaves the key field empty. */ auth?: { header: string; value: string }; @@ -28,7 +28,7 @@ async function agentRequest( .json() .then((body: { error?: string }) => body.error) .catch(() => undefined); - throw new Error(message ?? "Teammate operation failed"); + throw new Error(message ?? "Coworker operation failed"); } return response; } diff --git a/app/src/lib/agents/queries.ts b/app/src/lib/agents/queries.ts index 2e9d5476..91c52247 100644 --- a/app/src/lib/agents/queries.ts +++ b/app/src/lib/agents/queries.ts @@ -3,7 +3,7 @@ import { queryOptions } from "@tanstack/react-query"; export type AgentVisibility = "public" | "private"; /** - * A teammate as the browser sees it. + * A coworker as the browser sees it. * * `canManage` and `systemOwned` are server-decided authorization facts; components render from the * returned flags rather than recomputing ownership rules. @@ -15,7 +15,7 @@ export type AgentProfile = { roleDescription: string; avatarSeed: string; visibility: AgentVisibility; - /** Where this teammate runs. Null for the Bot in the box. */ + /** Where this coworker runs. Null for the Bot in the box. */ endpoint: string | null; /** Whether a key is set for it. Never the key itself. */ hasAuth: boolean; @@ -23,9 +23,9 @@ export type AgentProfile = { systemOwned: boolean; canManage: boolean; /** - * Whether the signed-in person created this teammate. + * Whether the signed-in person created this coworker. * - * Separate from `canManage`, which is also true for administrators on everybody's teammates. Split + * Separate from `canManage`, which is also true for administrators on everybody's coworkers. Split * a roster on `canManage` and an administrator's "mine" fills up with other people's work. */ mine: boolean; @@ -47,7 +47,7 @@ export function agentListQueryOptions(hidden = false) { credentials: "include", }, ); - if (!response.ok) throw new Error("Could not load teammates"); + if (!response.ok) throw new Error("Could not load coworkers"); return ((await response.json()) as { agents: AgentProfile[] }).agents; }, }); @@ -60,7 +60,7 @@ export function agentQueryOptions(agentId: string) { const response = await fetch(`/api/agents/${agentId}`, { credentials: "include", }); - if (!response.ok) throw new Error("Could not load this teammate"); + if (!response.ok) throw new Error("Could not load this coworker"); return ((await response.json()) as { agent: AgentProfile }).agent; }, }); diff --git a/app/src/lib/channels/mutations.ts b/app/src/lib/channels/mutations.ts index c48821cb..776756cf 100644 --- a/app/src/lib/channels/mutations.ts +++ b/app/src/lib/channels/mutations.ts @@ -2,7 +2,7 @@ import { mutationOptions, type QueryClient } from "@tanstack/react-query"; import { type AgentChannel, channelKeys } from "./queries"; /** - * Start a new channel with one or more teammates. + * Start a new channel with one or more coworkers. * * Deliberately not idempotent: every call creates a channel with its own thread. */ diff --git a/app/src/lib/channels/queries.ts b/app/src/lib/channels/queries.ts index b332223a..7a58550b 100644 --- a/app/src/lib/channels/queries.ts +++ b/app/src/lib/channels/queries.ts @@ -3,8 +3,8 @@ import { queryOptions } from "@tanstack/react-query"; /** * A channel as the browser sees it. * - * `threadId` is what makes two channels with the same teammate independent conversations, and - * `active` is false once a linked teammate has been deleted: the transcript stays readable, but + * `threadId` is what makes two channels with the same coworker independent conversations, and + * `active` is false once a linked coworker has been deleted: the transcript stays readable, but * nothing more can be said in it. */ export type AgentChannel = { diff --git a/app/src/routes/_authed/_app/agents/index.tsx b/app/src/routes/_authed/_app/agents/index.tsx index 6259c761..436b7769 100644 --- a/app/src/routes/_authed/_app/agents/index.tsx +++ b/app/src/routes/_authed/_app/agents/index.tsx @@ -12,7 +12,7 @@ import { Empty, EmptyHeader, EmptyTitle } from "@/components/ui/empty"; import { agentListQueryOptions } from "@/lib/agents/queries"; /** - * Creating and inspecting a teammate are search-parameter states so the roster remains mounted and + * Creating and inspecting a coworker are search-parameter states so the roster remains mounted and * Back closes the detail pane. */ const agentsSearchSchema = z.object({ diff --git a/app/src/routes/_authed/_app/channel/$channelId.tsx b/app/src/routes/_authed/_app/channel/$channelId.tsx index 717d33b6..5af2dfaa 100644 --- a/app/src/routes/_authed/_app/channel/$channelId.tsx +++ b/app/src/routes/_authed/_app/channel/$channelId.tsx @@ -60,7 +60,7 @@ function RouteComponent() { const isSettingsOpen = settings === true; const prefersReducedMotion = useReducedMotion(); const isWatching = watch === true; - /** Channel routing currently supports one teammate. */ + /** Channel routing currently supports one coworker. */ const agentId = channel.data?.agentIds[0]; /** Needs-you state is rendered by the screen when the screen is already open. */ const needsYou = useNeedsYou(agentId, !isWatching); @@ -178,7 +178,7 @@ function RouteComponent() { ) : null}