Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
aa2786b
Merge remote-tracking branch 'upstream/main'
phantom5099 Apr 13, 2026
df2c67d
Merge remote-tracking branch 'upstream/main'
phantom5099 Apr 14, 2026
4108e80
Merge remote-tracking branch 'upstream/main'
phantom5099 Apr 14, 2026
260a2d3
Merge remote-tracking branch 'refs/remotes/upstream/main'
phantom5099 Apr 14, 2026
ede7033
feat(runtime):增加执行监控与死循环拦截
phantom5099 Apr 14, 2026
07a4d3b
fix(runtime): harden terminal event and tool parallelism
xgopilot Apr 14, 2026
974e838
Merge pull request #7 from phantom5099/fork-pr-289-1776160731
phantom5099 Apr 14, 2026
a316884
fix(runtime): align progress streak semantics and reduce permission H…
xgopilot Apr 14, 2026
3a8a8f1
Merge pull request #8 from phantom5099/fork-pr-289-1776160731
phantom5099 Apr 14, 2026
473ede1
fix(runtime): tighten tool execution scheduling and docs alignment
xgopilot Apr 14, 2026
114fa1f
Merge pull request #9 from phantom5099/fork-pr-289-1776165746
phantom5099 Apr 14, 2026
cd2f5d6
fix(runtime): address unresolved review findings and simplify paths
xgopilot Apr 14, 2026
1b7ce02
Merge pull request #10 from phantom5099/fork-pr-289-1776165746
phantom5099 Apr 14, 2026
6f21d32
Merge branch 'main' into main
phantom5099 Apr 14, 2026
269f83d
fix(runtime): repair runtime tests compile errors
xgopilot Apr 14, 2026
c059d4d
Merge pull request #11 from phantom5099/fork-pr-289-1776165746
phantom5099 Apr 14, 2026
5d2a94f
fix(runtime): preserve ErrMaxLoopReached sentinel on loop-limit exit
xgopilot Apr 14, 2026
080ea05
Merge pull request #12 from phantom5099/fork-pr-289-1776165746
phantom5099 Apr 14, 2026
c49c55e
Merge remote-tracking branch 'upstream/main'
phantom5099 Apr 14, 2026
811f93f
Merge remote-tracking branch 'origin/main'
phantom5099 Apr 14, 2026
50df6ce
fix(runtime): resolve conflicts with origin/main semantics
xgopilot Apr 14, 2026
2695aab
Merge pull request #13 from phantom5099/fork-pr-289-1776165746
phantom5099 Apr 14, 2026
1d49f23
Merge remote-tracking branch 'origin/main'
phantom5099 Apr 14, 2026
082c6a2
Merge remote-tracking branch 'upstream/main'
phantom5099 Apr 14, 2026
0c3a8c0
test(runtime,tui): fix compile regression and cover new branches
xgopilot Apr 14, 2026
45df4e4
Merge pull request #14 from phantom5099/fork-pr-289-1776165746
phantom5099 Apr 14, 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
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

## 2. 最高优先级规则
- 不要为了“可能兼容旧版本”破坏当前架构;若新设计已确定,优先直接切换到新实现。
- 不允许过度设计、过度包装
- 项目中可能存在语义不清的地方,必须要谨慎分析
- 不要跨层直连;新功能默认沿 `TUI -> Runtime -> Provider / Tool Manager` 主链路设计。
- 不要把模型厂商差异泄漏到 `runtime`、`tui` 或上层调用方。
- 不要在 `runtime` 或 `tui` 里直接写工具执行逻辑;所有可被模型调用的能力必须进入 `internal/tools`。
Expand Down Expand Up @@ -43,7 +45,6 @@
## 4. AI 修改代码时的执行流程
- 先定位改动所属模块,再检查是否会破坏职责边界。
- 优先做最小闭环改动,避免无关重构。
- 项目中可能存在语义不清的地方,必须要谨慎分析
- 如果新增能力会被模型调用,先设计 `tools` 抽象,再接入 `runtime`。
- 如果改动涉及 provider 协议,差异收敛在 `internal/provider` 内,不向外扩散特定厂商字段。
- 如果改动涉及配置,统一经由 `ConfigManager` 或现有配置管理入口处理。
Expand Down
10 changes: 5 additions & 5 deletions docs/context-compact.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ context:
优先复用会话记录的 `provider` / `model`,缺失时回退到当前配置。
6. summary generator 调用模型生成完整 `task_state` 与 display summary。
7. runner 校验 display summary 结构与长度,必要时截断,并写入 `task_state.last_updated_at`。
8. compact 成功时回写 `session.TaskState` 与会话消息并发出 `compact_done`;失败时发出 `compact_error`。
8. compact 成功时回写 `session.TaskState` 与会话消息并发出 `compact_applied`;失败时发出 `compact_error`。

其中 `reactive` mode 在 context 包内与 `manual` 复用同一条压缩管线:

Expand All @@ -77,10 +77,9 @@ context:

1. 识别 provider 归一化后的 typed error,必要时回退到错误文本匹配。
2. 触发 `compact.Run(mode=reactive)`,并在仍然命中“上下文过长”时继续做逐步降级恢复。
3. 继续复用 `compact_start`、`compact_done`、`compact_error` 事件,并通过 `trigger_mode=reactive` 区分来源。
3. 继续复用 `compact_start`、`compact_applied`、`compact_error` 事件,并通过 `trigger_mode=reactive` 区分来源。
4. 每次 `Run()` 最多执行 3 次 reactive compact 降级尝试;每次尝试都会进一步收缩 `manual_keep_recent_messages`,超过上限后返回最后一次 provider 错误。


## 生成协议

compact generator 必须只返回一个 JSON 对象,顶层固定包含:
Expand Down Expand Up @@ -142,13 +141,14 @@ constraints:
compact 相关 runtime 事件包括:

- `compact_start`
- `compact_done`
- `compact_applied`
- `compact_error`

`compact_done` payload 包含:
`compact_applied` payload 包含:

- `applied`
Comment thread
fennoai[bot] marked this conversation as resolved.
Comment thread
fennoai[bot] marked this conversation as resolved.
Comment thread
fennoai[bot] marked this conversation as resolved.
- `before_chars`
- `before_tokens`
- `after_chars`
- `saved_ratio`
- `trigger_mode`
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ custom provider 目录:

```yaml
selected_provider: openai
current_model: gpt-4.1
current_model: gpt-5.4
shell: bash
tool_timeout_sec: 20

Expand Down
11 changes: 6 additions & 5 deletions docs/runtime-provider-event-flow.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

## Runtime 事件类型

当前 runtime 对外暴露一组小而稳定的事件:
当前 runtime 对外暴露一组小而稳定的事件(1A 硬切后不再保留旧事件镜像)

- `user_message`
Comment thread
fennoai[bot] marked this conversation as resolved.
Comment thread
fennoai[bot] marked this conversation as resolved.
- `agent_chunk`
Expand All @@ -11,14 +11,15 @@
- `tool_start`
- `tool_chunk`
- `tool_result`
- `run_canceled`
- `error`
- `phase_changed`
- `progress_evaluated`
- `stop_reason_decided`
- `provider_retry`
- `permission_request`
- `permission_requested`
- `permission_resolved`
- `token_usage`
- `compact_start`
- `compact_done`
- `compact_applied`
- `compact_error`

这三类 compact 事件同时用于 `manual`、`auto` 与 `reactive` 三种来源,调用方可通过 payload 中的 `trigger_mode` 区分。
Expand Down
2 changes: 1 addition & 1 deletion internal/config/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ func (p ResolvedProviderConfig) ToRuntimeConfig() provider.RuntimeConfig {
const (
OpenAIName = "openai"
OpenAIDefaultBaseURL = "https://api.openai.com/v1"
OpenAIDefaultModel = "gpt-4.1"
OpenAIDefaultModel = "gpt-5.4"

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.

Default OpenAI model changed to gpt-5.4, but docs still show current_model: gpt-4.1 (docs/guides/configuration.md). Please update the docs in this PR to keep configuration guidance aligned with runtime defaults.

OpenAIDefaultAPIKeyEnv = "OPENAI_API_KEY"

GeminiName = "gemini"
Expand Down
2 changes: 1 addition & 1 deletion internal/runtime/compact.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func (s *Service) runCompactForSession(
}
}

s.emit(ctx, EventCompactDone, runID, session.ID, fromCompactResult(result))
s.emit(ctx, EventCompactApplied, runID, session.ID, fromCompactResult(result))
return session, result, nil
}

Expand Down
38 changes: 38 additions & 0 deletions internal/runtime/controlplane/decider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package controlplane

import (
"context"
"errors"
"strings"
)

// StopInput 汇总停止决议所需的信号(可多信号并存,由 DecideStopReason 按优先级表决)。
type StopInput struct {
ContextCanceled bool
MaxLoopsReached bool
RunError error
Success bool
}

// DecideStopReason 按固定优先级返回唯一 StopReason:取消 > 达到轮数上限 > 错误 > 成功。
func DecideStopReason(in StopInput) (StopReason, string) {
if in.ContextCanceled {
return StopReasonCanceled, ""
}
if in.MaxLoopsReached {
if in.RunError != nil {
return StopReasonMaxLoops, strings.TrimSpace(in.RunError.Error())
}
return StopReasonMaxLoops, "runtime: max loop reached"
}
if in.RunError != nil {
if errors.Is(in.RunError, context.Canceled) {
return StopReasonCanceled, ""
}
return StopReasonError, strings.TrimSpace(in.RunError.Error())
}
if in.Success {
return StopReasonSuccess, ""
}
return StopReasonError, "runtime: stop reason undetermined"
}
88 changes: 88 additions & 0 deletions internal/runtime/controlplane/decider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package controlplane

import (
"context"
"errors"
"testing"
)

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

errSample := errors.New("boom")
cases := []struct {
name string
in StopInput
reason StopReason
}{
{
name: "canceled_wins_over_max_loops",
in: StopInput{
ContextCanceled: true,
MaxLoopsReached: true,
RunError: errSample,
},
reason: StopReasonCanceled,
},
{
name: "max_loops_wins_over_error",
in: StopInput{
MaxLoopsReached: true,
RunError: errSample,
},
reason: StopReasonMaxLoops,
},
{
name: "error_when_no_max_loop_flag",
in: StopInput{
RunError: errSample,
},
reason: StopReasonError,
},
{
name: "success",
in: StopInput{
Success: true,
},
reason: StopReasonSuccess,
},
{
name: "context_canceled_on_error_field",
in: StopInput{
RunError: context.Canceled,
},
reason: StopReasonCanceled,
},
}

for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, _ := DecideStopReason(tc.in)
if got != tc.reason {
t.Fatalf("DecideStopReason() = %q, want %q", got, tc.reason)
}
})
}
}

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

reason, detail := DecideStopReason(StopInput{MaxLoopsReached: true})
if reason != StopReasonMaxLoops {
t.Fatalf("reason = %q, want %q", reason, StopReasonMaxLoops)
}
if detail != "runtime: max loop reached" {
t.Fatalf("detail = %q, want default max-loop detail", detail)
}

reason, detail = DecideStopReason(StopInput{})
if reason != StopReasonError {
t.Fatalf("reason = %q, want %q", reason, StopReasonError)
}
if detail != "runtime: stop reason undetermined" {
t.Fatalf("detail = %q, want undetermined detail", detail)
}
}
4 changes: 4 additions & 0 deletions internal/runtime/controlplane/envelope.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package controlplane

// PayloadVersion 为 runtime 事件 envelope 的当前协议版本号。
const PayloadVersion = 1
13 changes: 13 additions & 0 deletions internal/runtime/controlplane/phase.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package controlplane

// Phase 表示单轮 ReAct 内的显式阶段(plan -> execute -> verify)。
type Phase string

const (
// PhasePlan 规划阶段:构建上下文、调用 provider 直至得到 assistant 消息(含工具调用决策)。
PhasePlan Phase = "plan"
// PhaseExecute 执行阶段:执行本批次全部工具调用。
PhaseExecute Phase = "execute"
// PhaseVerify 验证阶段:工具结果已回灌,等待下一轮 provider 校验或收尾。
PhaseVerify Phase = "verify"
)
39 changes: 39 additions & 0 deletions internal/runtime/controlplane/progress.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package controlplane

// ProgressEvidenceKind 标识工具/适配器产出的证据类型,runtime 仅聚合不做语义推断。
type ProgressEvidenceKind string

const (
// EvidenceNewInfoNonDup 表示本轮引入了非重复的新信息(用于 streak 回归约束)。
EvidenceNewInfoNonDup ProgressEvidenceKind = "EVIDENCE_NEW_INFO_NON_DUP"
)

// ProgressEvidenceRecord 描述一条可计分的进展证据。
type ProgressEvidenceRecord struct {
Kind ProgressEvidenceKind `json:"kind"`
Detail string `json:"detail,omitempty"`
}

// ProgressScore 表示一次评估后的分值增量与 streak 快照。
type ProgressScore struct {
ScoreDelta int `json:"score_delta"`
NoProgressStreak int `json:"no_progress_streak"`
RepeatCycleStreak int `json:"repeat_cycle_streak"`
}

// ProgressState 汇总当前运行期 progress 控制面状态。
type ProgressState struct {
LastScore ProgressScore `json:"last_score"`
}

// ApplyProgressEvidence 根据证据更新分值与 streak。
func ApplyProgressEvidence(state ProgressState, records []ProgressEvidenceRecord) ProgressState {
next := state.LastScore
if len(records) == 0 {
next.NoProgressStreak++
} else {
next.NoProgressStreak = 0
next.ScoreDelta++
}
return ProgressState{LastScore: next}
}
42 changes: 42 additions & 0 deletions internal/runtime/controlplane/progress_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package controlplane

import "testing"

func TestApplyProgressEvidenceNoEvidenceIncrementsNoProgress(t *testing.T) {
t.Parallel()
state := ProgressState{}
next := ApplyProgressEvidence(state, nil)
if next.LastScore.NoProgressStreak != 1 {
t.Fatalf("expected no_progress_streak 1, got %d", next.LastScore.NoProgressStreak)
}
}

func TestApplyProgressEvidenceOnlyNonDupResetsNoProgressStreak(t *testing.T) {
t.Parallel()
state := ProgressState{
LastScore: ProgressScore{NoProgressStreak: 3},
}
next := ApplyProgressEvidence(state, []ProgressEvidenceRecord{
{Kind: EvidenceNewInfoNonDup},
})
if next.LastScore.NoProgressStreak != 0 {
t.Fatalf("expected streak reset to 0, got %d", next.LastScore.NoProgressStreak)
}
if next.LastScore.ScoreDelta != 1 {
t.Fatalf("expected score_delta 1, got %d", next.LastScore.ScoreDelta)
}
}

func TestApplyProgressEvidenceMixedResetsNoProgress(t *testing.T) {
t.Parallel()
state := ProgressState{
LastScore: ProgressScore{NoProgressStreak: 2},
}
next := ApplyProgressEvidence(state, []ProgressEvidenceRecord{
{Kind: EvidenceNewInfoNonDup},
{Kind: ProgressEvidenceKind("other_evidence")},
})
if next.LastScore.NoProgressStreak != 0 {
t.Fatalf("expected streak reset, got %d", next.LastScore.NoProgressStreak)
}
}
15 changes: 15 additions & 0 deletions internal/runtime/controlplane/stop_reason.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package controlplane

// StopReason 表示一次 Run 的最终停止原因,互斥且由决议器唯一确定。
type StopReason string

const (
// StopReasonSuccess 表示助手正常结束(无待执行工具调用)。
StopReasonSuccess StopReason = "success"
// StopReasonMaxLoops 表示达到配置的最大推理轮数。
StopReasonMaxLoops StopReason = "max_loops"
// StopReasonError 表示不可恢复的运行时或 provider 错误。
StopReasonError StopReason = "error"
// StopReasonCanceled 表示运行上下文被取消(含用户中断)。
StopReasonCanceled StopReason = "canceled"
)
Loading
Loading