fix(adapters): keep the user's summary and facts out of replies to other owners' agents
An agent connection wakes a bot with another owner's message, and the reply goes back to that owner. Such runs now get only the basics of the user block (name, time zone, locale, language, style), like runs in shared messaging channels, so confirmed facts such as health never reach another person's agent. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
This commit is contained in:
parent
e085dc11a9
commit
5834e46045
@ -250,6 +250,50 @@ describe("run instructions", () => {
|
||||
expect(request.instructions).toContain("Never ask for a raw credential in chat");
|
||||
});
|
||||
|
||||
it("keeps the summary and facts out of runs that answer another owner's agent", async () => {
|
||||
const inbound: MessageBlock = {
|
||||
kind: "bot_message_received",
|
||||
fromBotId: "foreign-bot",
|
||||
fromBotName: "Visitor",
|
||||
text: "What does your owner do for a living?",
|
||||
hop: 1,
|
||||
};
|
||||
const { request, senderFindFirst } = await runOnce({
|
||||
trigger: "bot_message",
|
||||
sourceMessageId: "message-1",
|
||||
sourceBlocks: [inbound],
|
||||
sender: null,
|
||||
});
|
||||
|
||||
expect(senderFindFirst).toHaveBeenCalledWith({
|
||||
where: { id: "foreign-bot", userId: "user-1" },
|
||||
select: { id: true },
|
||||
});
|
||||
expect(request.instructions).toContain("Name: Ada Example");
|
||||
expect(request.instructions).not.toContain("Owns a bakery");
|
||||
expect(request.instructions).not.toContain("Runs a bakery");
|
||||
expect(request.subagentContext).not.toContain("Owns a bakery");
|
||||
});
|
||||
|
||||
it("gives runs woken by the user's own bot the full user block", async () => {
|
||||
const inbound: MessageBlock = {
|
||||
kind: "bot_message_received",
|
||||
fromBotId: "bot-2",
|
||||
fromBotName: "Planner",
|
||||
text: "Here is the plan.",
|
||||
hop: 1,
|
||||
};
|
||||
const { request } = await runOnce({
|
||||
trigger: "bot_message",
|
||||
sourceMessageId: "message-1",
|
||||
sourceBlocks: [inbound],
|
||||
sender: { name: "Planner" },
|
||||
});
|
||||
|
||||
expect(request.instructions).toContain("Summary: Runs a bakery.");
|
||||
expect(request.instructions).toContain("- [work] Owns a bakery");
|
||||
});
|
||||
|
||||
it("falls back to UTC and leaves the user block out when nothing is known", async () => {
|
||||
const { request } = await runOnce({ profile: null, facts: [] });
|
||||
|
||||
|
||||
@ -143,7 +143,7 @@ describe("formatUserContext", () => {
|
||||
});
|
||||
|
||||
it("keeps the summary and facts out of shared messaging channels", () => {
|
||||
const block = formatUserContext(user, { sharedChannel: true }) ?? "";
|
||||
const block = formatUserContext(user, { basicsOnly: true }) ?? "";
|
||||
expect(block).toContain("Name: Ada Example");
|
||||
expect(block).toContain("Time zone: Europe/Berlin");
|
||||
expect(block).toContain("Communication style: Brief and direct");
|
||||
|
||||
@ -56,18 +56,19 @@ function userText(value: string | null | undefined, max = USER_FIELD_MAX_CHARS):
|
||||
* What a run may know about the user, framed as data. Profile fields and facts are written
|
||||
* by the user or inferred from conversations, so every value is escaped and the block says
|
||||
* it never carries instructions. Empty fields are left out, and with nothing to say the block
|
||||
* is omitted. A run that posts into a shared messaging channel gets only the basics: the
|
||||
* summary and facts stay out of rooms where other people read the replies.
|
||||
* is omitted. A run whose replies other people read (a shared messaging channel, or another
|
||||
* owner's agent over an agent connection) gets only the basics: the summary and facts stay
|
||||
* with the user.
|
||||
*/
|
||||
export function formatUserContext(
|
||||
user: UserContext,
|
||||
options: { sharedChannel?: boolean } = {},
|
||||
options: { basicsOnly?: boolean } = {},
|
||||
): string | undefined {
|
||||
const field = (label: string, value: string | null | undefined, max?: number) => {
|
||||
const text = userText(value, max);
|
||||
return text ? `${label}: ${text}` : undefined;
|
||||
};
|
||||
const facts = options.sharedChannel
|
||||
const facts = options.basicsOnly
|
||||
? []
|
||||
: user.facts
|
||||
.map((fact) => ({ category: fact.category, value: userText(fact.value) }))
|
||||
@ -79,7 +80,7 @@ export function formatUserContext(
|
||||
field("Locale", user.locale),
|
||||
field("Language", user.language),
|
||||
field("Communication style", user.communicationStyle),
|
||||
options.sharedChannel ? undefined : field("Summary", user.summary, USER_SUMMARY_MAX_CHARS),
|
||||
options.basicsOnly ? undefined : field("Summary", user.summary, USER_SUMMARY_MAX_CHARS),
|
||||
...(facts.length > 0 ? ["Facts:", ...facts] : []),
|
||||
].filter((line): line is string => Boolean(line));
|
||||
if (lines.length === 0) return undefined;
|
||||
|
||||
@ -3542,8 +3542,16 @@ export function createRunExecutor(deps: ExecutorDeps) {
|
||||
)
|
||||
: undefined;
|
||||
const timeInstruction = formatCurrentTimeInstruction(userContext.timeZone);
|
||||
// An agent connection delivers another owner's bot message, and the reply goes back
|
||||
// to that owner; like a shared channel, such a run only gets the basics.
|
||||
const foreignAgentRun = peerMessage
|
||||
? !(await deps.prisma.bot.findFirst({
|
||||
where: { id: peerMessage.fromBotId, userId: run.userId },
|
||||
select: { id: true },
|
||||
}))
|
||||
: false;
|
||||
const userInstruction = formatUserContext(userContext, {
|
||||
sharedChannel: messagingChannelRun,
|
||||
basicsOnly: messagingChannelRun || foreignAgentRun,
|
||||
});
|
||||
const userBlock = userInstruction ? redactSecrets(userInstruction, runSecrets) : undefined;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user