feat: inject queued messages into next API call for mid-task guidance

This commit is contained in:
shdomi8599 2026-03-02 09:12:16 +09:00
parent 193053110b
commit 69db1b2f26
2 changed files with 138 additions and 0 deletions

View file

@ -2639,6 +2639,31 @@ export class Task extends EventEmitter<TaskEvents> 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

View file

@ -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")
})
})