diff --git a/docs/design/steering-spec.md b/docs/design/steering-spec.md
index 0951bf864..33e882bb1 100644
--- a/docs/design/steering-spec.md
+++ b/docs/design/steering-spec.md
@@ -159,7 +159,7 @@ sequenceDiagram
AgentLoop-->>runLLMIteration: [] (empty, or messages)
alt pendingMessages not empty
- runLLMIteration->>runLLMIteration: inject into messages[]
save to session
+ runLLMIteration->>runLLMIteration: inject into messages[]
(saved to session on delivery)
end
runLLMIteration->>LLM: Chat(messages, tools)
@@ -214,6 +214,11 @@ These results are:
This ensures the LLM knows which of its requested actions were not performed.
+> **Note:** the assistant reply that triggered the steering is saved to session
+> history only after confirmed channel delivery (`OnDelivered`). Skipped tool
+> results are saved synchronously as part of the turn because they are
+> intermediate state within the same turn, not a final outbound response.
+
### Loop condition change
The iteration loop condition was changed from:
diff --git a/docs/steering.md b/docs/steering.md
index 63294ac5f..05a6ccdfc 100644
--- a/docs/steering.md
+++ b/docs/steering.md
@@ -102,6 +102,12 @@ if response == "" {
agent-scoped sessions continue on the correct agent instead of always using
the default one.
+> **Note on session persistence:** the assistant reply produced by `Continue`
+> is saved to session history only after the outbound message is confirmed
+> delivered by the channel (via the internal `OnDelivered` callback). If the
+> agent loop processes a fast follow-up before delivery completes, the pending
+> reply is still visible in LLM context via the `pendingDeliveries` mechanism.
+
## Polling points in the loop
Steering is checked at the following points in the agent cycle:
diff --git a/pkg/agent/steering.go b/pkg/agent/steering.go
index 9fbc718d6..c10ad7b3e 100644
--- a/pkg/agent/steering.go
+++ b/pkg/agent/steering.go
@@ -355,6 +355,18 @@ func (al *AgentLoop) continueResponse(
return al.continueWithSteeringMessages(ctx, agent, sessionKey, channel, chatID, steeringMsgs)
}
+// Continue is the public API for resuming an idle agent from external callers.
+// It dequeues pending steering messages and runs them through the agent loop,
+// returning the assistant reply content on success.
+// If no steering messages are pending, it returns an empty string.
+func (al *AgentLoop) Continue(ctx context.Context, sessionKey, channel, chatID string) (string, error) {
+ resp, err := al.continueResponse(ctx, sessionKey, channel, chatID)
+ if err != nil {
+ return "", err
+ }
+ return resp.Content, nil
+}
+
func (al *AgentLoop) InterruptGraceful(hint string) error {
ts := al.getAnyActiveTurnState()
if ts == nil {