From 69db1b2f269ca6fbfdd59713561c7c464f89e8c7 Mon Sep 17 00:00:00 2001 From: shdomi8599 Date: Mon, 2 Mar 2026 09:12:16 +0900 Subject: [PATCH] feat: inject queued messages into next API call for mid-task guidance --- src/core/task/Task.ts | 25 ++++ .../mid-task-message-injection.spec.ts | 113 ++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 src/core/task/__tests__/mid-task-message-injection.spec.ts diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 1d4320493a0..6c35c9a26df 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -2639,6 +2639,31 @@ export class Task extends EventEmitter implements TaskLike { // Add environment details as its own text block, separate from tool // results. let finalUserContent = [...contentWithoutEnvDetails, { type: "text" as const, text: environmentDetails }] + + // Inject any queued user messages into this API call. + // This allows users to provide mid-task guidance that the agent + // picks up immediately on the next API request, rather than + // waiting for the current turn to fully complete. + if (!this.messageQueueService.isEmpty()) { + const queuedTexts: string[] = [] + + while (!this.messageQueueService.isEmpty()) { + const msg = this.messageQueueService.dequeueMessage() + + if (msg?.text) { + queuedTexts.push(msg.text) + } + } + + if (queuedTexts.length > 0) { + const injection = + "\n[USER_MID_TASK_MESSAGE]\nThe user has sent the following message(s) while you were working. " + + "Please acknowledge and incorporate this feedback into your current task:\n" + + queuedTexts.join("\n") + + "\n[/USER_MID_TASK_MESSAGE]" + finalUserContent.push({ type: "text" as const, text: injection }) + } + } // Only add user message to conversation history if: // 1. This is the first attempt (retryAttempt === 0), AND // 2. The original userContent was not empty (empty signals delegation resume where diff --git a/src/core/task/__tests__/mid-task-message-injection.spec.ts b/src/core/task/__tests__/mid-task-message-injection.spec.ts new file mode 100644 index 00000000000..f9d92973cdf --- /dev/null +++ b/src/core/task/__tests__/mid-task-message-injection.spec.ts @@ -0,0 +1,113 @@ +import { MessageQueueService } from "../../message-queue/MessageQueueService" + +/** + * Tests for the mid-task message injection feature. + * + * When a user queues messages while the agent is working, + * they should be drained and injected into the next API call's + * user content as [USER_MID_TASK_MESSAGE] blocks. + * + * This test validates the drain-and-inject logic in isolation, + * using the same MessageQueueService that Task.ts uses. + */ +describe("Mid-task message injection", () => { + let queue: MessageQueueService + + beforeEach(() => { + queue = new MessageQueueService() + }) + + afterEach(() => { + queue.dispose() + }) + + /** + * Simulates the drain-and-inject logic from Task.ts L2647-2666. + * Returns the injection text block if any messages were queued, + * or undefined if the queue was empty. + */ + function drainAndBuildInjection(mqs: MessageQueueService): string | undefined { + if (mqs.isEmpty()) { + return undefined + } + + const queuedTexts: string[] = [] + + while (!mqs.isEmpty()) { + const msg = mqs.dequeueMessage() + + if (msg?.text) { + queuedTexts.push(msg.text) + } + } + + if (queuedTexts.length === 0) { + return undefined + } + + return ( + "\n[USER_MID_TASK_MESSAGE]\nThe user has sent the following message(s) while you were working. " + + "Please acknowledge and incorporate this feedback into your current task:\n" + + queuedTexts.join("\n") + + "\n[/USER_MID_TASK_MESSAGE]" + ) + } + + it("returns undefined when queue is empty", () => { + const result = drainAndBuildInjection(queue) + expect(result).toBeUndefined() + }) + + it("injects a single queued message with correct tags", () => { + queue.addMessage("use TypeScript instead of JavaScript") + const result = drainAndBuildInjection(queue) + + expect(result).toBeDefined() + expect(result).toContain("[USER_MID_TASK_MESSAGE]") + expect(result).toContain("use TypeScript instead of JavaScript") + expect(result).toContain("[/USER_MID_TASK_MESSAGE]") + }) + + it("injects multiple queued messages in order", () => { + queue.addMessage("first advice") + queue.addMessage("second advice") + queue.addMessage("third advice") + const result = drainAndBuildInjection(queue) + + expect(result).toBeDefined() + expect(result).toContain("first advice") + expect(result).toContain("second advice") + expect(result).toContain("third advice") + + // Verify order: first should appear before second + const firstIndex = result!.indexOf("first advice") + const secondIndex = result!.indexOf("second advice") + const thirdIndex = result!.indexOf("third advice") + expect(firstIndex).toBeLessThan(secondIndex) + expect(secondIndex).toBeLessThan(thirdIndex) + }) + + it("drains the queue completely after injection", () => { + queue.addMessage("some advice") + queue.addMessage("more advice") + + drainAndBuildInjection(queue) + + expect(queue.isEmpty()).toBe(true) + // Second drain should return undefined + expect(drainAndBuildInjection(queue)).toBeUndefined() + }) + + it("does not interfere with messages queued after drain", () => { + queue.addMessage("before drain") + drainAndBuildInjection(queue) + + // Simulate new message arriving after drain (during API call) + queue.addMessage("after drain") + expect(queue.isEmpty()).toBe(false) + + const result = drainAndBuildInjection(queue) + expect(result).toContain("after drain") + expect(result).not.toContain("before drain") + }) +})