diff --git a/internal/agent/loop.go b/internal/agent/loop.go index 436216bc..a37499c2 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -396,6 +396,8 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) // 2g. Cross-session task reminder: notify team leads about pending and in-progress tasks. // Stale recovery (expired lock → pending) is handled by the background TaskTicker. + // Reminders are injected BEFORE the user message so the user's actual message is always + // the last message — prevents trailing assistant messages that proxy providers reject. if l.teamStore != nil && l.agentUUID != uuid.Nil { if team, _ := l.teamStore.GetTeamForAgent(ctx, l.agentUUID); team != nil && team.LeadAgentID == l.agentUUID { if tasks, err := l.teamStore.ListTasks(ctx, team.ID, "newest", "active", req.UserID, "", "", 0, 0); err == nil { @@ -434,16 +436,20 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) } if len(parts) > 0 { reminder := "[System] " + strings.Join(parts, "\n\n") + // Pop user message, inject reminder, push user message back + userMsg := messages[len(messages)-1] + messages = messages[:len(messages)-1] messages = append(messages, providers.Message{Role: "user", Content: reminder}, providers.Message{Role: "assistant", Content: "I see the task status. Let me handle accordingly."}, + userMsg, ) } } } } - // 2g. Member task reminder: inject task context for members working on dispatched tasks. + // 2h. Member task reminder: inject task context for members working on dispatched tasks. // Caches task subject/number for mid-loop progress nudge (avoids extra DB query). var memberTaskSubject string var memberTaskNumber int @@ -457,9 +463,13 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) "Stay focused on this task. Your final response becomes the task result — make it clear and complete. "+ "For long tasks, report progress: team_tasks(action=\"progress\", percent=50, text=\"status\").", task.TaskNumber, task.Subject) + // Pop user message, inject reminder, push user message back + userMsg := messages[len(messages)-1] + messages = messages[:len(messages)-1] messages = append(messages, providers.Message{Role: "user", Content: reminder}, providers.Message{Role: "assistant", Content: "Understood. I'll focus on this task and report progress."}, + userMsg, ) } } diff --git a/internal/providers/openai.go b/internal/providers/openai.go index 49692143..9a1c014a 100644 --- a/internal/providers/openai.go +++ b/internal/providers/openai.go @@ -340,6 +340,17 @@ func (p *OpenAIProvider) buildRequestBody(model string, req ChatRequest, stream msgs = append(msgs, msg) } + // Safety net: strip trailing assistant message to prevent HTTP 400 from + // proxy providers (LiteLLM, OpenRouter) that don't support assistant prefill. + // This should rarely trigger — the agent loop ensures user message is last. + if len(msgs) > 0 { + if role, _ := msgs[len(msgs)-1]["role"].(string); role == "assistant" { + slog.Warn("openai: stripped trailing assistant message (unsupported prefill)", + "provider", p.name, "model", model) + msgs = msgs[:len(msgs)-1] + } + } + body := map[string]any{ "model": model, "messages": msgs,