Skip to content

Commit 8aaa5de

Browse files
authored
Merge pull request #394 from phantom5099/main
pref(runtime): 统一执行生命周期,并强化任务结束与工作区安全机制
2 parents 60a0cce + 8d24600 commit 8aaa5de

43 files changed

Lines changed: 2202 additions & 362 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package controlplane
2+
3+
// CompletionBlockedReason 表示 completion gate 阻塞完成的原因。
4+
type CompletionBlockedReason string
5+
6+
const (
7+
// CompletionBlockedReasonNone 表示当前不存在阻塞原因。
8+
CompletionBlockedReasonNone CompletionBlockedReason = ""
9+
// CompletionBlockedReasonPendingTodo 表示仍存在未完成
10+
CompletionBlockedReasonPendingTodo CompletionBlockedReason = "pending_todo"
11+
// CompletionBlockedReasonUnverifiedWrite 表示仍存在未验证写入。
12+
CompletionBlockedReasonUnverifiedWrite CompletionBlockedReason = "unverified_write"
13+
// CompletionBlockedReasonPostExecuteClosureRequired 表示刚完成执行后仍需闭环。
14+
CompletionBlockedReasonPostExecuteClosureRequired CompletionBlockedReason = "post_execute_closure_required"
15+
)
16+
17+
// CompletionState 描述 completion gate 所需的运行事实。
18+
type CompletionState struct {
19+
HasPendingAgentTodos bool `json:"has_pending_agent_todos"`
20+
HasUnverifiedWrites bool `json:"has_unverified_writes"`
21+
CompletionBlockedReason CompletionBlockedReason `json:"completion_blocked_reason,omitempty"`
22+
}
23+
24+
// EvaluateCompletion 依据当前事实计算是否允许本轮 completed。
25+
func EvaluateCompletion(state CompletionState, assistantHasToolCalls bool) (CompletionState, bool) {
26+
state.CompletionBlockedReason = CompletionBlockedReasonNone
27+
28+
if assistantHasToolCalls {
29+
state.CompletionBlockedReason = CompletionBlockedReasonPostExecuteClosureRequired
30+
return state, false
31+
}
32+
if state.HasPendingAgentTodos {
33+
state.CompletionBlockedReason = CompletionBlockedReasonPendingTodo
34+
return state, false
35+
}
36+
if state.HasUnverifiedWrites {
37+
state.CompletionBlockedReason = CompletionBlockedReasonUnverifiedWrite
38+
return state, false
39+
}
40+
return state, true
41+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package controlplane
2+
3+
import "testing"
4+
5+
func TestEvaluateCompletionBlockedByPendingTodo(t *testing.T) {
6+
t.Parallel()
7+
8+
state, completed := EvaluateCompletion(CompletionState{
9+
HasPendingAgentTodos: true,
10+
}, false)
11+
if completed {
12+
t.Fatalf("expected completion to be blocked")
13+
}
14+
if state.CompletionBlockedReason != CompletionBlockedReasonPendingTodo {
15+
t.Fatalf("blocked reason = %q, want %q", state.CompletionBlockedReason, CompletionBlockedReasonPendingTodo)
16+
}
17+
}
18+
19+
func TestEvaluateCompletionBlockedByUnverifiedWrite(t *testing.T) {
20+
t.Parallel()
21+
22+
state, completed := EvaluateCompletion(CompletionState{
23+
HasUnverifiedWrites: true,
24+
}, false)
25+
if completed {
26+
t.Fatalf("expected completion to be blocked")
27+
}
28+
if state.CompletionBlockedReason != CompletionBlockedReasonUnverifiedWrite {
29+
t.Fatalf("blocked reason = %q, want %q", state.CompletionBlockedReason, CompletionBlockedReasonUnverifiedWrite)
30+
}
31+
}
32+
33+
func TestEvaluateCompletionBlockedAfterToolCalls(t *testing.T) {
34+
t.Parallel()
35+
36+
state, completed := EvaluateCompletion(CompletionState{}, true)
37+
if completed {
38+
t.Fatalf("expected completion to be blocked after tool call turn")
39+
}
40+
if state.CompletionBlockedReason != CompletionBlockedReasonPostExecuteClosureRequired {
41+
t.Fatalf("blocked reason = %q, want %q", state.CompletionBlockedReason, CompletionBlockedReasonPostExecuteClosureRequired)
42+
}
43+
}
44+
45+
func TestEvaluateCompletionAllowsSatisfiedClosure(t *testing.T) {
46+
t.Parallel()
47+
48+
state, completed := EvaluateCompletion(CompletionState{}, false)
49+
if !completed {
50+
t.Fatalf("expected completion to succeed")
51+
}
52+
if state.CompletionBlockedReason != CompletionBlockedReasonNone {
53+
t.Fatalf("blocked reason = %q, want empty", state.CompletionBlockedReason)
54+
}
55+
}

internal/runtime/controlplane/decider.go

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,26 @@ import (
66
"strings"
77
)
88

9-
// StopInput 汇总停止决议所需的信号(可多信号并存,由 DecideStopReason 按优先级表决)
9+
// StopInput 汇总最终 stop 决议所需的信号
1010
type StopInput struct {
11-
ContextCanceled bool
12-
RunError error
13-
Success bool
11+
UserInterrupted bool
12+
FatalError error
13+
Completed bool
1414
}
1515

16-
// DecideStopReason 按固定优先级返回唯一 StopReason:取消 > 错误 > 成功
16+
// DecideStopReason 按固定优先级返回唯一的最终 stop 原因
1717
func DecideStopReason(in StopInput) (StopReason, string) {
18-
if in.ContextCanceled {
19-
return StopReasonCanceled, ""
18+
if in.UserInterrupted {
19+
return StopReasonUserInterrupt, ""
2020
}
21-
if in.RunError != nil {
22-
if errors.Is(in.RunError, context.Canceled) {
23-
return StopReasonCanceled, ""
21+
if in.FatalError != nil {
22+
if errors.Is(in.FatalError, context.Canceled) {
23+
return StopReasonUserInterrupt, ""
2424
}
25-
return StopReasonError, strings.TrimSpace(in.RunError.Error())
25+
return StopReasonFatalError, strings.TrimSpace(in.FatalError.Error())
2626
}
27-
if in.Success {
28-
return StopReasonSuccess, ""
27+
if in.Completed {
28+
return StopReasonCompleted, ""
2929
}
30-
return StopReasonError, "runtime: stop reason undetermined"
30+
return StopReasonFatalError, "runtime: stop reason undetermined"
3131
}

internal/runtime/controlplane/decider_test.go

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,48 +11,50 @@ func TestDecideStopReasonPriority(t *testing.T) {
1111

1212
errSample := errors.New("boom")
1313
cases := []struct {
14-
name string
15-
in StopInput
16-
reason StopReason
14+
name string
15+
in StopInput
16+
wantReason StopReason
1717
}{
1818
{
19-
name: "canceled_wins_over_error",
19+
name: "user_interrupt_wins_over_fatal",
2020
in: StopInput{
21-
ContextCanceled: true,
22-
RunError: errSample,
21+
UserInterrupted: true,
22+
FatalError: errSample,
2323
},
24-
reason: StopReasonCanceled,
24+
wantReason: StopReasonUserInterrupt,
2525
},
2626
{
27-
name: "error",
27+
name: "fatal_error_wins_over_completed",
2828
in: StopInput{
29-
RunError: errSample,
29+
FatalError: errSample,
30+
Completed: true,
3031
},
31-
reason: StopReasonError,
32+
wantReason: StopReasonFatalError,
3233
},
3334
{
34-
name: "success",
35+
name: "completed",
3536
in: StopInput{
36-
Success: true,
37+
Completed: true,
3738
},
38-
reason: StopReasonSuccess,
39+
wantReason: StopReasonCompleted,
3940
},
4041
{
41-
name: "context_canceled_on_error_field",
42+
name: "context_canceled_maps_to_user_interrupt",
4243
in: StopInput{
43-
RunError: context.Canceled,
44+
FatalError: context.Canceled,
4445
},
45-
reason: StopReasonCanceled,
46+
wantReason: StopReasonUserInterrupt,
4647
},
4748
}
4849

4950
for _, tc := range cases {
5051
tc := tc
5152
t.Run(tc.name, func(t *testing.T) {
5253
t.Parallel()
54+
5355
got, _ := DecideStopReason(tc.in)
54-
if got != tc.reason {
55-
t.Fatalf("DecideStopReason() = %q, want %q", got, tc.reason)
56+
if got != tc.wantReason {
57+
t.Fatalf("DecideStopReason() = %q, want %q", got, tc.wantReason)
5658
}
5759
})
5860
}
@@ -62,8 +64,8 @@ func TestDecideStopReasonDetails(t *testing.T) {
6264
t.Parallel()
6365

6466
reason, detail := DecideStopReason(StopInput{})
65-
if reason != StopReasonError {
66-
t.Fatalf("reason = %q, want %q", reason, StopReasonError)
67+
if reason != StopReasonFatalError {
68+
t.Fatalf("reason = %q, want %q", reason, StopReasonFatalError)
6769
}
6870
if detail != "runtime: stop reason undetermined" {
6971
t.Fatalf("detail = %q, want undetermined detail", detail)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
package controlplane
22

33
// PayloadVersion 为 runtime 事件 envelope 的当前协议版本号。
4-
const PayloadVersion = 1
4+
const PayloadVersion = 2
Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,75 @@
11
package controlplane
22

3-
// Phase 表示单轮 ReAct 内的显式阶段(plan -> execute -> dispatch -> verify)。
4-
type Phase string
3+
import "fmt"
4+
5+
// RunState 表示单次 Run 生命周期中的显式运行态,统一承载主链 phase 与外围治理态。
6+
type RunState string
57

68
const (
7-
// PhasePlan 规划阶段:构建上下文、调用 provider 直至得到 assistant 消息(含工具调用决策)。
8-
PhasePlan Phase = "plan"
9-
// PhaseExecute 执行阶段:执行本批次全部工具调用。
10-
PhaseExecute Phase = "execute"
11-
// PhaseDispatch 调度阶段:执行 Todo 驱动的子代理任务派发。
12-
PhaseDispatch Phase = "dispatch"
13-
// PhaseVerify 验证阶段:工具结果已回灌,等待下一轮 provider 校验或收尾。
14-
PhaseVerify Phase = "verify"
9+
// RunStatePlan 表示规划阶段:构建上下文并驱动 provider 产出 assistant 决策。
10+
RunStatePlan RunState = "plan"
11+
// RunStateExecute 表示执行阶段:执行本轮 assistant 产生的全部工具调用。
12+
RunStateExecute RunState = "execute"
13+
// RunStateVerify 表示验证阶段:工具结果已回灌,等待下一轮模型收尾或继续推进。
14+
RunStateVerify RunState = "verify"
15+
// RunStateCompacting 表示当前正在执行 compact 或 reactive compact。
16+
RunStateCompacting RunState = "compacting"
17+
// RunStateWaitingPermission 表示当前正在等待权限决议,执行流被显式挂起。
18+
RunStateWaitingPermission RunState = "waiting_permission"
19+
// RunStateStopped 表示本次 Run 已完成终止决议,不再继续推进生命周期。
20+
RunStateStopped RunState = "stopped"
1521
)
22+
23+
var allowedRunStateTransitions = map[RunState]map[RunState]struct{}{
24+
"": {
25+
RunStatePlan: {},
26+
},
27+
RunStatePlan: {
28+
RunStatePlan: {},
29+
RunStateExecute: {},
30+
RunStateCompacting: {},
31+
RunStateWaitingPermission: {},
32+
RunStateStopped: {},
33+
},
34+
RunStateExecute: {
35+
RunStateExecute: {},
36+
RunStateVerify: {},
37+
RunStateCompacting: {},
38+
RunStateWaitingPermission: {},
39+
RunStateStopped: {},
40+
},
41+
RunStateVerify: {
42+
RunStateVerify: {},
43+
RunStatePlan: {},
44+
RunStateCompacting: {},
45+
RunStateWaitingPermission: {},
46+
RunStateStopped: {},
47+
},
48+
RunStateCompacting: {
49+
RunStateCompacting: {},
50+
RunStatePlan: {},
51+
RunStateWaitingPermission: {},
52+
RunStateStopped: {},
53+
},
54+
RunStateWaitingPermission: {
55+
RunStateWaitingPermission: {},
56+
RunStatePlan: {},
57+
RunStateExecute: {},
58+
RunStateVerify: {},
59+
RunStateCompacting: {},
60+
RunStateStopped: {},
61+
},
62+
RunStateStopped: {
63+
RunStateStopped: {},
64+
},
65+
}
66+
67+
// ValidateRunStateTransition 校验生命周期迁移是否合法,避免主链 phase 与外围治理态分裂成多套规则。
68+
func ValidateRunStateTransition(from RunState, to RunState) error {
69+
if nextStates, ok := allowedRunStateTransitions[from]; ok {
70+
if _, allowed := nextStates[to]; allowed {
71+
return nil
72+
}
73+
}
74+
return fmt.Errorf("runtime: invalid run state transition %q -> %q", from, to)
75+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package controlplane
2+
3+
import "testing"
4+
5+
func TestValidateRunStateTransitionMainlineAndGovernanceStates(t *testing.T) {
6+
t.Parallel()
7+
8+
validTransitions := []struct {
9+
from RunState
10+
to RunState
11+
}{
12+
{from: "", to: RunStatePlan},
13+
{from: RunStatePlan, to: RunStateExecute},
14+
{from: RunStateExecute, to: RunStateVerify},
15+
{from: RunStateVerify, to: RunStatePlan},
16+
{from: RunStatePlan, to: RunStateCompacting},
17+
{from: RunStateCompacting, to: RunStatePlan},
18+
{from: RunStateExecute, to: RunStateWaitingPermission},
19+
{from: RunStateWaitingPermission, to: RunStateExecute},
20+
{from: RunStateVerify, to: RunStateStopped},
21+
}
22+
23+
for _, tc := range validTransitions {
24+
tc := tc
25+
t.Run(string(tc.from)+"->"+string(tc.to), func(t *testing.T) {
26+
t.Parallel()
27+
if err := ValidateRunStateTransition(tc.from, tc.to); err != nil {
28+
t.Fatalf("ValidateRunStateTransition(%q,%q) error = %v", tc.from, tc.to, err)
29+
}
30+
})
31+
}
32+
}
33+
34+
func TestValidateRunStateTransitionRejectsInvalidJump(t *testing.T) {
35+
t.Parallel()
36+
37+
if err := ValidateRunStateTransition(RunStatePlan, RunStateVerify); err == nil {
38+
t.Fatalf("expected invalid transition to return error")
39+
}
40+
}

0 commit comments

Comments
 (0)