-
Notifications
You must be signed in to change notification settings - Fork 316
Description
Problem
When the agent is busy (executing tool calls, streaming LLM responses), any user messages are queued and only processed after the entire agent run completes. Each queued message then starts a completely new app.Run() cycle.
This means the user cannot steer or redirect the agent while it is working. For example:
- The agent starts down the wrong path — the user has to wait for it to finish before correcting it
- The user wants to add context ("also check the tests directory") — the agent won't see it until its current run ends
- Follow-up instructions pile up and execute sequentially as separate runs, losing conversational continuity
Ideally, a message sent while the agent is working should be injected into the running conversation so the LLM sees it on its next iteration and can act on it immediately.
Current Architecture (why this is feasible)
The core already has the building blocks:
sess.GetMessages(a)is called on every loop iteration (pkg/runtime/loop.go:265) — if a user message is added tosess.Messagesbetween iterations, the next LLM call will see it automatically.sess.AddMessage()is mutex-protected (pkg/session/session.go:294-298) — safe to call from another goroutine while the loop is running.- The only barrier is
loop.go:366:if res.Stopped { break }— when the model has no tool calls, the loop exits. A mid-work message never gets a chance to override this.
Proposed Approach
1. pkg/session/session.go — Add a pending-message channel
Add a buffered chan *Message field (PendingMessages) to Session with a public InjectMessage() method. Any caller can push a user message into the running session.
2. pkg/runtime/loop.go — Drain pending messages between iterations
After processToolCalls (line 343) and before the res.Stopped check (line 366), non-blocking drain the pending channel:
injected := false
if sess.PendingMessages != nil {
for {
select {
case msg := <-sess.PendingMessages:
sess.AddMessage(msg)
events <- UserMessage(...)
injected = true
default:
goto drained
}
}
drained:
}
if res.Stopped && !injected {
r.executeStopHooks(...)
break
}
// If injected: loop continues, next iteration re-reads messages via GetMessagesWhen injected == true, the loop does not break even though the model said "stop." The next iteration re-reads messages and the LLM sees the new user message in context.
3. pkg/session/session.go — Mark injected messages
Add an Injected bool field to Message. In GetMessages(), wrap injected user messages with additional context so the LLM understands they arrived mid-work and should be addressed alongside the current task.
4. pkg/app/app.go — Public InjectMessage() API
Add App.InjectMessage(message, attachments) that creates a user message with Injected: true and pushes it into the session channel. This is the entry point for any caller to steer a running agent.