Let bots take in messages sent while they reply
A reply read the session once at the start, so a message sent while it ran was never seen, and the follow-up reply queued for it then finished silently: the message was saved before the reply, so the thread ended with the bot. Those messages went unanswered. The text loop now reads new user messages after every round of tools and before an answer goes out, and the bot decides from what each one means whether it changes the work (folded in, or the unsent answer redrafted) or is a separate request, which it acknowledges at once with the new send_message tool and does after the current work. Messages sent that way are marked interim, so the thinking row stays up while the bot works. A reply job records the newest user message it read, and a queued reply only stays silent when a finished reply already read everything. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import sendMessageTool from "./send-message-tool";
|
||||
|
||||
const signal = new AbortController().signal;
|
||||
|
||||
function tool() {
|
||||
const posted: string[] = [];
|
||||
const sent: number[] = [];
|
||||
const send = sendMessageTool({
|
||||
bot_id: 2,
|
||||
session_id: 100,
|
||||
post: async ({ text }) => {
|
||||
posted.push(text);
|
||||
return { id: posted.length };
|
||||
},
|
||||
onSend: ({ message_id }) => sent.push(message_id),
|
||||
});
|
||||
|
||||
return { send, posted, sent };
|
||||
}
|
||||
|
||||
describe("sendMessageTool", () => {
|
||||
test("posts the message now and tells the bot to carry on", async () => {
|
||||
const { send, posted, sent } = tool();
|
||||
|
||||
const result = await send.run({ args: { text: "Noted, I'll do that once this is done." }, signal });
|
||||
|
||||
expect(posted).toEqual(["Noted, I'll do that once this is done."]);
|
||||
expect(sent).toEqual([1]);
|
||||
expect(result).toContain("Carry on");
|
||||
});
|
||||
|
||||
test("stops at the cap, so a reply can't flood the chat", async () => {
|
||||
const { send, posted } = tool();
|
||||
|
||||
for (let i = 0; i < AppData.BotReplyInterimMessagesMax; i++) {
|
||||
await send.run({ args: { text: `Heads-up ${i}` }, signal });
|
||||
}
|
||||
|
||||
const error = await Promise.resolve(send.run({ args: { text: "One more" }, signal })).catch(
|
||||
(e) => e,
|
||||
);
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(posted).toHaveLength(AppData.BotReplyInterimMessagesMax);
|
||||
});
|
||||
|
||||
test("refuses an empty message", () => {
|
||||
expect(tool().send.schema?.safeParse({ text: " " }).success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { z } from "zod";
|
||||
import pushBotNotice from "@/src/bots-queue/push-bot-notice";
|
||||
import { AppData } from "@/src/data/app-data";
|
||||
import insertBotMessage from "@/src/functions/backend/db/bots/insert-bot-message";
|
||||
import type { HarnessTool } from "../../types";
|
||||
|
||||
type Params = {
|
||||
bot_id: number;
|
||||
/**
|
||||
* The session being answered: the message goes in it.
|
||||
*/
|
||||
session_id: number;
|
||||
workspace_id?: number | null;
|
||||
/**
|
||||
* Called with each message sent, so the reply knows it said something.
|
||||
*/
|
||||
onSend?: (params: { message_id: number }) => void;
|
||||
/**
|
||||
* Saves and pushes the message (`insertBotMessage`, `pushBotNotice`).
|
||||
*/
|
||||
post?: (params: { text: string }) => Promise<{ id?: number | "" }>;
|
||||
};
|
||||
|
||||
type Args = {
|
||||
text: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lets a reply post a message in the conversation now, without ending the
|
||||
* run: the bot acknowledges something the user wrote while it works ("Noted,
|
||||
* I'll do that once this is done") and carries on. The message is marked
|
||||
* `interim`, so the conversation keeps showing the bot at work, and is
|
||||
* pushed at once. At most `AppData.BotReplyInterimMessagesMax` per run.
|
||||
*/
|
||||
export default function sendMessageTool({
|
||||
bot_id,
|
||||
session_id,
|
||||
workspace_id,
|
||||
onSend,
|
||||
post = async ({ text }) => {
|
||||
const message = await insertBotMessage({ bot_id, session_id, workspace_id, text, interim: true });
|
||||
|
||||
await pushBotNotice({ bot_id, message });
|
||||
|
||||
return message;
|
||||
},
|
||||
}: Params): HarnessTool<Args> {
|
||||
let sent = 0;
|
||||
|
||||
return {
|
||||
name: "send_message",
|
||||
description: `Send the user a message now and keep working: your reply still comes when you finish. Use it to acknowledge something they wrote while you were working that is separate from what you're doing (say you'll get to it, then carry on), or for a short heads-up on long work. Not for your answer, and not for something that changes what you're doing: take that into account instead. At most ${AppData.BotReplyInterimMessagesMax} per reply.`,
|
||||
schema: z.object({
|
||||
text: z.string().trim().min(1).max(2000).describe(`The message (markdown), short`),
|
||||
}),
|
||||
run: async ({ args }) => {
|
||||
if (sent >= AppData.BotReplyInterimMessagesMax) {
|
||||
throw new Error(
|
||||
`You've sent ${sent} messages in this reply already. Carry on, and put the rest in your answer.`,
|
||||
);
|
||||
}
|
||||
|
||||
const message = await post({ text: args.text });
|
||||
|
||||
sent++;
|
||||
onSend?.({ message_id: Number(message.id) });
|
||||
|
||||
return `Sent. Carry on with what you were doing.`;
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user