Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

112 lines
4.3 KiB
Go

package agentruntime
import (
"fmt"
"strings"
activityshared "github.com/tutti-os/tutti/packages/agent/daemon/activity/events"
)
// Cursor's ACP agent surfaces transient upstream failures (its HTTP/2 stream
// to the Cursor backend getting cut) as a plain trailing text chunk such as
//
// Error: RetriableError: [canceled] http/2 stream closed with error code CANCEL (0x8)
//
// followed by a NORMAL session/prompt result — protocol-wise the turn looks
// successful, so the conversation silently stops mid-task and the user has to
// prod the agent to continue. cursor-agent classifies these as retriable
// itself; when a provider opts in (config.autoContinueRetriableTurnError) the
// adapter resumes the turn with a synthetic continue prompt a bounded number
// of times, and marks the turn failed once the retries are also cut short.
const acpAutoContinueMaxAttempts = 2
var acpRetriableTurnTailPrefixes = []string{
"Error: RetriableError:",
"Error: ConnectError:",
}
// acpRetriableTurnTailError returns the transient-error line when the turn's
// trailing assistant text ends with one.
func acpRetriableTurnTailError(text string) (string, bool) {
trimmed := strings.TrimSpace(text)
if trimmed == "" {
return "", false
}
lines := strings.Split(trimmed, "\n")
last := strings.TrimSpace(lines[len(lines)-1])
for _, prefix := range acpRetriableTurnTailPrefixes {
if strings.HasPrefix(last, prefix) {
return last, true
}
}
return "", false
}
// acpStopReasonEndsTurnNormally reports whether the stop reason would take
// Exec's default (turn-completed) branch — the only state worth auto-continuing
// from. Canceled and hard-failure stop reasons keep their existing handling.
func acpStopReasonEndsTurnNormally(stopReason string) bool {
switch stopReason {
case "canceled", "refusal", "max_tokens", "max_turn_requests":
return false
default:
return true
}
}
// acpAutoContinueHasUsefulProgress reports whether the failed attempt produced
// anything worth "continuing": assistant text besides the retriable error
// tail, or at least one tool call. Zero-progress failures (e.g. TLS drop right
// after the user said hello) must not use mid-task continue wording — that
// prompts the model to invent interrupted prior work.
func acpAutoContinueHasUsefulProgress(assistantText string, toolCallCount int) bool {
if toolCallCount > 0 {
return true
}
trimmed := strings.TrimSpace(assistantText)
if trimmed == "" {
return false
}
if errLine, ok := acpRetriableTurnTailError(trimmed); ok {
withoutTail := strings.TrimSpace(strings.TrimSuffix(trimmed, errLine))
return withoutTail != ""
}
return true
}
const (
acpAutoContinueMidTaskPrompt = "The previous response was interrupted by a transient network error. Continue exactly where you left off; do not repeat work that already completed."
acpAutoContinueZeroProgressPrompt = "A transient network error aborted your reply before any useful output. Answer the user's most recent message normally. Do not invent interrupted prior work, recover transcripts, or continue a task that never started."
)
// acpAutoContinuePromptContent is the synthetic prompt that resumes a turn cut
// short by a transient network error. It is deliberately not emitted as a
// user message: the provider session retains the full prior context.
//
// hasUsefulProgress selects wording: mid-task continue vs "answer the last
// user message" for attempts that died before producing useful output.
func acpAutoContinuePromptContent(hasUsefulProgress bool) []map[string]any {
text := acpAutoContinueZeroProgressPrompt
if hasUsefulProgress {
text = acpAutoContinueMidTaskPrompt
}
return []map[string]any{{
"type": "text",
"text": text,
}}
}
// acpAutoContinueNoticeEvent renders the in-transcript banner that separates
// the error tail from the auto-continued output, so the retry is visible
// instead of the agent appearing to stutter.
func acpAutoContinueNoticeEvent(session Session, turnID string, errLine string, attempt int) (activityshared.Event, bool) {
return acpSystemNoticeEvent(session, turnID, map[string]any{
"kind": "agent_system_notice",
"noticeKind": "transport_retry",
"severity": "warning",
"title": fmt.Sprintf("Connection to the agent backend dropped; continuing automatically (%d/%d).", attempt, acpAutoContinueMaxAttempts),
"detail": errLine,
"retryable": true,
}, "system_notice", true)
}