Skip to content

Commit 408335f

Browse files
committed
feat: activate Evolution Lab scheduled discovery
1 parent b11e89f commit 408335f

12 files changed

Lines changed: 656 additions & 43 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@
22

33
本项目遵循“先记录真实能力,再发布版本”的原则。未完成签名、公证和真实端到端基准的构建均标记为 Preview。
44

5+
## 1.0.2
6+
7+
### Fixed
8+
9+
- Evolution Lab 的“定时提出候选”不再只是保存开关;AgentOS 现会在应用空闲 10 分钟后执行首次本地扫描,之后每 6 小时最多扫描一次;
10+
- 自动发现从近 30 天任务快照中识别失败恢复或重复工作流信号,并对同一批信号去重;
11+
- 候选生成不调用模型、不预留 Token、不创建插件沙箱,也不会自动安装;准备、模型运行、验证和采纳仍需用户明确操作;
12+
- Evolution Lab 会显示最近扫描、下一次扫描窗口与本轮状态,并在后台发现候选时实时刷新。
13+
514
## 0.9.0-preview.29
615

716
### Added

Nova.AgentOS.Bridge/Program.cs

Lines changed: 97 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,10 @@ internal sealed class AgentOsBridgeHost : IDisposable
114114
private readonly ConcurrentDictionary<string, byte> _agentRuns =
115115
new(StringComparer.OrdinalIgnoreCase);
116116
private readonly SemaphoreSlim _bootGate = new(1, 1);
117+
private readonly CancellationTokenSource _lifetime = new();
118+
private Task? _evolutionDiscoveryLoop;
119+
private long _lastForegroundActivityUnixMs =
120+
DateTimeOffset.Now.ToUnixTimeMilliseconds();
117121
private bool _booted;
118122

119123
public AgentOsBridgeHost(Func<string, object, Task> publish)
@@ -125,7 +129,15 @@ public AgentOsBridgeHost(Func<string, object, Task> publish)
125129
}
126130

127131
public async Task<object?> ExecuteAsync(string method, JsonObject parameters)
128-
=> method switch
132+
{
133+
if (IsForegroundActivity(method))
134+
{
135+
Interlocked.Exchange(
136+
ref _lastForegroundActivityUnixMs,
137+
DateTimeOffset.Now.ToUnixTimeMilliseconds());
138+
}
139+
140+
return method switch
129141
{
130142
"boot" => await BootAsync(),
131143
"health" => await HealthAsync(),
@@ -165,6 +177,7 @@ public AgentOsBridgeHost(Func<string, object, Task> publish)
165177
RequiredString(parameters, "id")),
166178
_ => throw new InvalidOperationException($"Unknown bridge method: {method}")
167179
};
180+
}
168181

169182
private async Task<object> BootAsync()
170183
{
@@ -187,6 +200,8 @@ await _kernel.ReportServiceAsync(
187200
"Electron bridge lease layer active",
188201
boot.BootId);
189202
_booted = true;
203+
_evolutionDiscoveryLoop = RunEvolutionDiscoveryLoopAsync(
204+
_lifetime.Token);
190205
}
191206
}
192207
finally
@@ -197,6 +212,68 @@ await _kernel.ReportServiceAsync(
197212
return ProjectKernel();
198213
}
199214

215+
private async Task RunEvolutionDiscoveryLoopAsync(
216+
CancellationToken cancellationToken)
217+
{
218+
using var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
219+
try
220+
{
221+
while (await timer.WaitForNextTickAsync(cancellationToken))
222+
{
223+
try
224+
{
225+
if (_active.Count > 0 || _agentRuns.Count > 0)
226+
{
227+
continue;
228+
}
229+
230+
var lastActivity = DateTimeOffset.FromUnixTimeMilliseconds(
231+
Interlocked.Read(ref _lastForegroundActivityUnixMs));
232+
if (DateTimeOffset.Now - lastActivity < TimeSpan.FromMinutes(10))
233+
{
234+
continue;
235+
}
236+
237+
var discovery = await _evolutionLab.TryDiscoverCandidateAsync(
238+
_snapshots.LoadAll(),
239+
cancellationToken: cancellationToken);
240+
if (!discovery.Scanned)
241+
{
242+
continue;
243+
}
244+
245+
await _publish("evolution_event", new
246+
{
247+
kind = discovery.Candidate is null ? "scan" : "candidate",
248+
candidateId = discovery.Candidate?.Id,
249+
objective = discovery.Candidate?.Objective,
250+
discovery.Snapshot.DiscoveryStatus,
251+
discovery.Snapshot.LastDiscoveryAt,
252+
discovery.Snapshot.NextDiscoveryAt
253+
});
254+
}
255+
catch (OperationCanceledException) when (
256+
cancellationToken.IsCancellationRequested)
257+
{
258+
throw;
259+
}
260+
catch (Exception exception)
261+
{
262+
await _publish("evolution_event", new
263+
{
264+
kind = "error",
265+
discoveryStatus = $"自动发现本轮失败,将在下个检查周期重试:{exception.Message}",
266+
lastDiscoveryAt = DateTimeOffset.Now
267+
});
268+
}
269+
}
270+
}
271+
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
272+
{
273+
// Normal bridge shutdown.
274+
}
275+
}
276+
200277
private async Task<object> HealthAsync()
201278
{
202279
await BootAsync();
@@ -1198,11 +1275,29 @@ private static TaskState NormalizeRecoveredState(TaskState state)
11981275
? TaskState.Paused
11991276
: state;
12001277

1278+
private static bool IsForegroundActivity(string method)
1279+
=> method is
1280+
"start_task"
1281+
or "run_agent"
1282+
or "verify_result"
1283+
or "task_event"
1284+
or "complete_task"
1285+
or "propose_evolution"
1286+
or "prepare_evolution"
1287+
or "evaluate_evolution"
1288+
or "adopt_evolution"
1289+
or "reject_evolution"
1290+
or "configure_evolution_lab";
1291+
12011292
private static string NormalizeRecoveredStage(TaskState state, string stage)
12021293
=> state is TaskState.Running or TaskState.Waiting or TaskState.BudgetExhausted
12031294
? "Previous host stopped; task is safely paused"
12041295
: stage;
12051296

12061297
public void Dispose()
1207-
=> _supervisor.Dispose();
1298+
{
1299+
_lifetime.Cancel();
1300+
_supervisor.Dispose();
1301+
_lifetime.Dispose();
1302+
}
12081303
}

NovaDesktop.Electron/electron/main.cjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,6 +1109,9 @@ function createBridgeClient() {
11091109
if (eventName === "agent_event" && mainWindow && !mainWindow.isDestroyed()) {
11101110
mainWindow.webContents.send("nova:agent-event", payload);
11111111
}
1112+
if (eventName === "evolution_event" && mainWindow && !mainWindow.isDestroyed()) {
1113+
mainWindow.webContents.send("nova:evolution-event", payload);
1114+
}
11121115
};
11131116
return client;
11141117
}

NovaDesktop.Electron/electron/preload.cjs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@ contextBridge.exposeInMainWorld("nova", {
5050
prepareEvolution: (request) => invoke("nova:prepare-evolution", request),
5151
evaluateEvolution: (request) => invoke("nova:evaluate-evolution", request),
5252
adoptEvolution: (request) => invoke("nova:adopt-evolution", request),
53-
rejectEvolution: (request) => invoke("nova:reject-evolution", request)
53+
rejectEvolution: (request) => invoke("nova:reject-evolution", request),
54+
onEvolutionEvent: (listener) => {
55+
const handler = (_event, payload) => listener(payload);
56+
ipcRenderer.on("nova:evolution-event", handler);
57+
return () => ipcRenderer.removeListener("nova:evolution-event", handler);
58+
}
5459
},
5560
window: {
5661
minimize: () => invoke("nova:window-minimize"),

NovaDesktop.Electron/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

NovaDesktop.Electron/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "nova-agentos-electron",
3-
"version": "1.0.1",
3+
"version": "1.0.2",
44
"private": true,
55
"author": "NOVA AgentOS Project",
66
"description": "NOVA AgentOS next-generation desktop shell",

NovaDesktop.Electron/src/App.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import type {
4040
BootInfo,
4141
CapabilityState,
4242
DesktopSnapshot,
43+
EvolutionDiscoveryEvent,
4344
EvolutionLabState,
4445
LivingMemoryState,
4546
ExecutionMode,
@@ -100,6 +101,23 @@ function now() {
100101
});
101102
}
102103

104+
function formatLocalDateTime(value?: string | null) {
105+
if (!value) return "尚未安排";
106+
return new Date(value).toLocaleString("zh-CN", {
107+
month: "2-digit",
108+
day: "2-digit",
109+
hour: "2-digit",
110+
minute: "2-digit"
111+
});
112+
}
113+
114+
function formatDiscoveryWindow(value?: string | null) {
115+
if (!value) return "等待首次扫描";
116+
return new Date(value).getTime() <= Date.now()
117+
? "已到扫描窗口,等待应用空闲"
118+
: `下次窗口 ${formatLocalDateTime(value)}`;
119+
}
120+
103121
function readableRunError(error: unknown) {
104122
const raw = error instanceof Error ? error.message : "任务执行失败";
105123
return raw
@@ -610,6 +628,24 @@ function App() {
610628
};
611629
}, []);
612630

631+
useEffect(() => {
632+
const unsubscribe = window.nova.growth.onEvolutionEvent(
633+
async (event: EvolutionDiscoveryEvent) => {
634+
try {
635+
setEvolutionLab(await window.nova.growth.getEvolutionLab());
636+
} catch {
637+
// The persisted state will be loaded the next time the growth hub opens.
638+
}
639+
if (event.kind === "candidate") {
640+
setNotice("Evolution Lab 已生成一个本地改进候选,等待你的审阅");
641+
} else if (event.kind === "error") {
642+
setNotice(event.discoveryStatus);
643+
}
644+
}
645+
);
646+
return unsubscribe;
647+
}, []);
648+
613649
useEffect(() => {
614650
if (!running) {
615651
setRuntimePulse("等待下一项任务");
@@ -1823,6 +1859,22 @@ function App() {
18231859
</form>
18241860
)}
18251861

1862+
{evolutionLab && (
1863+
<div className="evolution-discovery-status">
1864+
<div>
1865+
<strong>{evolutionLab.discoveryStatus}</strong>
1866+
<small>
1867+
最近扫描 {formatLocalDateTime(evolutionLab.lastDiscoveryAt)}
1868+
</small>
1869+
</div>
1870+
<span>
1871+
{evolutionLab.policy.scheduledDiscoveryEnabled
1872+
? formatDiscoveryWindow(evolutionLab.nextDiscoveryAt)
1873+
: "自动发现未启用"}
1874+
</span>
1875+
</div>
1876+
)}
1877+
18261878
<form
18271879
className="evolution-proposal"
18281880
onSubmit={async (event) => {

NovaDesktop.Electron/src/styles.css

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2222,6 +2222,41 @@ select:focus-visible {
22222222
cursor: pointer;
22232223
}
22242224

2225+
.evolution-discovery-status {
2226+
display: flex;
2227+
align-items: center;
2228+
justify-content: space-between;
2229+
gap: 12px;
2230+
margin: -2px 0 12px;
2231+
padding: 9px 11px;
2232+
border-left: 2px solid #7f9270;
2233+
background: #171a15;
2234+
}
2235+
2236+
.evolution-discovery-status > div {
2237+
display: grid;
2238+
gap: 3px;
2239+
min-width: 0;
2240+
}
2241+
2242+
.evolution-discovery-status strong {
2243+
overflow: hidden;
2244+
color: #cbd3c2;
2245+
font-size: 9px;
2246+
text-overflow: ellipsis;
2247+
white-space: nowrap;
2248+
}
2249+
2250+
.evolution-discovery-status small,
2251+
.evolution-discovery-status > span {
2252+
color: #7e8876;
2253+
font-size: 8px;
2254+
}
2255+
2256+
.evolution-discovery-status > span {
2257+
flex: 0 0 auto;
2258+
}
2259+
22252260
.evolution-proposal {
22262261
display: grid;
22272262
grid-template-columns: minmax(0, 1fr) auto;

NovaDesktop.Electron/src/types.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,19 @@ export interface EvolutionLabState {
204204
usedTokensThisMonth: number;
205205
remainingTokensThisMonth: number;
206206
usageMonth: string;
207+
lastDiscoveryAt?: string | null;
208+
nextDiscoveryAt?: string | null;
209+
discoveryStatus: string;
210+
lastDiscoveryCandidateId?: string | null;
211+
}
212+
213+
export interface EvolutionDiscoveryEvent {
214+
kind: "scan" | "candidate" | "error";
215+
candidateId?: string | null;
216+
objective?: string | null;
217+
discoveryStatus: string;
218+
lastDiscoveryAt?: string | null;
219+
nextDiscoveryAt?: string | null;
207220
}
208221

209222
export interface DesktopSnapshot {
@@ -324,6 +337,7 @@ export interface NovaApi {
324337
evaluateEvolution(request: { id: string }): Promise<EvolutionLabState>;
325338
adoptEvolution(request: { id: string }): Promise<EvolutionLabState>;
326339
rejectEvolution(request: { id: string }): Promise<EvolutionLabState>;
340+
onEvolutionEvent(listener: (event: EvolutionDiscoveryEvent) => void): () => void;
327341
};
328342
window: {
329343
minimize(): Promise<void>;

0 commit comments

Comments
 (0)