Skip to content

Let a person set standing instructions that every coworker follows - #335

Open
nicklaunches wants to merge 1 commit into
CopilotKit:mainfrom
nicklaunches:pr/standing-instructions
Open

Let a person set standing instructions that every coworker follows#335
nicklaunches wants to merge 1 commit into
CopilotKit:mainfrom
nicklaunches:pr/standing-instructions

Conversation

@nicklaunches

Copy link
Copy Markdown

Related: #325, #280.

Screenshot to follow. The Settings section (empty, filled, and the saved state) will be
attached as a comment; everything else below is verified against the branch.

The problem

A deployment has two carriers for a durable instruction, and both are about the work rather than
about the person. A coworker's role belongs to the coworker and reads the same to everybody who
talks to it. A skill is pulled in for one task.

Neither can hold "always write to me in British English", or "we are a two-person company, never
call us a team", or "our product is spelled like this". Those are facts about the person asking,
true of every task they will ever ask for, in every channel, from every coworker. Today the only
places to put them are the composer, retyped every time, or somebody's role, where it becomes
everybody's.

What changed

A settings section. One text box per person on the Settings page, between the appearance switch
and the shortcut list, since it is the only thing on that screen that changes what a coworker says
rather than what the browser looks like. Empty is absence: an empty save deletes the row rather than
storing "", so there is one representation of "has written none".

A prompt seam. standingInstructionsGuidance renders the text into a block placed straight
after the coworker's role and before everything else, and the block carries its own precedence
sentence: the role decides what a coworker does, these decide how it does it. That sentence is load
bearing, not decoration. Two standing instructions in one prompt is a conflict otherwise resolved by
whichever the model read last, and the resolution is not symmetric: a person may say how they want
things done and must not be able to say what a coworker is for. Without it, "always answer in one
line" quietly overrides a role that exists to produce a filing with its sources in it.

Every path a run can take. LoadInstructions is a closure rather than a captured string, for
the same reason LoadToolsForBot is: a person can change their instructions between two runs, and a
value read at boot would serve everybody the first person's preferences. It is wired into the
request path, into agentFor so a handoff delivered at three in the morning carries the same text
the Bot in front of the person does, and into the routine builder so overnight work reads the way
the owner's own turn would. It is read once per roster build and only when something built-in is
being built.

Ownership. The routes are requireUser, scoped to context.var.actor.id, and there is
deliberately no route for reading or writing somebody else's, an administrator included: this text
goes into a prompt that then speaks as that person's coworker, so a way to set it for another
account would be a way to put words in their mouth in every channel they work in. The row cascades
with the account.

Remote Bots are not sent it. A remote AG-UI Bot composes its own prompt at somebody else's
endpoint. Whether it would honour the text and how it would rank against the role are both unknown,
and one deployment's person addressing another deployment's server with their own prose is a
separate question. Not done here, and the reasoning is written down where somebody will look for it.

Failure is silence. A read that fails costs a coworker a paragraph. One that refused to start
would cost the conversation, and a preferences row is not worth a run.

Schema. New user_instructions table, migration 0026_user_instructions. The user id is the
primary key rather than a column beside a surrogate one, because "what has this person asked every
coworker to do" must have exactly one answer.

How it was verified

  • server/tests/schema.test.ts covers the table: the user id as primary key, the column set, and
    the cascade to users.
  • server/tests/settings-instructions-routes.test.ts covers the routes: read, write, delete on
    empty, the length limit, the 503 when the store is absent, and that the actor comes from the
    session rather than the request.
  • server/tests/copilot.test.ts covers the prompt seam: where the block sits relative to the role,
    that it is absent when nobody has written any, that it never reaches a remote Bot, and that a
    failed read costs a paragraph rather than a run.
  • bun test server/tests/copilot.test.ts server/tests/schema.test.ts server/tests/settings-instructions-routes.test.ts gives 61 pass, 0 fail.
  • server/tests/user-instructions.integration.test.ts covers the store against a real database
    (upsert, delete on empty, and the cascade). It needs a live Postgres and was not run in this
    environment; it runs in CI with the rest of the integration suite.
  • bun run typecheck clean across app, server and worker; bunx biome check clean on all changed
    files.

🤖 Generated with Claude Code

A deployment had two carriers for a durable instruction and both are
about the work rather than about the person. A coworker's role belongs to
the coworker and reads the same to everybody who talks to it; a skill is
pulled in for one task. Neither can hold "always write to me in British
English" or "we are a two-person company, never call us a team", which is
a fact about the person and true in every channel, so people retyped it
into the composer or went without.

Settings now carries one text per person. It is spliced into every
built-in coworker's prompt straight after the role, on every run in every
channel, and into the runs a routine starts on the owner's behalf while
they are asleep, so overnight work reads the way their own turn would. The
block carries its own precedence sentence: the role decides what a
coworker does and these decide how it does it, because a person may say
how they want things done and must not be able to say what a coworker is
for.

The text belongs to the person who wrote it. The routes take the actor
from the session and never from the path or the body, there is no route
for reading or writing somebody else's, an administrator included, and the
row cascades with the account. Remote AG-UI Bots are not given it: that
prompt is composed at somebody else's endpoint, so there is no knowing
whether it is read or how it ranks against the role.

Empty is absence rather than a stored empty string, so there is one
representation of "has written none". Reading them fails silently at build
time: a coworker that could not be told loses a paragraph, where one that
refused to start would lose the conversation.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WaHWJ1niprhBc5NzJ9pxme
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