Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -18,7 +18,7 @@ https://github.com/user-attachments/assets/535ef7ee-1631-4a69-b839-564c56cf90b4
<div align="center">

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.
Expand All @@ -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.

Expand Down Expand Up @@ -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 <https://httpbin.org/forms/post>, 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=<id>` selects one. |
| `/skills` | Create and enable personal skills. |
| `/settings` | User preferences. |
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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. |
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion app/src/components/agents/abstract-avatar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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. */}
<span aria-hidden="true" className="contents">
<Avatar name={seed} size={size} />
</span>
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/agents/agent-fields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<unknown>;
Expand Down
4 changes: 2 additions & 2 deletions app/src/components/agents/agent-profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string | null>(null);
const [confirmingDeleteId, setConfirmingDeleteId] = useState<string | null>(
null,
Expand All @@ -76,7 +76,7 @@ export function AgentProfile({ agentId }: { agentId: string }) {
if (agent.error || !agent.data) {
return (
<p className="p-8 text-sm text-destructive" role="alert">
Could not load this teammate.
Could not load this coworker.
</p>
);
}
Expand Down
8 changes: 4 additions & 4 deletions app/src/components/agents/new-agent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ export function NewAgent() {
return (
<div className="mx-auto flex w-full max-w-xl flex-col gap-6 p-8">
<header>
<h1 className="text-2xl font-semibold">New teammate</h1>
<h1 className="text-2xl font-semibold">New coworker</h1>
<p className="mt-1 text-sm text-muted-foreground">
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.
</p>
</header>
Expand All @@ -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"
/>
</div>
);
Expand Down
10 changes: 5 additions & 5 deletions app/src/components/channels/channel-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -301,14 +301,14 @@ export function ChannelChat({
) : null}
{channel.active ? null : (
<p className="pb-2 text-sm text-muted-foreground" role="status">
This teammate has been deleted. The conversation stays readable,
This coworker has been deleted. The conversation stays readable,
but it can no longer reply.
</p>
)}
</>
}
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.
//
Expand Down
6 changes: 3 additions & 3 deletions app/src/components/channels/compose-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion app/src/components/channels/composer/composer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 4 additions & 4 deletions app/src/components/channels/recipient-field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -61,9 +61,9 @@ export function RecipientField({
{isFull ? null : (
<InputGroup className="h-8 w-56 border-none bg-transparent">
<InputGroupInput
aria-label="Choose a teammate"
aria-label="Choose a coworker"
onChange={(event) => setSearch(event.target.value)}
placeholder="Choose a teammate…"
placeholder="Choose a coworker…"
value={search}
/>
<InputGroupAddon />
Expand Down Expand Up @@ -96,7 +96,7 @@ export function RecipientField({
))}
{matches.length === 0 ? (
<li className="px-2 py-1.5 text-sm text-muted-foreground">
No teammate by that name.
No coworker by that name.
</li>
) : null}
</ul>
Expand Down
4 changes: 2 additions & 2 deletions app/src/lib/agents/form.ts
Original file line number Diff line number Diff line change
@@ -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({
Expand All @@ -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.
*/
Expand Down
4 changes: 2 additions & 2 deletions app/src/lib/agents/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand All @@ -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;
}
Expand Down
12 changes: 6 additions & 6 deletions app/src/lib/agents/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -15,17 +15,17 @@ 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;
hidden: boolean;
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;
Expand All @@ -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;
},
});
Expand All @@ -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;
},
});
Expand Down
2 changes: 1 addition & 1 deletion app/src/lib/channels/mutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
4 changes: 2 additions & 2 deletions app/src/lib/channels/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion app/src/routes/_authed/_app/agents/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
Loading