agent: auto-nudge when model narrates intent without acting
When the model emits a text-only turn containing narration phrases
("let me", "i'll", "i will", etc.) with no tool calls, the loop now
injects an ephemeral "Continue. Act now." user message and runs another
step rather than treating the turn as completion. Capped at 2 nudges
per run to prevent infinite loops. UI shows "[nudge: continuing…]" so
the user can see what happened.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ab575ee385
commit
76c5e696d5
|
|
@ -12,6 +12,29 @@ import (
|
|||
)
|
||||
|
||||
const maxRateLimitRetries = 3
|
||||
const maxNudges = 2
|
||||
|
||||
// nudgeMsg is injected as a user turn when the model narrates intent without
|
||||
// acting. It is ephemeral — added to the history slice for the next ChatStream
|
||||
// call but never persisted to State, so it won't appear in future sessions.
|
||||
const nudgeMsg = "Continue. Use execute_code now — do not describe what you will do."
|
||||
|
||||
// narrationPhrases are case-insensitive prefixes/substrings that indicate the
|
||||
// model is narrating intent rather than acting.
|
||||
var narrationPhrases = []string{
|
||||
"let me",
|
||||
"i'll ",
|
||||
"i will ",
|
||||
"i'm going to",
|
||||
"i am going to",
|
||||
"i need to",
|
||||
"first, i",
|
||||
"first i'll",
|
||||
"now i'll",
|
||||
"now i will",
|
||||
"to do this",
|
||||
"i'll now",
|
||||
}
|
||||
|
||||
type ToolExecutor func(name string, args json.RawMessage) (string, error)
|
||||
type OutputFn func(msg OutputMsg)
|
||||
|
|
@ -39,11 +62,19 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
maxSteps = 1
|
||||
}
|
||||
|
||||
var pendingNudge string
|
||||
nudgeCount := 0
|
||||
|
||||
for step := range maxSteps {
|
||||
history := state.History()
|
||||
if cfg.SystemPrompt != "" {
|
||||
history = append([]backend.Message{{Role: "system", Content: cfg.SystemPrompt}}, history...)
|
||||
}
|
||||
// Inject nudge from previous step if the model narrated instead of acted.
|
||||
if pendingNudge != "" {
|
||||
history = append(history, backend.Message{Role: "user", Content: pendingNudge})
|
||||
pendingNudge = ""
|
||||
}
|
||||
|
||||
// Stream the assistant's response, retrying on HTTP 429.
|
||||
var ch <-chan backend.StreamEvent
|
||||
|
|
@ -130,6 +161,14 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
}
|
||||
|
||||
if len(toolCalls) == 0 {
|
||||
// If the model narrated intent without acting, nudge it to continue
|
||||
// rather than treating the turn as completion.
|
||||
if nudgeCount < maxNudges && isNarration(content.String()) {
|
||||
nudgeCount++
|
||||
pendingNudge = nudgeMsg
|
||||
emit(cfg, OutputMsg{Role: "nudge", Content: nudgeMsg})
|
||||
continue
|
||||
}
|
||||
if err := state.MarkComplete(); err != nil {
|
||||
return fmt.Errorf("mark complete: %w", err)
|
||||
}
|
||||
|
|
@ -143,6 +182,23 @@ func Run(ctx context.Context, cfg Config, state State) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// isNarration returns true when text looks like the model is describing what it
|
||||
// is about to do rather than doing it. Only short responses are considered —
|
||||
// a longer response likely contains actual content or analysis.
|
||||
func isNarration(text string) bool {
|
||||
text = strings.TrimSpace(text)
|
||||
if text == "" || len(text) > 800 {
|
||||
return false
|
||||
}
|
||||
lower := strings.ToLower(text)
|
||||
for _, phrase := range narrationPhrases {
|
||||
if strings.Contains(lower, phrase) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func emit(cfg Config, msg OutputMsg) {
|
||||
if cfg.Output != nil {
|
||||
cfg.Output(msg)
|
||||
|
|
|
|||
4
main.go
4
main.go
|
|
@ -468,6 +468,10 @@ func (m *model) apply(am agentMsg) {
|
|||
}
|
||||
m.display = append(m.display, "= "+s)
|
||||
|
||||
case "nudge":
|
||||
// Model narrated without acting; loop injected a continuation prompt.
|
||||
m.display = append(m.display, "[nudge: continuing…]")
|
||||
|
||||
case "retry":
|
||||
m.state = agentRetrying
|
||||
if secs, err := strconv.Atoi(am.content); err == nil {
|
||||
|
|
|
|||
Reference in New Issue