@@ -139,6 +139,14 @@ type Runtime interface {
139139 // if the runtime does not support local title generation (e.g. remote runtimes).
140140 TitleGenerator () * sessiontitle.Generator
141141
142+ // Steer enqueues a user message for urgent mid-turn injection into the
143+ // running agent loop. Returns an error if the queue is full or steering
144+ // is not available.
145+ Steer (msg QueuedMessage ) error
146+ // FollowUp enqueues a message for end-of-turn processing. Each follow-up
147+ // gets a full undivided agent turn. Returns an error if the queue is full.
148+ FollowUp (msg QueuedMessage ) error
149+
142150 // Close releases resources held by the runtime (e.g., session store connections).
143151 Close () error
144152}
@@ -201,6 +209,14 @@ type LocalRuntime struct {
201209
202210 currentAgentMu sync.RWMutex
203211
212+ // steerQueue stores urgent mid-turn messages. The agent loop drains
213+ // ALL pending messages after tool execution, before the stop check.
214+ steerQueue MessageQueue
215+
216+ // followUpQueue stores end-of-turn messages. The agent loop pops
217+ // exactly ONE message after the model stops and stop-hooks have run.
218+ followUpQueue MessageQueue
219+
204220 // onToolsChanged is called when an MCP toolset reports a tool list change.
205221 onToolsChanged func (Event )
206222
@@ -228,6 +244,22 @@ func WithTracer(t trace.Tracer) Opt {
228244 }
229245}
230246
247+ // WithSteerQueue sets a custom MessageQueue for mid-turn message injection.
248+ // If not provided, an in-memory buffered queue is used.
249+ func WithSteerQueue (q MessageQueue ) Opt {
250+ return func (r * LocalRuntime ) {
251+ r .steerQueue = q
252+ }
253+ }
254+
255+ // WithFollowUpQueue sets a custom MessageQueue for end-of-turn follow-up
256+ // messages. If not provided, an in-memory buffered queue is used.
257+ func WithFollowUpQueue (q MessageQueue ) Opt {
258+ return func (r * LocalRuntime ) {
259+ r .followUpQueue = q
260+ }
261+ }
262+
231263func WithSessionCompaction (sessionCompaction bool ) Opt {
232264 return func (r * LocalRuntime ) {
233265 r .sessionCompaction = sessionCompaction
@@ -291,6 +323,8 @@ func NewLocalRuntime(agents *team.Team, opts ...Opt) (*LocalRuntime, error) {
291323 currentAgent : defaultAgent .Name (),
292324 resumeChan : make (chan ResumeRequest ),
293325 elicitationRequestCh : make (chan ElicitationResult ),
326+ steerQueue : NewInMemoryMessageQueue (defaultSteerQueueCapacity ),
327+ followUpQueue : NewInMemoryMessageQueue (defaultFollowUpQueueCapacity ),
294328 sessionCompaction : true ,
295329 managedOAuth : true ,
296330 sessionStore : session .NewInMemorySessionStore (),
@@ -1015,6 +1049,26 @@ func (r *LocalRuntime) ResumeElicitation(ctx context.Context, action tools.Elici
10151049 }
10161050}
10171051
1052+ // Steer enqueues a user message for urgent mid-turn injection into the
1053+ // running agent loop. The message will be picked up after the current batch
1054+ // of tool calls finishes but before the loop checks whether to stop.
1055+ func (r * LocalRuntime ) Steer (msg QueuedMessage ) error {
1056+ if ! r .steerQueue .Enqueue (context .Background (), msg ) {
1057+ return errors .New ("steer queue full" )
1058+ }
1059+ return nil
1060+ }
1061+
1062+ // FollowUp enqueues a message to be processed after the current agent turn
1063+ // finishes. Unlike Steer, follow-ups are popped one at a time and each gets
1064+ // a full undivided agent turn.
1065+ func (r * LocalRuntime ) FollowUp (msg QueuedMessage ) error {
1066+ if ! r .followUpQueue .Enqueue (context .Background (), msg ) {
1067+ return errors .New ("follow-up queue full" )
1068+ }
1069+ return nil
1070+ }
1071+
10181072// Run starts the agent's interaction loop
10191073
10201074func (r * LocalRuntime ) startSpan (ctx context.Context , name string , opts ... trace.SpanStartOption ) (context.Context , trace.Span ) {
0 commit comments