Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
113 changes: 113 additions & 0 deletions src/core/task/__tests__/mid-task-message-injection.spec.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
Loading