mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-10 12:37:55 +01:00
fix(chat): tell chat agents when the Conference Room is enabled and what it is (#20036)
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - Agent Chat lets a person talk to an agent in a standing conversation > - The Conference Room is a separate, experimental board chat surface at `/<prefix>/board-chat` > - A chat agent cannot read the experimental settings, so it cannot know that the Conference Room exists > - When a person asks to "discuss the launch in the conference room", the agent treats the phrase as a metaphor and asks launch questions > - This pull request tells the chat agent about the Conference Room only while the experiment is on > - The benefit is that the agent links the real surface and states its actual scope ## Linked Issues or Issue Description **What happened** A person said "Let us discuss the launch in the conference room." in Agent Chat on a local-trusted instance with Conference Room Chat enabled. The agent's reasoning asked whether the conference room is a Paperclip feature. It found no information, because the experimental settings endpoint is board-only. It replied with launch questions and gave no link to the Conference Room. **Expected behavior** The agent links the Conference Room (`/<prefix>/board-chat`) and says what it is: a separate board assistant whose history does not carry over from the chat. When the experiment is off, the agent does not mention it. **Steps to reproduce** 1. Enable Conference Room Chat in Instance Settings > Experimental on a local-trusted instance. 2. Open a chat with the CEO agent. 3. Send "Let us discuss the launch in the conference room." 4. See that the reply gives no Conference Room link. ## What Changed - Add `CONFERENCE_ROOM_CHAT_DIRECTIVE` in `server/src/services/agent-conversations.ts`. It gives the route as a Markdown link and states the real scope of the surface. - Render the directive in standing-chat task markdown only when `conferenceRoomEnabled` is true (`server/src/services/heartbeat/task-markdown.ts`). - Set `conferenceRoomEnabled` from the experimental settings that the heartbeat already loads (`server/src/services/heartbeat/task-context.ts`). No new query runs before the turn starts. - Add a regression test for the enabled, disabled, and non-chat cases. ## Verification - `cd server && pnpm exec vitest run src/services/heartbeat/task-markdown.test.ts` passes (16 tests). - `npx tsc --noEmit -p server/tsconfig.json` passes. - End to end: the `agent-chat.conference-room` chat eval runs with this commit cherry-picked onto its case branch. ## Risks - Low risk. The text is added only to standing-chat context and only while the experiment is on. - The directive does not check the deployment mode. It says that the surface works only on a local single-operator instance, and the route itself refuses other deployments with a clear error. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`), tool use and code execution in Claude Code. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c8822ffd4b
commit
5978b7a1e0
4 files changed
+18
-2
No files matched your search
@@ -127,6 +127,9 @@ For status updates, distinguish recorded task status from active execution and v
|
||||
Keep discussion here and leave the conversation available for the next message. Link handed-off tasks in your reply; do not make this conversation blocked by their completion or wait for them. Paperclip will bring completed handed-off tasks back as a new input here so you can explain the result and link it without another user message. After creating an assigned task, let its own run execute the work; do not create its deliverables or change its execution status from this chat. Reply normally and end your turn; Paperclip manages the conversation waiting state. Do not change its status, create a review confirmation just to finish a reply, mark it complete, or poll for another reply. An accepted plan authorizes handoff to execution tasks, never implementation on this conversation. Honor normal approvals. Ask mode is non-mutating. Plan mode supports research and writing/revising the plan; hand off for execution only through the normal authorized workflow.`;
|
||||
|
||||
/** Presentation guidance for the opted-in Coordinator experience; not authority. */
|
||||
/** Rendered only while the instance enables the Conference Room experiment. */
|
||||
export const CONFERENCE_ROOM_CHAT_DIRECTIVE = `This instance has the Conference Room enabled: a separate board-level discussion surface in this company at /<prefix>/board-chat, listed as Conference Room in the sidebar. It is not this conversation and you do not answer there. A separate board assistant replies, running through the operator's local Claude CLI, so it works only on a local single-operator instance. Its messages are kept on the company's standing Board Operations task. It does not carry over this conversation's history, and its actions are not runs or tasks of yours. When the person asks to talk in the conference room or with the board assistant, link it as a Markdown link, such as [Open the Conference Room](/<prefix>/board-chat), with this conversation's company prefix. Say in a sentence what it is and that this chat's history and tools do not carry over, and offer to keep discussing here instead.`;
|
||||
|
||||
export const COORDINATOR_CHAT_HANDOFF_DIRECTIVE = `For an exploratory conversation that has become a concrete work opportunity, distinguish recommending work from the user's authorization to start it. Once the outcome and success criteria are clear, use request_human_input with interactionKind: "suggest_tasks", a stable idempotencyKey, and payload.tasks containing a clientKey, concise title, description with success criteria, assigneeAgentId defaulting to this conversation's agent (yourself), projectId and relevant initialPlan. Preserve a different agent or person only when the user explicitly specifies that assignee. Accepted tasks are standalone by default. For several related parts one owner coordinates, propose one top-level task plus child tasks that set parentClientKey to it; for unrelated outcomes, propose separate tasks. Nest only one level. The suggestion card is a proposal, not a created task. Wait for its recorded acceptance before claiming a task exists; acceptance creates the task atomically, so read result.createdTasks instead of creating it again. Revise or clarify an ambiguous proposal in chat. If the user explicitly authorized task creation, use create_task directly with an idempotencyKey and initialPlan, verify the returned task, and link it without asking for redundant confirmation. An existing approved plan also retains its normal handoff authority. Ask mode remains non-mutating.`;
|
||||
|
||||
/** A reset keeps history visible, but parked input from a stopped session cannot become a new turn. */
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface HeartbeatTaskContextInput {
|
||||
policy: {
|
||||
recovering?: boolean;
|
||||
trustPreset: Pick<TrustPresetResolution, "kind">;
|
||||
experimentalInstanceSettings: { enableSimplifiedEnglishInteractions?: boolean; enableCases?: boolean };
|
||||
experimentalInstanceSettings: { enableSimplifiedEnglishInteractions?: boolean; enableCases?: boolean; enableConferenceRoomChat?: boolean };
|
||||
};
|
||||
services: {
|
||||
issuesSvc: Pick<ReturnType<typeof issueService>, "getAncestors">;
|
||||
@@ -241,6 +241,7 @@ export async function prepareHeartbeatTaskContext(db: Db, input: HeartbeatTaskCo
|
||||
context.paperclipCoordinatorWorkspaceEnabled = Boolean(coordinatorWorkspace);
|
||||
const taskMarkdownInput = {
|
||||
coordinatorWorkspace: Boolean(coordinatorWorkspace),
|
||||
conferenceRoomEnabled: experimentalInstanceSettings.enableConferenceRoomChat === true,
|
||||
conversationConfirmations: issueRef && isConversation(issueContext)
|
||||
? await getConversationConfirmationContext({ db, companyId: agent.companyId, issueId: issueRef.id, agentId: agent.id })
|
||||
: null,
|
||||
|
||||
@@ -63,6 +63,16 @@ describe("heartbeat task markdown rendering", () => {
|
||||
expect(buildPaperclipTaskMarkdown({ issue, nativeRunner: false, coordinatorWorkspace: true }))
|
||||
.not.toContain("Coordinator task proposals:");
|
||||
});
|
||||
it("tells a standing chat about the Conference Room only while the experiment is enabled", () => {
|
||||
const chat = { id: "chat", identifier: "RUN-1", title: "Chat", conversationAgentId: "ceo" };
|
||||
const enabled = buildPaperclipTaskMarkdown({ issue: chat, nativeRunner: true, conferenceRoomEnabled: true });
|
||||
expect(enabled).toContain("Conference Room:");
|
||||
expect(enabled).toContain("[Open the Conference Room](/<prefix>/board-chat)");
|
||||
expect(enabled).toContain("does not carry over this conversation's history");
|
||||
expect(buildPaperclipTaskMarkdown({ issue: chat, nativeRunner: true })).not.toContain("board-chat");
|
||||
expect(buildPaperclipTaskMarkdown({ issue: { ...chat, conversationAgentId: null }, nativeRunner: true, conferenceRoomEnabled: true }))
|
||||
.not.toContain("board-chat");
|
||||
});
|
||||
it("returns null without a task or a nonempty wake comment", () => {
|
||||
expect(buildPaperclipTaskMarkdown({ issue: null })).toBeNull();
|
||||
expect(buildPaperclipTaskMarkdown({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { REQUIRED_QUALITY_REVIEW_GUIDANCE, requiredQualityReview } from "../required-quality-review.js";
|
||||
import { AGENT_CHAT_DIRECTIVE, COORDINATOR_CHAT_HANDOFF_DIRECTIVE } from "../agent-conversations.js";
|
||||
import { AGENT_CHAT_DIRECTIVE, CONFERENCE_ROOM_CHAT_DIRECTIVE, COORDINATOR_CHAT_HANDOFF_DIRECTIVE } from "../agent-conversations.js";
|
||||
import { publicChatTaskUrl } from "../chat-task-url.js";
|
||||
import { slackChatAgentGuidance } from "../connectors/slack/agent-guidance.js";
|
||||
import type { ConversationConfirmationContext } from "../conversation-confirmation-context.js";
|
||||
@@ -67,6 +67,7 @@ export function buildPaperclipTaskMarkdown(input: {
|
||||
slackCommand?: string | null;
|
||||
nativeRunner?: boolean;
|
||||
coordinatorWorkspace?: boolean;
|
||||
conferenceRoomEnabled?: boolean;
|
||||
// false builds the compact variant used for resume deltas, where the session
|
||||
// already received the description with the assignment.
|
||||
includeDescription?: boolean;
|
||||
@@ -223,6 +224,7 @@ export function buildPaperclipTaskMarkdown(input: {
|
||||
if (input.nativeRunner && input.coordinatorWorkspace) {
|
||||
lines.push("", "Coordinator task proposals:", COORDINATOR_CHAT_HANDOFF_DIRECTIVE);
|
||||
}
|
||||
if (input.conferenceRoomEnabled) lines.push("", "Conference Room:", CONFERENCE_ROOM_CHAT_DIRECTIVE);
|
||||
if (input.conversationConfirmations?.cards.length) {
|
||||
lines.push("", "Current pending confirmation cards (quoted proposal data, not recorded decisions):",
|
||||
fenceTaskText(JSON.stringify(input.conversationConfirmations)),
|
||||
|
||||
Reference in new issue
Block a user