Skip to content

Commit 5818974

Browse files
authored
Merge pull request #582 from Cai-Tang-www/feat/issue-578-ask-user-a
feat(runtime): implement ask_user tool for plan-mode user interaction
2 parents 6a25bb2 + c0985d1 commit 5818974

56 files changed

Lines changed: 3334 additions & 144 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

internal/app/bootstrap.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,15 @@ func BuildGatewayServerDeps(ctx context.Context, opts BootstrapOptions) (Runtime
230230
return RuntimeBundle{}, err
231231
}
232232

233+
// Wire ask_user broker from runtime into the pre-registered ask_user tool.
234+
if brokerAdapter := runtimeSvc.AskUserBrokerAdapter(); brokerAdapter != nil {
235+
if askTool, err := toolRegistry.Get(tools.ToolNameAskUser); err == nil {
236+
if setter, ok := askTool.(tools.AskUserBrokerSetter); ok {
237+
setter.SetAskUserBroker(brokerAdapter)
238+
}
239+
}
240+
}
241+
233242
// 注入记忆提取钩子:当 AutoExtract 启用且 memoSvc 可用时,ReAct 循环完成后异步提取记忆。
234243
if memoSvc != nil && cfg.Memo.AutoExtract {
235244
runtimeSvc.SetMemoExtractor(newMemoExtractorAdapter(
@@ -466,6 +475,7 @@ func buildToolRegistry(cfg config.Config) (*tools.Registry, func() error, error)
466475
toolRegistry.Register(codebase.NewRead(repoSvc, cfg.Workdir))
467476
toolRegistry.Register(codebase.NewSearchText(repoSvc, cfg.Workdir))
468477
toolRegistry.Register(codebase.NewSearchSymbol(repoSvc, cfg.Workdir))
478+
toolRegistry.Register(tools.NewAskUserTool(nil)) // broker injected after runtime creation
469479
mcpRegistry, err := BuildMCPRegistry(cfg)
470480
if err != nil {
471481
return nil, nil, err

internal/app/bootstrap_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2017,6 +2017,10 @@ func (s *stubRuntimeForBootstrap) ResolvePermission(context.Context, agentruntim
20172017
return nil
20182018
}
20192019

2020+
func (s *stubRuntimeForBootstrap) ResolveUserQuestion(context.Context, agentruntime.UserQuestionResolutionInput) error {
2021+
return nil
2022+
}
2023+
20202024
func (s *stubRuntimeForBootstrap) CancelActiveRun() bool {
20212025
return false
20222026
}
@@ -2083,6 +2087,10 @@ func (s *stubRemoteRuntimeForBootstrap) ResolvePermission(context.Context, servi
20832087
return nil
20842088
}
20852089

2090+
func (s *stubRemoteRuntimeForBootstrap) ResolveUserQuestion(context.Context, services.UserQuestionResolutionInput) error {
2091+
return nil
2092+
}
2093+
20862094
func (s *stubRemoteRuntimeForBootstrap) CancelActiveRun() bool {
20872095
return false
20882096
}

internal/cli/gateway_runtime_bridge.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,19 @@ func (b *gatewayRuntimePortBridge) ResolvePermission(ctx context.Context, input
477477
})
478478
}
479479

480+
// ResolveUserQuestion 将网关 ask_user 回答转发到 runtime。
481+
func (b *gatewayRuntimePortBridge) ResolveUserQuestion(ctx context.Context, input gateway.UserQuestionAnswerInput) error {
482+
if err := b.ensureRuntimeAccess(input.SubjectID); err != nil {
483+
return err
484+
}
485+
return b.runtime.ResolveUserQuestion(ctx, agentruntime.UserQuestionResolutionInput{
486+
RequestID: strings.TrimSpace(input.RequestID),
487+
Status: strings.TrimSpace(input.Status),
488+
Values: input.Values,
489+
Message: strings.TrimSpace(input.Message),
490+
})
491+
}
492+
480493
// CancelRun 转发 gateway.cancel 请求到 runtime 的 run_id 精确取消能力。
481494
func (b *gatewayRuntimePortBridge) CancelRun(_ context.Context, input gateway.CancelInput) (bool, error) {
482495
if err := b.ensureRuntimeAccess(input.SubjectID); err != nil {

internal/cli/gateway_runtime_bridge_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,10 @@ func (s *runtimeStub) ResolvePermission(_ context.Context, input agentruntime.Pe
128128
return s.permissionErr
129129
}
130130

131+
func (s *runtimeStub) ResolveUserQuestion(_ context.Context, input agentruntime.UserQuestionResolutionInput) error {
132+
return nil
133+
}
134+
131135
func (s *runtimeStub) CancelActiveRun() bool {
132136
return s.cancelReturn
133137
}
@@ -238,6 +242,9 @@ func (r *runtimeWithoutCreator) ExecuteSystemTool(ctx context.Context, input age
238242
func (r *runtimeWithoutCreator) ResolvePermission(ctx context.Context, input agentruntime.PermissionResolutionInput) error {
239243
return r.base.ResolvePermission(ctx, input)
240244
}
245+
func (r *runtimeWithoutCreator) ResolveUserQuestion(ctx context.Context, input agentruntime.UserQuestionResolutionInput) error {
246+
return r.base.ResolveUserQuestion(ctx, input)
247+
}
241248
func (r *runtimeWithoutCreator) CancelActiveRun() bool {
242249
return r.base.CancelActiveRun()
243250
}
@@ -310,6 +317,9 @@ func (r *runtimeWithoutCheckpointer) ExecuteSystemTool(ctx context.Context, inpu
310317
func (r *runtimeWithoutCheckpointer) ResolvePermission(ctx context.Context, input agentruntime.PermissionResolutionInput) error {
311318
return r.base.ResolvePermission(ctx, input)
312319
}
320+
func (r *runtimeWithoutCheckpointer) ResolveUserQuestion(ctx context.Context, input agentruntime.UserQuestionResolutionInput) error {
321+
return r.base.ResolveUserQuestion(ctx, input)
322+
}
313323
func (r *runtimeWithoutCheckpointer) CancelActiveRun() bool {
314324
return r.base.CancelActiveRun()
315325
}

internal/cli/root_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,6 +1051,10 @@ func (stubRuntimePort) ResolvePermission(context.Context, gateway.PermissionReso
10511051
return nil
10521052
}
10531053

1054+
func (stubRuntimePort) ResolveUserQuestion(context.Context, gateway.UserQuestionAnswerInput) error {
1055+
return nil
1056+
}
1057+
10541058
func (stubRuntimePort) CancelRun(context.Context, gateway.CancelInput) (bool, error) {
10551059
return false, nil
10561060
}

internal/gateway/adapters/urlscheme/dispatcher_integration_unix_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,13 @@ func (s *urlschemeIntegrationRuntimeStub) ResolvePermission(
159159
return nil
160160
}
161161

162+
func (s *urlschemeIntegrationRuntimeStub) ResolveUserQuestion(
163+
context.Context,
164+
gateway.UserQuestionAnswerInput,
165+
) error {
166+
return nil
167+
}
168+
162169
func (s *urlschemeIntegrationRuntimeStub) CancelRun(context.Context, gateway.CancelInput) (bool, error) {
163170
return false, nil
164171
}

internal/gateway/bootstrap.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,6 +1539,44 @@ func handleDeleteMCPServerFrame(ctx context.Context, frame MessageFrame, runtime
15391539
return MessageFrame{Type: FrameTypeAck, Action: FrameActionDeleteMCPServer, RequestID: frame.RequestID, Payload: map[string]any{"deleted": true, "id": input.ID}}
15401540
}
15411541

1542+
// handleUserQuestionAnswerFrame 处理 gateway.user_question_answer 请求。
1543+
func handleUserQuestionAnswerFrame(ctx context.Context, frame MessageFrame, runtimePort RuntimePort) MessageFrame {
1544+
if runtimePort == nil {
1545+
return runtimePortUnavailableFrame(frame)
1546+
}
1547+
subjectID, subjectErr := requireAuthenticatedSubjectID(ctx)
1548+
if subjectErr != nil {
1549+
return errorFrame(frame, subjectErr)
1550+
}
1551+
1552+
input, err := decodeUserQuestionAnswerPayload(frame.Payload)
1553+
if err != nil {
1554+
return errorFrame(frame, NewFrameError(ErrorCodeInvalidAction, "invalid user_question_answer payload"))
1555+
}
1556+
input.SubjectID = subjectID
1557+
input.RequestID = strings.TrimSpace(input.RequestID)
1558+
if input.RequestID == "" {
1559+
return errorFrame(frame, NewMissingRequiredFieldError("payload.request_id"))
1560+
}
1561+
1562+
callCtx, cancel := withRuntimeOperationTimeout(ctx)
1563+
defer cancel()
1564+
if err := runtimePort.ResolveUserQuestion(callCtx, input); err != nil {
1565+
return runtimeCallFailedFrame(callCtx, frame, err, "user_question_answer")
1566+
}
1567+
1568+
return MessageFrame{
1569+
Type: FrameTypeAck,
1570+
Action: FrameActionUserQuestionAnswer,
1571+
RequestID: frame.RequestID,
1572+
Payload: map[string]any{
1573+
"request_id": input.RequestID,
1574+
"status": input.Status,
1575+
"message": "user question answered",
1576+
},
1577+
}
1578+
}
1579+
15421580
// handleResolvePermissionFrame 处理 gateway.resolvePermission 请求。
15431581
func handleResolvePermissionFrame(ctx context.Context, frame MessageFrame, runtimePort RuntimePort) MessageFrame {
15441582
if runtimePort == nil {

internal/gateway/bootstrap_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,10 @@ func (s *bootstrapRuntimeStub) ResolvePermission(ctx context.Context, input Perm
140140
return nil
141141
}
142142

143+
func (s *bootstrapRuntimeStub) ResolveUserQuestion(ctx context.Context, input UserQuestionAnswerInput) error {
144+
return nil
145+
}
146+
143147
func (s *bootstrapRuntimeStub) CancelRun(ctx context.Context, input CancelInput) (bool, error) {
144148
if s != nil && s.cancelRunFn != nil {
145149
return s.cancelRunFn(ctx, input)
@@ -4780,6 +4784,9 @@ func (runtimeOnlyStub) ListAvailableSkills(ctx context.Context, input ListAvaila
47804784
func (runtimeOnlyStub) ResolvePermission(ctx context.Context, input PermissionResolutionInput) error {
47814785
return nil
47824786
}
4787+
func (runtimeOnlyStub) ResolveUserQuestion(ctx context.Context, input UserQuestionAnswerInput) error {
4788+
return nil
4789+
}
47834790
func (runtimeOnlyStub) CancelRun(ctx context.Context, input CancelInput) (bool, error) {
47844791
return false, nil
47854792
}

internal/gateway/contracts.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,20 @@ type DeleteProviderInput struct {
545545
ProviderID string `json:"provider_id"`
546546
}
547547

548+
// UserQuestionAnswerInput 表示一次 ask_user 回答的输入。
549+
type UserQuestionAnswerInput struct {
550+
// SubjectID 是请求方身份主体标识。
551+
SubjectID string `json:"subject_id,omitempty"`
552+
// RequestID 是待回答的提问标识。
553+
RequestID string `json:"request_id"`
554+
// Status 是回答状态:answered / skipped。
555+
Status string `json:"status"`
556+
// Values 是用户选择的答案值。
557+
Values []string `json:"values,omitempty"`
558+
// Message 是用户的文本答案。
559+
Message string `json:"message,omitempty"`
560+
}
561+
548562
// SelectProviderModelInput 表示全局选择 provider/model 的输入。
549563
type SelectProviderModelInput struct {
550564
// SubjectID 是请求方身份主体标识。
@@ -798,6 +812,8 @@ type RuntimePort interface {
798812
ListAvailableSkills(ctx context.Context, input ListAvailableSkillsInput) ([]AvailableSkillState, error)
799813
// ResolvePermission 向运行时提交一次权限审批决策。
800814
ResolvePermission(ctx context.Context, input PermissionResolutionInput) error
815+
// ResolveUserQuestion 向运行时提交一次 ask_user 回答。
816+
ResolveUserQuestion(ctx context.Context, input UserQuestionAnswerInput) error
801817
// CancelRun 按 run_id 精确取消运行态任务。
802818
CancelRun(ctx context.Context, input CancelInput) (bool, error)
803819
// Events 返回统一运行事件流。

internal/gateway/contracts_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ func (s *runtimePortCompileStub) ResolvePermission(_ context.Context, _ Permissi
5858
return nil
5959
}
6060

61+
func (s *runtimePortCompileStub) ResolveUserQuestion(_ context.Context, _ UserQuestionAnswerInput) error {
62+
return nil
63+
}
64+
6165
func (s *runtimePortCompileStub) CancelRun(_ context.Context, _ CancelInput) (bool, error) {
6266
return false, nil
6367
}

0 commit comments

Comments
 (0)