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
112 lines
4.3 KiB
Go
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)
|
|
}
|