Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
560a716
fix(tools): 重构EmitChunk错误传播与并发语义
Cai-Tang-www Apr 9, 2026
05ad184
fix: 修复流取消误判并补齐覆盖用例
Cai-Tang-www Apr 9, 2026
b574036
fix(test):补充测试覆盖率
creatang Apr 8, 2026
c31d7e9
fix:代码块渲染
creatang Apr 9, 2026
6f9dd4c
fix(tui):固定输入区
creatang Apr 9, 2026
0e4af34
refactor:美化/help界面
creatang Apr 9, 2026
55c08a4
fix(tui): 修复 /help 编码与覆盖率问题(UTF-8)
creatang Apr 9, 2026
8ca48f0
test(tui):补充 update 分支覆盖,修复 codecov patch 覆盖率
creatang Apr 9, 2026
64fb3b1
Merge pull request #205 from Cai-Tang-www/fix/emit-chunk-emitter
phantom5099 Apr 9, 2026
6b5a253
feat(tui/runtime): 打通权限审批选择菜单闭环并增强ask提示词
Cai-Tang-www Apr 10, 2026
29b9702
test(tui): 补齐权限审批闭环分支覆盖并修复布局边界
Cai-Tang-www Apr 10, 2026
14ecb63
feat(runtime): recover with reactive compact on context-too-long
xgopilot Apr 10, 2026
494499b
fix(runtime): address review issues — loop budget, token-reset persis…
xgopilot Apr 10, 2026
c7dc253
fix(tui): close permission ask review gaps
xgopilot Apr 10, 2026
825f225
fix(runtime): rebuild context after auto compact
xgopilot Apr 10, 2026
654adbe
Merge pull request #211 from 1024XEngineer/xgopilot/issue-146-1775790664
minorcell Apr 10, 2026
8c1f663
Merge pull request #210 from Cai-Tang-www/fix/permission-ask-select-c…
minorcell Apr 10, 2026
b381864
fix(tui): address help modal and review regressions
xgopilot Apr 10, 2026
45294f5
fix(runtime/tools): 收敛 EmitChunk 遗留 review 问题
xgopilot Apr 10, 2026
a4a2749
fix(runtime): resolve unresolved EmitChunk reviews
xgopilot Apr 10, 2026
1f7eea9
Merge pull request #215 from 1024XEngineer/xgopilot/issue-213-1775793526
minorcell Apr 10, 2026
8b44672
fix(tui): resolve pr-208 conflicts with main
xgopilot Apr 10, 2026
5736244
fix(test):补充测试覆盖率
creatang Apr 8, 2026
f947e2f
fix:代码块渲染
creatang Apr 9, 2026
6c92d04
fix(tui):固定输入区
creatang Apr 9, 2026
e9b490d
refactor:美化/help界面
creatang Apr 9, 2026
760fa46
fix(tui): 修复 /help 编码与覆盖率问题(UTF-8)
creatang Apr 9, 2026
90391e1
test(tui):补充 update 分支覆盖,修复 codecov patch 覆盖率
creatang Apr 9, 2026
aa1e6d1
fix(test): rebase后对齐权限提示测试
creatang Apr 10, 2026
144e499
merge: resolve conflicts with remote branch
creatang Apr 10, 2026
3fe66f0
fix: address PR review regressions
xgopilot Apr 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions docs/context-compact.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@

## 概览

- runtime 当前仅接入手动触发的 compact,不包含自动 compact。
- `internal/context/compact` 已支持 `manual` 与 `reactive` 两种 mode,供 runtime 后续在 provider 上下文过长错误场景接入调用
- runtime 已接入手动 compact、基于 token 阈值的自动 compact,以及 provider 上下文过长后的 `reactive` compact 自动恢复
- `internal/context/compact` 支持 `manual` 与 `reactive` 两种 mode。
- 用户通过 `/compact` 对当前会话执行一次上下文压缩。
- compact 前会先写入完整 transcript,随后生成并校验 compact summary,再回写会话消息。

Expand Down Expand Up @@ -69,7 +69,12 @@ context:
3. 生成并校验 `[compact_summary]`。
4. 返回压缩后的消息与 transcript 元信息。

当前 runtime 主链尚未自动调用 `reactive` mode;后续接入时可继续复用现有 compact 事件,并通过 `trigger_mode=reactive` 区分。
当 provider 返回“上下文过长”错误时,runtime 会:

1. 识别 provider 归一化后的 typed error,必要时回退到错误文本匹配。
2. 触发一次 `compact.Run(mode=reactive)`。
3. 继续复用 `compact_start`、`compact_done`、`compact_error` 事件,并通过 `trigger_mode=reactive` 区分来源。
4. 每次 `Run()` 最多只执行一次 reactive 重试,避免无限循环。

## 摘要协议

Expand Down Expand Up @@ -106,7 +111,7 @@ constraints:

## 事件

manual compact 相关 runtime 事件包括:
compact 相关 runtime 事件包括:

- `compact_start`
- `compact_done`
Expand Down
13 changes: 9 additions & 4 deletions docs/runtime-provider-event-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
- `tool_result`
- `error`
- `token_usage`
- `compact_start`
- `compact_done`
- `compact_error`

## ReAct 主循环

Expand All @@ -18,10 +21,12 @@
3. 读取最新配置快照。
4. 解析当前 provider 配置并构建 provider 实例。
5. 调用 `context.Builder` 生成本轮请求使用的 `system prompt` 和消息上下文。
6. 调用 `Provider.Chat`,并把流式事件桥接给 TUI。
7. 保存 assistant 完整回复。
8. 执行返回的工具调用,并保存每一个工具结果。
9. 如果仍需继续推理,则进入下一轮;否则结束。
6. 如命中 token 阈值自动压缩建议,则先执行一次 compact,再继续构造请求。
7. 调用 `Provider.Chat`,并把流式事件桥接给 TUI。
8. 如 provider 返回“上下文过长”错误,则触发一次 `reactive` compact,并仅重试一次当前请求。
9. 保存 assistant 完整回复。
10. 执行返回的工具调用,并保存每一个工具结果。
11. 如果仍需继续推理,则进入下一轮;否则结束。

### Context Builder 输入与职责

Expand Down
4 changes: 3 additions & 1 deletion internal/context/compact/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ type Mode string
const (
// ModeManual runs the explicit user-triggered compact flow.
ModeManual Mode = "manual"
// ModeAuto runs the token-threshold-triggered compact flow.
ModeAuto Mode = "auto"
// ModeReactive runs the provider-error-triggered compact flow.
ModeReactive Mode = "reactive"
)
Expand Down Expand Up @@ -111,7 +113,7 @@ func (s *Service) Run(ctx context.Context, input Input) (Result, error) {
return Result{}, err
}

if input.Mode != ModeManual && input.Mode != ModeReactive {
if input.Mode != ModeManual && input.Mode != ModeAuto && input.Mode != ModeReactive {
return Result{}, fmt.Errorf("compact: unsupported mode %q", input.Mode)
}

Expand Down
48 changes: 48 additions & 0 deletions internal/context/compact/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,54 @@ func TestReactiveCompactUsesKeepRecentAndReportsReactiveMode(t *testing.T) {
}
}

func TestAutoCompactUsesManualStrategyAndReportsAutoMode(t *testing.T) {
t.Parallel()

generator := &stubSummaryGenerator{summary: validSemanticSummary()}
runner := NewRunner(generator)
home := t.TempDir()
runner.userHomeDir = func() (string, error) { return home, nil }

messages := []providertypes.Message{
{Role: providertypes.RoleUser, Content: "old requirement"},
{Role: providertypes.RoleAssistant, Content: "old answer"},
{Role: providertypes.RoleUser, Content: "middle request"},
{Role: providertypes.RoleAssistant, Content: "middle answer"},
{Role: providertypes.RoleUser, Content: "recent request"},
{Role: providertypes.RoleAssistant, Content: "recent answer"},
}

result, err := runner.Run(context.Background(), Input{
Mode: ModeAuto,
SessionID: "session-auto",
Workdir: t.TempDir(),
Messages: messages,
Config: config.CompactConfig{
ManualStrategy: config.CompactManualStrategyKeepRecent,
ManualKeepRecentMessages: 4,
MaxSummaryChars: 1200,
},
})
if err != nil {
t.Fatalf("Run() error = %v", err)
}
if !result.Applied {
t.Fatalf("expected auto compact applied")
}
if result.Metrics.TriggerMode != string(ModeAuto) {
t.Fatalf("expected trigger mode %q, got %q", ModeAuto, result.Metrics.TriggerMode)
}
if len(generator.calls) != 1 {
t.Fatalf("expected generator to run once, got %d", len(generator.calls))
}
if generator.calls[0].Mode != ModeAuto {
t.Fatalf("expected summary input mode %q, got %q", ModeAuto, generator.calls[0].Mode)
}
if generator.calls[0].Config.ManualStrategy != config.CompactManualStrategyKeepRecent {
t.Fatalf("expected auto compact to retain manual strategy, got %q", generator.calls[0].Config.ManualStrategy)
}
}

func TestManualCompactKeepRecentProtectsLatestExplicitUserInstruction(t *testing.T) {
t.Parallel()

Expand Down
2 changes: 2 additions & 0 deletions internal/context/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ var defaultPromptSections = []promptSection{
{
title: "Tool Usage",
content: "- Use tools when they reduce uncertainty or are required to complete the task safely.\n" +
"- For risky operations, call the relevant tool first and let the runtime permission layer decide ask/allow/deny.\n" +
"- Do not self-reject a user-requested operation before attempting the proper tool call and permission flow.\n" +
"- Stay within the current workspace unless the user clearly asks for something else.\n" +
"- Do not claim work is done unless the needed files, commands, or verification actually succeeded.",
},
Expand Down
27 changes: 26 additions & 1 deletion internal/context/prompt_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package context

import "testing"
import (
"strings"
"testing"
)

func TestDefaultSystemPromptSectionsReturnsCachedSections(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -90,3 +93,25 @@ func TestComposeSystemPromptSkipsEmptySections(t *testing.T) {
t.Fatalf("composeSystemPrompt() = %q, want %q", got, want)
}
}

func TestDefaultToolUsagePromptEncouragesAskFlow(t *testing.T) {
t.Parallel()

sections := defaultSystemPromptSections()
var toolUsage string
for _, section := range sections {
if section.title == "Tool Usage" {
toolUsage = section.content
break
}
}
if toolUsage == "" {
t.Fatalf("expected Tool Usage section to exist")
}
if !strings.Contains(toolUsage, "permission layer") {
t.Fatalf("expected Tool Usage to mention permission layer, got %q", toolUsage)
}
if !strings.Contains(toolUsage, "Do not self-reject") {
t.Fatalf("expected Tool Usage to discourage self-reject, got %q", toolUsage)
}
}
101 changes: 92 additions & 9 deletions internal/provider/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"net/http"
"strings"
)

// 通用领域错误。
Expand All @@ -20,17 +21,41 @@ var (
type ProviderErrorCode string

const (
ErrorCodeAuthFailed ProviderErrorCode = "auth_failed" // 认证失败(401)
ErrorCodeForbidden ProviderErrorCode = "forbidden" // 权限不足(403)
ErrorCodeNotFound ProviderErrorCode = "not_found" // 资源不存在(404)
ErrorCodeClient ProviderErrorCode = "client_error" // 客户端请求错误(4xx,排除上述分类)
ErrorCodeRateLimit ProviderErrorCode = "rate_limited" // 限流(429)
ErrorCodeServer ProviderErrorCode = "server_error" // 服务端错误(5xx)
ErrorCodeTimeout ProviderErrorCode = "timeout" // 超时
ErrorCodeNetwork ProviderErrorCode = "network_error" // 网络错误(连接拒绝、DNS 失败等)
ErrorCodeUnknown ProviderErrorCode = "unknown" // 未知错误
ErrorCodeAuthFailed ProviderErrorCode = "auth_failed" // 认证失败(401)
ErrorCodeForbidden ProviderErrorCode = "forbidden" // 权限不足(403)
ErrorCodeNotFound ProviderErrorCode = "not_found" // 资源不存在(404)
ErrorCodeClient ProviderErrorCode = "client_error" // 客户端请求错误(4xx,排除上述分类)
ErrorCodeRateLimit ProviderErrorCode = "rate_limited" // 限流(429)
ErrorCodeServer ProviderErrorCode = "server_error" // 服务端错误(5xx)
ErrorCodeTimeout ProviderErrorCode = "timeout" // 超时
ErrorCodeNetwork ProviderErrorCode = "network_error" // 网络错误(连接拒绝、DNS 失败等)
ErrorCodeContextTooLong ProviderErrorCode = "context_too_long" // 上下文超出模型窗口
ErrorCodeUnknown ProviderErrorCode = "unknown" // 未知错误
)

var contextTooLongFragments = []string{
"context length",
"context_length_exceeded",
"context window",
"maximum context length",
"maximum prompt length",
"prompt is too long",
}

var contextTokenFragments = []string{
"requested too many tokens",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"requested too many tokens" / "too many tokens" are too broad for context_too_long detection. Providers also use those phrases for bad-request cases like oversized max_tokens / requested output, which compaction cannot fix. In those cases this PR will route the error into the new reactive-compact retry path instead of surfacing the real configuration problem.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new "requested too many tokens" / "too many tokens" fragments are broad enough to catch ordinary 400 validation errors that are not caused by prompt/context size (for example, asking for too many completion tokens). Those requests will now be upgraded to context_too_long and sent through the reactive-compact retry path even though compact cannot fix them.

"too many tokens",
}

var contextTokenHints = []string{
"context",
"prompt",
"message",
"input",
"history",
"window",
}

// ProviderError 是 provider 层的领域错误类型。
type ProviderError struct {
StatusCode int // HTTP 状态码,0 表示非 HTTP 错误(如网络超时)
Expand Down Expand Up @@ -78,6 +103,13 @@ func classifyStatus(statusCode int) ProviderErrorCode {
// NewProviderErrorFromStatus 根据 HTTP 状态码和消息构造 ProviderError。
func NewProviderErrorFromStatus(statusCode int, message string) *ProviderError {
code := classifyStatus(statusCode)
// Only elevate to context_too_long for generic client errors (e.g. 400/413).
// Do not override specific classifications such as rate_limited (429) even if
// the message contains token-count fragments, which would mis-route throttling
// errors into the reactive-compact path.
if code == ErrorCodeClient && matchesContextTooLong(message) {
code = ErrorCodeContextTooLong
}
return &ProviderError{
StatusCode: statusCode,
Code: code,
Expand Down Expand Up @@ -105,3 +137,54 @@ func NewTimeoutProviderError(message string) *ProviderError {
Retryable: true, // 超时默认可重试
}
}

// IsContextTooLong 判断 provider 错误是否表示请求上下文超出模型窗口。
// 优先识别 typed error,必要时再回退到消息文本匹配,兼容不同厂商或额外包装层。
// 已被归类为 rate_limited (429) 的错误不会因文本片段而被误判为 context_too_long。
func IsContextTooLong(err error) bool {
if err == nil {
return false
}

var pErr *ProviderError
if errors.As(err, &pErr) {
if pErr.Code == ErrorCodeContextTooLong {
return true
}
// Skip text fallback for errors that are already classified as a specific
// non-context error (e.g. rate_limited). Token-count fragments in 429
// messages must not route the runtime into the reactive-compact path.
if pErr.Code == ErrorCodeRateLimit {
return false
}
if matchesContextTooLong(pErr.Message) {
return true
}
}

return matchesContextTooLong(err.Error())
}

// matchesContextTooLong 统一收敛常见“上下文过长”报错片段,减少 runtime 对厂商文案的感知。
func matchesContextTooLong(message string) bool {
normalized := strings.ToLower(strings.TrimSpace(message))
if normalized == "" {
return false
}
for _, fragment := range contextTooLongFragments {
if strings.Contains(normalized, fragment) {
return true
}
}
for _, fragment := range contextTokenFragments {
if !strings.Contains(normalized, fragment) {
continue
}
for _, hint := range contextTokenHints {
if strings.Contains(normalized, hint) {
return true
}
}
}
return false
}
Loading
Loading