Skip to content

Make a coworker by talking to one, and grant it nothing - #31

Merged
jerelvelarde merged 1 commit into
mainfrom
jerel/bot-creator-fork
Sep 2, 2026
Merged

Make a coworker by talking to one, and grant it nothing#31
jerelvelarde merged 1 commit into
mainfrom
jerel/bot-creator-fork

Conversation

@jerelvelarde

Copy link
Copy Markdown
Owner

Making a coworker by talking to one

A vague request, an interview that asks rather than assumes, a card carrying the whole instruction the
coworker will run on, and then a conversation with the thing that was just made — which correctly says
it can reach nothing. Full size, as video:
bot-creator.mp4

The same change as CopilotKit/OpenBot#331, landed
here first because this fork is what deploys. Cherry-picked rather than merged: that branch sits on
feat/bot-templates, whose two migrations were re-timestamped to sort after upstream's onboarding
pair, and merging it here would re-run migrations this deployment applied under their earlier
timestamps. This commit adds no migration at all, so the pre-deploy step is a no-op.

The problem

Making a coworker means filling in a form, and the form asks for the one thing hardest to write cold.
role_description is not a label: with no endpoint it becomes the standing instruction handed to a
model on every turn in every channel that coworker is in. A blank box is the worst way to ask for it —
people type a sentence, get a coworker that answers vaguely, and never go back.

The three questions that actually make one useful are the ones nobody answers unprompted: what it must
never conclude, what it says when the evidence is thin, and whether something here already does the
job. A form cannot ask a follow-up. A conversation can.

The approach

A bot-creator skill, and the grant is the gate. The package ships it and grants it to
general-assistant, beside skill-creator. A Bot holding it is offered list_bots, read_bot,
list_bot_skills and save_bot; every other Bot is offered none of them, because four more tools on
every run costs the per-run narrowing more than it buys.

Browser-side, so nothing is re-implemented. POST /api/agents already answers who may create a
coworker and writes bot.created with the actor on it; POST /api/plugins/grants puts a skill on one.
A tool riding the signed-in session inherits both unchanged. A server-side tool would have to carry an
actor into runs that have none — a routine, a Slack thread, a schedule — and the first way that goes
wrong is a coworker created under a name nobody chose.

The card is the whole tool. save_bot has no handler: the run suspends, and the card shows the
name, the job, the skills and the entire role description, scrolled rather than clamped. That text
runs on somebody's behalf, so a clamp would hide half of what they are agreeing to.

It grants skills and nothing else. No address, no connector, no tool, no boundary, no visibility.
Everything the coworker might reach is granted afterwards on its profile. A card that could grant
capability would make asking a Bot the fastest route around the screens that decide it.

Creating and granting are two calls and the pair is not atomic, so a grant that fails after the
coworker exists is named in the answer rather than rounded up — rolling the coworker back would delete
something the person just watched being made over a skill they can add in two clicks.

What is not covered

  • No editing. The card creates; it never replaces. Changing a coworker stays on the profile screen:
    a coworker is a thing people have been talking to, and an unattended overwrite is not undoable.
  • No test drives a real conversation. The suite covers the card, the answers and the package
    coupling. The leg from interview to created coworker needs a model and a platform, so it is verified
    by hand against a running deployment.
  • No provenance. The trail cannot tell a coworker a Bot drafted from one somebody typed into the
    form. bot.created records the actor either way.

Verification

format:check, lint and typecheck clean. 377 pass, 0 fail across 57 files.
server/tests/tenant-package.test.ts passes and was updated rather than left — it pins the shipped
roster, and it is what caught the grant landing on Knowledge instead of the general assistant.
scripts/check-bot-templates.ts still passes over all 27 shipped templates, now against 7 seeded slugs.

Smoke-tested on this branch against a running deployment, with both authoring skills on one Bot: the
model called list_bots and list_bot_skills, drew the card, created Expense Review, and granted it
/check-a-claim and /find-a-document — both rows confirmed in plugin_grants, with bot.created
and two configuration.changed in the audit trail.

Two defects found by using it, both about state rather than logic. The first conversation this ever
had answered "No skills exist here yet" to a deployment holding nine — the model called the tool in the
first second of the run, the handler read a useQuery that had not returned, and an empty list and an
unloaded list were sharing a code path. The answers now fetch when asked, from a module a test calls
against a cold query client. Second, the link on a completed card lived in component state, so it would
have drawn once and silently stopped for anybody who reloaded; the coworker's id travels in the answer
now, read back by a function paired with the sentence and pinned by a test.

File What it holds down
app/tests/proposed-bot-card.test.tsx The whole role description unclipped; that rendering writes nothing; a partial grant named; a refusal leaving the run open; the completed card's link rebuilt from the answer alone
app/tests/bot-tool-answers.test.ts Every answer fetched against a cold query client, so an unloaded list can never read as an empty one
app/tests/bot-creator-slug.test.ts That the package ships the slug the app gates on, that a Bot holds it, and that it declares no tools

Merge notes

No migration, so the pre-deploy migration step does nothing and this cannot fail the way a schema
change can. app/src/lib/copilot/provider.tsx gains one component beside SkillTools, and
examples/fintech/agents.yaml gains bot-creator next to skill-creator on the general assistant —
that line is the gate for the whole feature, and both bot-creator-slug.test.ts and the tenant-package
assertion fail loudly if it is dropped.

Making a coworker means filling in a form, and the form asks for the one
thing hardest to write cold: a role description that becomes standing
instructions handed to a model on every turn in every channel. People
type a sentence, get a coworker that answers vaguely, and never come
back to fix it — the field that decides everything is the field a blank
box is worst at.

So it becomes an interview. The deployment ships a `bot-creator` skill
whose instruction is how to ask: what job it does, what it must never
conclude, and what it says when the evidence is thin. A Bot holding that
skill is offered four tools — `list_bots`, `read_bot`, `list_bot_skills`
and `save_bot` — and the interview ends in a card rather than in text
somebody retypes into the form.

The grant is the gate, not a flag. Four more tools on every run costs the
per-run narrowing more than it buys, and a Bot for looking up
transactions has no business making coworkers. The tools ride the
signed-in session from the browser, so `POST /api/agents` answers who may
and writes `bot.created` with the actor on it, unchanged. A server-side
tool would have to carry an actor into runs that have none — a routine, a
Slack thread, a schedule — and the first way that goes wrong is a
coworker created under the wrong name.

The card is the whole tool; there is no handler behind it. It shows the
entire role description, unclipped, because that text will run on
somebody's behalf and a clamp would hide half of what they are agreeing
to. It can grant skills and nothing else: no address, no connector, no
tool, no boundary, no visibility. A card that could grant capability
would make asking a Bot the fastest route around the screens that exist
to decide it.

Two things found by using it. The first conversation answered "No skills
exist here yet" to a deployment holding nine, because the handler read a
query that had not loaded — an empty list and an unloaded list were
sharing a code path, and the Bot repeated the tidier one to the person as
fact. The answers now fetch when asked. And the link on a completed card
lived in component state, so it would have appeared once and then
silently stopped for anybody who reloaded; the coworker's id travels in
the answer instead, read back by a function paired with the sentence.

Verification: 321 pass, 0 fail across 48 files, format and lint clean.
Driven end to end against a running deployment — a vague request, an
interview, a card, a coworker, and a conversation with it that correctly
says it cannot reach anything yet.
@jerelvelarde
jerelvelarde merged commit 7113120 into main Sep 2, 2026
13 checks passed
@jerelvelarde
jerelvelarde deleted the jerel/bot-creator-fork branch September 2, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant