diff --git a/packages/adapters/src/executor-instructions.test.ts b/packages/adapters/src/executor-instructions.test.ts index 1c361bc..416cade 100644 --- a/packages/adapters/src/executor-instructions.test.ts +++ b/packages/adapters/src/executor-instructions.test.ts @@ -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: [] }); diff --git a/packages/adapters/src/executor-prompt.test.ts b/packages/adapters/src/executor-prompt.test.ts index 5a04327..71c3f14 100644 --- a/packages/adapters/src/executor-prompt.test.ts +++ b/packages/adapters/src/executor-prompt.test.ts @@ -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"); diff --git a/packages/adapters/src/executor-prompt.ts b/packages/adapters/src/executor-prompt.ts index f917c4b..479f2dd 100644 --- a/packages/adapters/src/executor-prompt.ts +++ b/packages/adapters/src/executor-prompt.ts @@ -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; diff --git a/packages/adapters/src/executor.ts b/packages/adapters/src/executor.ts index 14e5569..2f82ee2 100644 --- a/packages/adapters/src/executor.ts +++ b/packages/adapters/src/executor.ts @@ -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;