Skip to content

Commit 04def0f

Browse files
authored
Merge pull request #1844 from afjcjsbx/fix/scope-steering
fix(agent) scope steering
2 parents 73a683f + 9e34459 commit 04def0f

File tree

4 files changed

+692
-99
lines changed

4 files changed

+692
-99
lines changed

docs/steering.md

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,18 @@ Agent Loop ▼
2121
└─ new LLM turn with steering message
2222
```
2323

24+
## Scoped queues
25+
26+
Steering is now isolated per resolved session scope, not stored in a single
27+
global queue.
28+
29+
- The active turn writes and reads from its own scope key (usually the routed session key such as `agent:<agent_id>:...`)
30+
- `Steer()` still works outside an active turn through a legacy fallback queue
31+
- `Continue()` first dequeues messages for the requested session scope, then falls back to the legacy queue for backwards compatibility
32+
33+
This prevents a message arriving from another chat, DM peer, or routed agent
34+
session from being injected into the wrong conversation.
35+
2436
## Configuration
2537

2638
In `config.json`, under `agents.defaults`:
@@ -86,12 +98,18 @@ if response == "" {
8698

8799
`Continue` internally uses `SkipInitialSteeringPoll: true` to avoid double-dequeuing the same messages (since it already extracted them and passes them directly as input).
88100

101+
`Continue` also resolves the target agent from the provided session key, so
102+
agent-scoped sessions continue on the correct agent instead of always using
103+
the default one.
104+
89105
## Polling points in the loop
90106

91-
Steering is checked at **two points** in the agent cycle:
107+
Steering is checked at the following points in the agent cycle:
92108

93109
1. **At loop start** — before the first LLM call, to catch messages enqueued during setup
94110
2. **After every tool completes** — including the first and the last. If steering is found and there are remaining tools, they are all skipped immediately
111+
3. **After a direct LLM response** — if a new steering message arrived while the model was generating a non-tool response, the loop continues instead of returning a stale answer
112+
4. **Right before the turn is finalized** — if steering arrived at the very end of the turn, the agent immediately starts a continuation turn instead of leaving the message orphaned in the queue
95113

96114
## Why remaining tools are skipped
97115

@@ -156,11 +174,26 @@ When the agent loop (`Run()`) starts processing a message, it spawns a backgroun
156174

157175
- Users on any channel (Telegram, Discord, etc.) don't need to do anything special — their messages are automatically captured as steering when the agent is busy
158176
- Audio messages are transcribed before being steered, so the agent receives text. If transcription fails, the original (non-transcribed) message is steered as-is
177+
- Only messages that resolve to the **same steering scope** as the active turn are redirected. Messages for other chats/sessions are requeued onto the inbound bus so they can be processed normally
178+
- `system` inbound messages are not treated as steering input
159179
- When `processMessage` finishes, the drain goroutine is canceled and normal message consumption resumes
160180

181+
## Steering with media
182+
183+
Steering messages can include `Media` refs, just like normal inbound user
184+
messages.
185+
186+
- The original `media://` refs are preserved in session history via `AddFullMessage`
187+
- Before the next provider call, steering messages go through the normal media resolution pipeline
188+
- Image refs are converted to data URLs for multimodal providers; non-image refs are resolved the same way as standard inbound media
189+
190+
This applies both to in-turn steering and to idle-session continuation through
191+
`Continue()`.
192+
161193
## Notes
162194

163195
- Steering **does not interrupt** a tool that is currently executing. It waits for the current tool to finish, then checks the queue.
164196
- With `one-at-a-time` mode, if multiple messages are enqueued rapidly, they will be processed one per iteration. This gives the model the opportunity to react to each message individually.
165197
- With `all` mode, all pending messages are combined into a single injection. Useful when you want the agent to receive all the context at once.
166198
- The steering queue has a maximum capacity of 10 messages (`MaxQueueSize`). `Steer()` returns an error when the queue is full. In the bus drain path, the error is logged as a warning and the message is effectively dropped.
199+
- Manual `Steer()` calls made outside an active turn still go to the legacy fallback queue, so older integrations keep working.

0 commit comments

Comments
 (0)