Skip to content

Commit 72a2093

Browse files
solthxchengzifeng
andauthored
feat(remote-control): 优化 Web 展示、状态同步与桥接控制流程 (#288)
Co-authored-by: chengzifeng <chengzifeng@meituan.com>
1 parent b5c299f commit 72a2093

64 files changed

Lines changed: 4139 additions & 313 deletions

Some content is hidden

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

.gitignore

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ src/utils/vendor/
1313
# AI tool runtime directories
1414
.agents/
1515
.claude/
16-
.codex/
1716
.omx/
1817
.docs/task/
1918
# Binary / screenshot files (root only)
@@ -30,3 +29,12 @@ __pycache__/
3029
logs
3130

3231
data
32+
.omc
33+
.codex/*
34+
!.codex/agents/
35+
!.codex/agents/**
36+
!.codex/skills/
37+
!.codex/skills/**
38+
.codex/skills/.system/**
39+
!.codex/prompts/
40+
!.codex/prompts/**

docs/features/kairos.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# KAIROS — 常驻助手模式
22

33
> Feature Flag: `FEATURE_KAIROS=1`(及子 Feature)
4-
> 实现状态:核心框架完整,部分子模块为 stub
4+
> 实现状态:核心框架完整,部分子模块为 stub;proactive/sleep 节奏控制已可用
55
> 引用数:154(全库最大)
66
77
## 一、功能概述
@@ -74,8 +74,9 @@ KAIROS 在系统提示中注入两大段落:
7474

7575
SleepTool 是 KAIROS/Proactive 的节奏控制核心。工具描述让模型理解"休眠"概念:
7676
- 工具名:`Sleep`
77-
- 功能:等待指定时间后响应 tick prompt
77+
- 功能:等待指定时间后响应 tick prompt;若队列出现新工作或 proactive 被关闭,会提前唤醒
7878
-`<tick_tag>` 配合实现心跳式自主工作
79+
- 远程控制 surfaces 可通过 `automation_state` 看到 `standby` / `sleeping` 两种状态
7980

8081
### 3.3 Bridge 集成
8182

@@ -172,8 +173,10 @@ FEATURE_KAIROS=1 FEATURE_TOKEN_BUDGET=1 bun run dev
172173
| `src/assistant/AssistantSessionChooser.ts` || Session 选择 UI(stub) |
173174
| `src/tools/BriefTool/` || BriefTool 实现(stub) |
174175
| `src/tools/SleepTool/prompt.ts` | ~30 | SleepTool 工具提示 |
176+
| `src/tools/SleepTool/SleepTool.ts` | ~200 | 休眠/唤醒与 automation metadata |
175177
| `src/services/mcp/channelNotification.ts` | 5 | 频道消息接入(stub) |
176178
| `src/memdir/memdir.ts` || 记忆目录管理(stub) |
177179
| `src/constants/prompts.ts:552-554,843-914` | 72 | 系统提示注入 |
178180
| `src/components/tasks/src/tasks/DreamTask/` | 3 | Dream 任务(stub) |
179-
| `src/proactive/index.ts` || Proactive 核心(stub,KAIROS 共享) |
181+
| `src/proactive/index.ts` || Proactive 核心(KAIROS 共享) |
182+
| `src/utils/sessionState.ts` || 向 bridge/CCR 暴露 automation 状态 |

docs/features/proactive.md

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# PROACTIVE — 主动模式
22

33
> Feature Flag: `FEATURE_PROACTIVE=1`(与 `FEATURE_KAIROS=1` 共享功能)
4-
> 实现状态:核心模块全部 Stub,布线完整
4+
> 实现状态:核心循环与 SleepTool 已落地,部分外围文档仍在补齐
55
> 引用数:37
66
77
## 一、功能概述
@@ -21,13 +21,13 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
2121

2222
| 模块 | 文件 | 状态 | 说明 |
2323
|------|------|------|------|
24-
| 核心逻辑 | `src/proactive/index.ts` | **Stub** | `activateProactive()``deactivateProactive()``isProactiveActive() => false` |
24+
| 核心逻辑 | `src/proactive/index.ts` | **已实现** | `activateProactive()``deactivateProactive()``pause/resume``nextTickAt` 调度状态 |
2525
| SleepTool 提示 | `src/tools/SleepTool/prompt.ts` | **完整** | 工具提示定义(工具名:`Sleep`|
2626
| 命令注册 | `src/commands.ts:62-65` | **布线** | 动态加载 `./commands/proactive.js` |
2727
| 工具注册 | `src/tools.ts:26-28` | **布线** | SleepTool 动态加载 |
28-
| REPL 集成 | `src/screens/REPL.tsx` | **布线** | tick 驱动逻辑、占位符、页脚 UI |
28+
| REPL 集成 | `src/screens/REPL.tsx` | **已实现** | tick 驱动、standby/sleeping 状态、页脚与 bridge automation metadata 上报 |
2929
| 系统提示 | `src/constants/prompts.ts:860-914` | **完整** | 自主工作行为指令(~55 行详细 prompt) |
30-
| 会话存储 | `src/utils/sessionStorage.ts:4892-4912` | **布线** | tick 消息注入对话流 |
30+
| 远控状态镜像 | `src/utils/sessionState.ts` | **已实现** | 向 remote-control/CCR 暴露 `automation_state` 元数据 |
3131

3232
### 2.2 系统提示内容
3333

@@ -46,7 +46,7 @@ PROACTIVE 实现 Tick 驱动的自主代理。CLI 在用户不输入时也能持
4646
### 2.3 数据流
4747

4848
```
49-
activateProactive() [需要实现]
49+
activateProactive()
5050
5151
5252
Tick 调度器启动
@@ -62,20 +62,22 @@ Tick 调度器启动
6262
└── 无事可做 → 必须调用 SleepTool
6363
6464
65-
SleepTool 等待 [需要实现]
65+
SleepTool 等待
66+
67+
├── 用户插入新工作 / 队列中有命令 → 立即唤醒
68+
├── proactive 被关闭 → 立即中断
69+
└── 进入休眠时向远端 surfaces 上报 `automation_state = sleeping`
6670
6771
6872
下一个 tick 到达
6973
```
7074

71-
## 三、需要补全的内容
75+
## 三、当前行为补充
7276

73-
| 优先级 | 模块 | 工作量 | 说明 |
74-
|--------|------|--------|------|
75-
| 1 | `src/proactive/index.ts` || Tick 调度器、activate/deactivate 状态机、pause/resume |
76-
| 2 | `src/tools/SleepTool/SleepTool.ts` || 工具执行(等待指定时间后触发 tick) |
77-
| 3 | `src/commands/proactive.js` || `/proactive` 斜杠命令处理器 |
78-
| 4 | `src/hooks/useProactive.ts` || React hook(REPL 引用但不存在) |
77+
- `standby`:proactive 已开启,当前没有执行中的 turn,且已调度下一个 tick。
78+
- `sleeping`:模型显式调用 `SleepTool` 进入等待窗口。
79+
- remote-control/CCR 通过 `external_metadata.automation_state` 接收这两个状态,用于 Web UI 的 Autopilot 状态显示。
80+
- `SleepTool` 现在不是纯定时器;它会在共享命令队列出现新工作时提前醒来。
7981

8082
## 四、关键设计决策
8183

@@ -101,9 +103,11 @@ FEATURE_PROACTIVE=1 FEATURE_KAIROS=1 FEATURE_KAIROS_BRIEF=1 bun run dev
101103

102104
| 文件 | 职责 |
103105
|------|------|
104-
| `src/proactive/index.ts` | 核心逻辑(stub) |
106+
| `src/proactive/index.ts` | 核心逻辑与 next-tick 状态 |
105107
| `src/tools/SleepTool/prompt.ts` | SleepTool 工具提示 |
108+
| `src/tools/SleepTool/SleepTool.ts` | 休眠/唤醒执行逻辑 |
106109
| `src/constants/prompts.ts:860-914` | 自主工作系统提示 |
107-
| `src/screens/REPL.tsx` | REPL tick 集成 |
110+
| `src/screens/REPL.tsx` | REPL tick 集成与 automation 状态上报 |
108111
| `src/utils/sessionStorage.ts:4892-4912` | Tick 消息注入 |
112+
| `src/utils/sessionState.ts` | bridge/CCR metadata 镜像 |
109113
| `src/components/PromptInput/PromptInputFooterLeftSide.tsx` | 页脚 UI 状态 |

docs/features/remote-control-self-hosting.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,8 @@ claude bridge
174174
- 查看已注册的运行环境(environment 模式)
175175
- 创建和管理会话
176176
- 实时查看对话消息和工具调用
177+
- 查看 Autopilot 状态(`standby` / `sleeping`)和自动运行指示
178+
- 查看 authoritative task snapshots 驱动的 Tasks 面板
177179
- 审批 Claude Code 的工具权限请求
178180

179181
Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境。
@@ -215,6 +217,7 @@ Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境
215217
9. 双向通信
216218
CLI ──消息/工具调用结果──► RCS ──► Browser
217219
CLI ◄──权限审批/指令───── RCS ◄──── Browser
220+
CLI ──automation_state / task_state──► RCS ──► Browser
218221
219222
10. 心跳保活(每 20 秒)
220223
CLI ──POST /v1/environments/:id/work/:workId/heartbeat──► RCS
@@ -224,6 +227,13 @@ Web UI 使用 UUID 认证(无需用户账户),适合受信任网络环境
224227

225228
## 故障排查
226229

230+
### Web UI 看不到当前 Autopilot 状态
231+
232+
- `standby`:proactive 已开启,正在等待下一个 tick
233+
- `sleeping`:模型正在 `SleepTool` 等待窗口中
234+
235+
这两个状态通过 worker `external_metadata.automation_state` 上报。如果页面只显示普通 working spinner,优先检查 CLI 和 RCS 之间的 worker metadata PUT 是否成功。
236+
227237
### CLI 无法连接
228238

229239
```

packages/builtin-tools/src/tools/SleepTool/SleepTool.ts

Lines changed: 115 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@ import { z } from 'zod/v4'
33
import type { ToolResultBlockParam } from 'src/Tool.js'
44
import { buildTool } from 'src/Tool.js'
55
import { lazySchema } from 'src/utils/lazySchema.js'
6+
import { notifyAutomationStateChanged } from 'src/utils/sessionState.js'
67
import { SLEEP_TOOL_NAME, DESCRIPTION, SLEEP_TOOL_PROMPT } from './prompt.js'
78

9+
const SLEEP_WAKE_CHECK_INTERVAL_MS = 500
10+
811
const inputSchema = lazySchema(() =>
912
z.strictObject({
1013
duration_seconds: z
@@ -19,6 +22,36 @@ type SleepInput = z.infer<InputSchema>
1922

2023
type SleepOutput = { slept_seconds: number; interrupted: boolean }
2124

25+
function isProactiveAutomationEnabled(): boolean {
26+
if (!(feature('PROACTIVE') || feature('KAIROS'))) {
27+
return false
28+
}
29+
30+
const mod =
31+
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
32+
return mod.isProactiveActive()
33+
}
34+
35+
function isProactiveSleepAllowed(): boolean {
36+
if (!(feature('PROACTIVE') || feature('KAIROS'))) {
37+
return true
38+
}
39+
40+
const mod =
41+
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
42+
return mod.isProactiveActive()
43+
}
44+
45+
function hasQueuedWakeSignal(): boolean {
46+
const queue =
47+
require('src/utils/messageQueueManager.js') as typeof import('src/utils/messageQueueManager.js')
48+
return queue.hasCommandsInQueue()
49+
}
50+
51+
function shouldInterruptSleep(): boolean {
52+
return !isProactiveSleepAllowed() || hasQueuedWakeSignal()
53+
}
54+
2255
export const SleepTool = buildTool({
2356
name: SLEEP_TOOL_NAME,
2457
searchHint: 'wait pause sleep rest idle duration timer',
@@ -42,6 +75,9 @@ export const SleepTool = buildTool({
4275
isReadOnly() {
4376
return true
4477
},
78+
interruptBehavior() {
79+
return 'cancel'
80+
},
4581

4682
userFacingName() {
4783
return SLEEP_TOOL_NAME
@@ -67,53 +103,84 @@ export const SleepTool = buildTool({
67103
},
68104

69105
async call(input: SleepInput, context) {
70-
// Refuse to sleep when proactive mode is off — prevents the model from
71-
// re-issuing Sleep after an interruption caused by /proactive disable.
72-
if (feature('PROACTIVE') || feature('KAIROS')) {
73-
const mod =
74-
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
75-
if (!mod.isProactiveActive()) {
76-
return {
77-
data: {
78-
slept_seconds: 0,
79-
interrupted: true,
80-
},
81-
}
106+
// Don't enter sleep if proactive was disabled or new work arrived while
107+
// the model was deciding to wait.
108+
if (shouldInterruptSleep()) {
109+
return {
110+
data: {
111+
slept_seconds: 0,
112+
interrupted: true,
113+
},
82114
}
83115
}
84116

85117
const { duration_seconds } = input
86118
const startTime = Date.now()
119+
const sleepUntil = startTime + duration_seconds * 1000
120+
121+
if (isProactiveAutomationEnabled()) {
122+
notifyAutomationStateChanged({
123+
enabled: true,
124+
phase: 'sleeping',
125+
next_tick_at: null,
126+
sleep_until: sleepUntil,
127+
})
128+
}
87129

88130
try {
89131
await new Promise<void>((resolve, reject) => {
90-
const timer = setTimeout(resolve, duration_seconds * 1000)
132+
let timer: ReturnType<typeof setTimeout> | null = null
133+
let wakeCheck: ReturnType<typeof setInterval> | null = null
134+
let settled = false
91135

92-
// Abort via user interrupt
93-
context.abortController.signal.addEventListener(
94-
'abort',
95-
() => {
136+
const cleanup = () => {
137+
if (timer !== null) {
96138
clearTimeout(timer)
97-
clearInterval(proactiveCheck)
98-
reject(new Error('interrupted'))
99-
},
100-
{ once: true },
101-
)
102-
103-
// Poll proactive state — if deactivated mid-sleep, interrupt early
104-
// so the user doesn't have to wait for the full duration.
105-
const proactiveCheck =
106-
feature('PROACTIVE') || feature('KAIROS')
107-
? setInterval(() => {
108-
const mod =
109-
require('src/proactive/index.js') as typeof import('src/proactive/index.js')
110-
if (!mod.isProactiveActive()) {
111-
clearTimeout(timer)
112-
clearInterval(proactiveCheck)
113-
reject(new Error('interrupted'))
114-
}
115-
}, 500)
116-
: (null as unknown as ReturnType<typeof setInterval>)
139+
timer = null
140+
}
141+
if (wakeCheck !== null) {
142+
clearInterval(wakeCheck)
143+
wakeCheck = null
144+
}
145+
context.abortController.signal.removeEventListener('abort', onAbort)
146+
}
147+
148+
const finish = () => {
149+
if (settled) return
150+
settled = true
151+
cleanup()
152+
resolve()
153+
}
154+
155+
const interrupt = () => {
156+
if (settled) return
157+
settled = true
158+
cleanup()
159+
reject(new Error('interrupted'))
160+
}
161+
162+
const onAbort = () => {
163+
interrupt()
164+
}
165+
166+
timer = setTimeout(finish, duration_seconds * 1000)
167+
168+
// Abort via user interrupt
169+
if (context.abortController.signal.aborted) {
170+
interrupt()
171+
return
172+
}
173+
context.abortController.signal.addEventListener('abort', onAbort, {
174+
once: true,
175+
})
176+
177+
// Poll proactive state and the shared command queue so new work can
178+
// wake Sleep without waiting for the full duration.
179+
wakeCheck = setInterval(() => {
180+
if (shouldInterruptSleep()) {
181+
interrupt()
182+
}
183+
}, SLEEP_WAKE_CHECK_INTERVAL_MS)
117184
})
118185
return {
119186
data: {
@@ -129,6 +196,17 @@ export const SleepTool = buildTool({
129196
interrupted: true,
130197
},
131198
}
199+
} finally {
200+
notifyAutomationStateChanged(
201+
isProactiveAutomationEnabled()
202+
? {
203+
enabled: true,
204+
phase: null,
205+
next_tick_at: null,
206+
sleep_until: null,
207+
}
208+
: null,
209+
)
132210
}
133211
},
134212
})

0 commit comments

Comments
 (0)