@bolloon/bolloon-agent 0.3.7 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,457 @@
1
+ /**
2
+ * agent-heartbeat.ts — 智能体社交心跳 + 生命周期 (2026-07-21, 重构于 2026-07-21)
3
+ *
4
+ * 目的: 让本地智能体拥有"心跳", 周期性宣告存活/能力, 并自主决定跟哪个远端智能体发起对话,
5
+ * 形成本地↔远端智能体的顺畅自动交流.
6
+ *
7
+ * ⚠️ 设计核心: 智能体生命周期 (避免"一直社交却毫无效果")
8
+ * 社交不是目的, 而是达成"目标"的手段. 因此本模块以一个 **目标驱动的状态机** 管理智能体:
9
+ *
10
+ * BOOTSTRAP ──start()──▶ DISCOVERING ──有存活 peer──▶ ENGAGING
11
+ * ▲ │ │
12
+ * │ │ 无目标/无存活 peer │ 目标达成 / 配额耗尽 / 无效果退避
13
+ * │ ▼ ▼
14
+ * └───────────────────── RESTING ◀─────────────────────┘
15
+ * │ (goalReevalMs 后重新评估, 重置配额再试一轮)
16
+ * ▼
17
+ * ENGAGING (新一轮)
18
+ *
19
+ * - 每个目标有配额 (maxInitiations) 与效果阈值 (effectThreshold): 达成即 RESTING, 不再闲聊.
20
+ * - 若连续多次发起却"毫无效果" (noEffectWindowMs 内无有效回复), 进入退避 RESTING (noEffectBackoffMs),
21
+ * 防止无限互 ping / 烧 LLM.
22
+ * - RESTING 不是消失: beacon 仍在发, 对端依旧能看到本智能体在线; 只是停止主动社交.
23
+ * - goalReevalMs 之后会重新评估目标 (重置配额) 再给一轮机会, 让智能体"活着"但不失控.
24
+ * - pause()/resume()/stop() 提供运行期控制; stop() 会清理全部定时器 (供全局 runtime 优雅关闭).
25
+ *
26
+ * 设计原则 (compile-first / 可测):
27
+ * - transport / decide / getPeers / self / getGoal / assessEffect 全部可注入, 不依赖真实网络或 LLM.
28
+ * - 生产环境: transport = p2p-outbox.sendOrQueue, decide = 本地 LLM 决策, getPeers = known_peers + remoteChannelCache.
29
+ * - 测试环境: 全部用 mock, 验证"beacon → 自主发起 → 远端回复(效果) → 目标达成 → RESTING".
30
+ *
31
+ * 协议 (复用 v3 P2P 的 {v:3, op, payload} 信封):
32
+ * - agent.heartbeat : 轻量 beacon, payload = {fromPublicKey, agentId, name, channels, ts}
33
+ * - agent.chat.send : 本地 agent 自主发起 (已有远端唤醒链路, server.ts:529 处理)
34
+ * - agent.chat.reply : 远端回复发回 (已有 SSE 链路, server.ts:1494 处理)
35
+ *
36
+ * 与全局 runtime 生命周期的集成:
37
+ * - stop() 清理定时器, 供 server.ts 的 cleanupAndExit (SIGTERM/SIGINT) 调用.
38
+ * - onActivity 回调喂给 Watchdog.recordActivity, 避免 24h 看门狗误判卡死.
39
+ * - 实例注册到 global.socialHeartbeat / global.agentHeartbeat, 供 HealthMonitor.checkHeartbeat 观测.
40
+ * - 暴露 getDiscoveredAgents() / isAntColonyEnabled() 兼容 HealthMonitor 契约.
41
+ */
42
+ const DEFAULTS = {
43
+ beaconIntervalMs: 30_000,
44
+ socialIntervalMs: 120_000,
45
+ cooldownMs: 10 * 60_000,
46
+ liveWindowMs: 3 * 30_000,
47
+ minAttemptsBeforeBackoff: 3,
48
+ noEffectWindowMs: 10 * 60_000,
49
+ noEffectBackoffMs: 30 * 60_000,
50
+ backoffFactor: 2,
51
+ maxSocialIntervalMs: 30 * 60_000,
52
+ goalReevalMs: 60 * 60_000,
53
+ };
54
+ const MAX_BACKOFF_LEVEL = 6;
55
+ export class AgentHeartbeat {
56
+ opts;
57
+ peerLiveness = new Map();
58
+ lastInitiated = new Map();
59
+ beaconTimer = null;
60
+ socialTimer = null;
61
+ started = false;
62
+ // === 生命周期状态 ===
63
+ phase = 'BOOTSTRAP';
64
+ goalRT = null;
65
+ /** 内置兜底目标 (无 getGoal 时使用, 带配额, 保证不会一直社交) */
66
+ builtinGoalRT = null;
67
+ backoffLevel = 0;
68
+ noEffectBackoffUntil = 0;
69
+ constructor(options) {
70
+ this.opts = {
71
+ self: options.self,
72
+ getPeers: options.getPeers,
73
+ transport: options.transport,
74
+ decide: options.decide,
75
+ getGoal: options.getGoal,
76
+ assessEffect: options.assessEffect,
77
+ onReply: options.onReply,
78
+ onPeerAlive: options.onPeerAlive,
79
+ onActivity: options.onActivity,
80
+ onLifecycleChange: options.onLifecycleChange,
81
+ beaconIntervalMs: options.beaconIntervalMs ?? DEFAULTS.beaconIntervalMs,
82
+ socialIntervalMs: options.socialIntervalMs ?? DEFAULTS.socialIntervalMs,
83
+ cooldownMs: options.cooldownMs ?? DEFAULTS.cooldownMs,
84
+ liveWindowMs: options.liveWindowMs ?? DEFAULTS.liveWindowMs,
85
+ minAttemptsBeforeBackoff: options.minAttemptsBeforeBackoff ?? DEFAULTS.minAttemptsBeforeBackoff,
86
+ noEffectWindowMs: options.noEffectWindowMs ?? DEFAULTS.noEffectWindowMs,
87
+ noEffectBackoffMs: options.noEffectBackoffMs ?? DEFAULTS.noEffectBackoffMs,
88
+ backoffFactor: options.backoffFactor ?? DEFAULTS.backoffFactor,
89
+ maxSocialIntervalMs: options.maxSocialIntervalMs ?? DEFAULTS.maxSocialIntervalMs,
90
+ goalReevalMs: options.goalReevalMs ?? DEFAULTS.goalReevalMs,
91
+ enabled: options.enabled ?? true,
92
+ socialEnabled: options.socialEnabled ?? (options.enabled ?? true),
93
+ };
94
+ }
95
+ isEnabled() {
96
+ return this.opts.enabled;
97
+ }
98
+ isSocialEnabled() {
99
+ return this.opts.enabled && this.opts.socialEnabled;
100
+ }
101
+ // ===================== 启动 / 停止 / 暂停 =====================
102
+ start() {
103
+ if (this.started || !this.opts.enabled)
104
+ return;
105
+ this.started = true;
106
+ this.setPhase('DISCOVERING');
107
+ this.beaconTimer = setInterval(() => {
108
+ this.tickBeacon().catch((e) => console.warn('[heartbeat] beacon tick 失败:', e?.message));
109
+ }, this.opts.beaconIntervalMs);
110
+ if (this.isSocialEnabled()) {
111
+ this.scheduleSocial();
112
+ }
113
+ // 立即发一次 beacon, 让对端尽快看到自己
114
+ this.tickBeacon().catch(() => { });
115
+ console.log(`[heartbeat] 社交心跳已启动 (beacon=${this.opts.beaconIntervalMs}ms` +
116
+ `${this.isSocialEnabled() ? `, social=${this.opts.socialIntervalMs}ms, cooldown=${this.opts.cooldownMs}ms` : ', social=关闭'} )`);
117
+ }
118
+ /** 优雅停止: 清理全部定时器 (供全局 runtime 的 SIGTERM/SIGINT 清理调用) */
119
+ stop() {
120
+ if (this.beaconTimer)
121
+ clearInterval(this.beaconTimer);
122
+ if (this.socialTimer)
123
+ clearTimeout(this.socialTimer);
124
+ this.beaconTimer = null;
125
+ this.socialTimer = null;
126
+ this.started = false;
127
+ this.setPhase('PAUSED');
128
+ console.log('[heartbeat] 社交心跳已停止 (定时器已清理)');
129
+ }
130
+ /** 暂停社交循环 (beacon 仍发, 智能体依旧在线可见, 只是停止主动聊天) */
131
+ pause() {
132
+ if (!this.started)
133
+ return;
134
+ if (this.socialTimer)
135
+ clearTimeout(this.socialTimer);
136
+ this.socialTimer = null;
137
+ this.setPhase('PAUSED');
138
+ console.log('[heartbeat] 社交循环已暂停 (仅保留 beacon)');
139
+ }
140
+ /** 从 PAUSED 恢复社交循环 */
141
+ resume() {
142
+ if (!this.started) {
143
+ this.start();
144
+ return;
145
+ }
146
+ if (this.phase !== 'PAUSED')
147
+ return;
148
+ this.setPhase('DISCOVERING');
149
+ if (this.isSocialEnabled())
150
+ this.scheduleSocial();
151
+ console.log('[heartbeat] 社交循环已恢复');
152
+ }
153
+ // ===================== beacon =====================
154
+ /** 周期性 beacon: 向每个已知 peer 宣告存活 + 自身渠道/能力 */
155
+ async tickBeacon() {
156
+ const self = await this.opts.self();
157
+ const peers = await this.opts.getPeers();
158
+ const payload = {
159
+ fromPublicKey: self.publicKey,
160
+ agentId: self.agentId,
161
+ name: self.name,
162
+ channels: self.channels,
163
+ ts: Date.now(),
164
+ };
165
+ for (const p of peers) {
166
+ if (p.publicKey === self.publicKey)
167
+ continue;
168
+ const r = await this.opts.transport
169
+ .send(p.publicKey, 'agent.heartbeat', payload)
170
+ .catch(() => 'FAILED');
171
+ if (r !== 'FAILED')
172
+ this.peerLiveness.set(p.publicKey, Date.now());
173
+ }
174
+ }
175
+ // ===================== 社交决策 tick (生命周期感知) =====================
176
+ /** 自适应 social 间隔 (退避时指数增长, 有上限) */
177
+ currentSocialInterval() {
178
+ const mult = Math.pow(this.opts.backoffFactor, this.backoffLevel);
179
+ return Math.min(this.opts.socialIntervalMs * mult, this.opts.maxSocialIntervalMs);
180
+ }
181
+ scheduleSocial() {
182
+ if (!this.started || !this.isSocialEnabled() || this.phase === 'PAUSED') {
183
+ this.socialTimer = null;
184
+ return;
185
+ }
186
+ this.socialTimer = setTimeout(() => {
187
+ this.tickSocial()
188
+ .catch((e) => console.warn('[heartbeat] social tick 失败:', e?.message))
189
+ .finally(() => {
190
+ if (this.started && this.phase !== 'PAUSED')
191
+ this.scheduleSocial();
192
+ });
193
+ }, this.currentSocialInterval());
194
+ }
195
+ /** 社交决策 tick: 先评估生命周期, 再决定是否对存活 peer 发起对话 */
196
+ async tickSocial() {
197
+ this.opts.onActivity?.();
198
+ const self = await this.opts.self();
199
+ const allPeers = (await this.opts.getPeers()).filter((p) => p.publicKey !== self.publicKey);
200
+ const now = Date.now();
201
+ const livePeers = allPeers.filter((p) => {
202
+ const seen = this.peerLiveness.get(p.publicKey) ?? p.lastSeen ?? 0;
203
+ return now - seen <= this.opts.liveWindowMs;
204
+ });
205
+ const goalRT = await this.resolveGoal();
206
+ // —— 生命周期评估: 决定本 tick 进入哪个阶段 ——
207
+ const evalResult = this.evaluateLifecycle(goalRT, livePeers.length, now);
208
+ if (evalResult.backoff) {
209
+ this.backoffLevel = Math.min(this.backoffLevel + 1, MAX_BACKOFF_LEVEL);
210
+ this.noEffectBackoffUntil = now + this.opts.noEffectBackoffMs;
211
+ }
212
+ if (evalResult.resetBackoff) {
213
+ this.backoffLevel = 0;
214
+ this.noEffectBackoffUntil = 0;
215
+ }
216
+ this.setPhase(evalResult.nextPhase);
217
+ if (this.phase === 'RESTING' || this.phase === 'PAUSED') {
218
+ console.log(`[heartbeat] 生命周期=${this.phase}, 跳过本次社交` +
219
+ `${this.noEffectBackoffUntil > now ? ` (无效果退避至 ${new Date(this.noEffectBackoffUntil).toLocaleTimeString()})` : ''}` +
220
+ ` (goal=${goalRT?.goal.description ?? '无'})`);
221
+ this.emitLifecycle();
222
+ return;
223
+ }
224
+ if (!this.opts.decide) {
225
+ this.setPhase('DISCOVERING');
226
+ this.emitLifecycle();
227
+ return;
228
+ }
229
+ if (livePeers.length === 0) {
230
+ this.setPhase('DISCOVERING');
231
+ console.log('[heartbeat] social tick: 没有存活 peer, 仅保持发现');
232
+ this.emitLifecycle();
233
+ return;
234
+ }
235
+ this.setPhase('ENGAGING');
236
+ const decision = await this.opts.decide({ self, peers: livePeers, goal: goalRT?.goal }).catch(() => ({ initiate: false }));
237
+ // 决策时即可声明目标达成
238
+ if (decision?.goalAchieved && goalRT) {
239
+ goalRT.achieved = true;
240
+ this.setPhase('RESTING');
241
+ console.log(`[heartbeat] 决策判定目标已达成 → RESTING: ${goalRT.goal.description}`);
242
+ this.emitLifecycle();
243
+ return;
244
+ }
245
+ if (!decision?.initiate) {
246
+ this.emitLifecycle();
247
+ return;
248
+ }
249
+ const { targetPeerPublicKey, targetChannelId, message } = decision;
250
+ if (!targetPeerPublicKey || !targetChannelId || !message) {
251
+ this.emitLifecycle();
252
+ return;
253
+ }
254
+ // 冷却: 避免对同一 peer 刷屏 / 无限互 ping
255
+ const last = this.lastInitiated.get(targetPeerPublicKey) ?? 0;
256
+ if (now - last < this.opts.cooldownMs) {
257
+ this.emitLifecycle();
258
+ return;
259
+ }
260
+ // 目标配额: 本目标已发起够多次则进入 RESTING
261
+ if (goalRT && goalRT.initiationsUsed >= goalRT.goal.maxInitiations) {
262
+ this.setPhase('RESTING');
263
+ console.log(`[heartbeat] 目标配额用尽 (${goalRT.initiationsUsed}/${goalRT.goal.maxInitiations}) → RESTING`);
264
+ this.emitLifecycle();
265
+ return;
266
+ }
267
+ const r = await this.opts.transport
268
+ .send(targetPeerPublicKey, 'agent.chat.send', {
269
+ channelId: targetChannelId,
270
+ text: message,
271
+ fromPublicKey: self.publicKey,
272
+ })
273
+ .catch(() => 'FAILED');
274
+ if (r !== 'FAILED') {
275
+ this.lastInitiated.set(targetPeerPublicKey, now);
276
+ if (goalRT) {
277
+ goalRT.initiationsUsed++;
278
+ goalRT.lastInitiateAt = now;
279
+ }
280
+ console.log(`[heartbeat] 主动发起对话 → ${targetPeerPublicKey.slice(0, 8)}… (channel=${targetChannelId}): "${message.slice(0, 40)}..."` +
281
+ `${goalRT ? ` [目标 ${goalRT.initiationsUsed}/${goalRT.goal.maxInitiations}]` : ''}`);
282
+ }
283
+ else {
284
+ console.warn(`[heartbeat] 主动发起失败 (peer ${targetPeerPublicKey.slice(0, 8)}… 不在线?)`);
285
+ }
286
+ this.emitLifecycle();
287
+ }
288
+ // ===================== 生命周期评估 =====================
289
+ makeGoalRuntime(g) {
290
+ return {
291
+ goal: { ...g, createdAt: g.createdAt ?? Date.now() },
292
+ initiationsUsed: 0,
293
+ effectfulReplies: 0,
294
+ achieved: false,
295
+ startedAt: Date.now(),
296
+ };
297
+ }
298
+ /** 解析当前目标: 复用未达成/未过期的目标; 否则尝试 getGoal(); 再否则用内置 discovery 目标 (带配额) */
299
+ async resolveGoal() {
300
+ if (this.goalRT && !this.goalRT.achieved) {
301
+ const ttl = this.goalRT.goal.ttlMs ?? Infinity;
302
+ if (Date.now() - this.goalRT.startedAt < ttl)
303
+ return this.goalRT;
304
+ }
305
+ const g = await this.opts.getGoal?.();
306
+ if (g) {
307
+ this.goalRT = this.makeGoalRuntime(g);
308
+ return this.goalRT;
309
+ }
310
+ // 兜底: 内置 discovery 目标, 保证不会"一直社交" (有配额)
311
+ if (!this.builtinGoalRT || this.builtinGoalRT.achieved) {
312
+ this.builtinGoalRT = this.makeGoalRuntime({
313
+ id: 'builtin-discovery',
314
+ description: '与已知 peer 建立并维持协作关系',
315
+ maxInitiations: 5,
316
+ effectThreshold: 2,
317
+ });
318
+ }
319
+ this.goalRT = this.builtinGoalRT;
320
+ return this.goalRT;
321
+ }
322
+ /**
323
+ * 生命周期转移决策.
324
+ * 返回下一个阶段 + 是否触发退避 + 是否重置退避.
325
+ */
326
+ evaluateLifecycle(goalRT, liveCount, now) {
327
+ // 无目标 → 用内置 discovery 兜底; 这里 goalRT 总有值
328
+ if (!goalRT)
329
+ return { nextPhase: 'RESTING', backoff: false, resetBackoff: false };
330
+ // 目标已达成 → 休息
331
+ if (goalRT.achieved)
332
+ return { nextPhase: 'RESTING', backoff: false, resetBackoff: true };
333
+ // 配额耗尽 → 休息
334
+ if (goalRT.initiationsUsed >= goalRT.goal.maxInitiations) {
335
+ return { nextPhase: 'RESTING', backoff: false, resetBackoff: false };
336
+ }
337
+ // 无效果退避中 → 保持休息
338
+ if (now < this.noEffectBackoffUntil) {
339
+ return { nextPhase: 'RESTING', backoff: false, resetBackoff: false };
340
+ }
341
+ // 连续多次发起却长时间无效果 → 进入退避
342
+ const sinceEffect = goalRT.lastEffectAt ? now - goalRT.lastEffectAt : now - goalRT.startedAt;
343
+ if (goalRT.initiationsUsed >= this.opts.minAttemptsBeforeBackoff && sinceEffect > this.opts.noEffectWindowMs) {
344
+ return { nextPhase: 'RESTING', backoff: true, resetBackoff: false };
345
+ }
346
+ // RESTING 重新评估: 超过 goalReevalMs 且有存活 peer → 重置配额, 给新一轮机会
347
+ if (this.phase === 'RESTING' && liveCount > 0) {
348
+ const sinceLast = goalRT.lastInitiateAt ? now - goalRT.lastInitiateAt : now - goalRT.startedAt;
349
+ if (sinceLast > this.opts.goalReevalMs) {
350
+ goalRT.initiationsUsed = 0;
351
+ return { nextPhase: 'ENGAGING', backoff: false, resetBackoff: true };
352
+ }
353
+ }
354
+ return { nextPhase: liveCount > 0 ? 'ENGAGING' : 'DISCOVERING', backoff: false, resetBackoff: false };
355
+ }
356
+ // ===================== 入站处理 =====================
357
+ /** 入站处理: 由 server.ts 的 data 事件处理器在收到 agent.heartbeat / agent.chat.reply 时调用 */
358
+ handleIncoming(op, payload, fromPublicKey) {
359
+ if (op === 'agent.heartbeat') {
360
+ this.peerLiveness.set(fromPublicKey, Date.now());
361
+ const info = {
362
+ publicKey: fromPublicKey,
363
+ name: payload?.name,
364
+ agentId: payload?.agentId,
365
+ channels: Array.isArray(payload?.channels) ? payload.channels : [],
366
+ lastSeen: Date.now(),
367
+ };
368
+ this.opts.onPeerAlive?.(info);
369
+ return;
370
+ }
371
+ if (op === 'agent.chat.reply') {
372
+ this.opts.onReply?.({
373
+ fromPublicKey,
374
+ channelId: payload?.channelId || '',
375
+ text: payload?.text || '',
376
+ });
377
+ // 效果度量: 一条有效回复推进目标 → 累计效果, 解除退避; 达阈值则目标达成 → RESTING
378
+ const goalRT = this.goalRT;
379
+ if (goalRT && !goalRT.achieved) {
380
+ const assessment = this.opts.assessEffect
381
+ ? this.opts.assessEffect({ goal: goalRT.goal, fromPublicKey, replyText: payload?.text || '' })
382
+ : { advanced: !!(payload?.text && String(payload.text).trim().length > 0), achievedGoal: false };
383
+ if (assessment.advanced) {
384
+ goalRT.effectfulReplies++;
385
+ goalRT.lastEffectAt = Date.now();
386
+ this.backoffLevel = 0;
387
+ this.noEffectBackoffUntil = 0;
388
+ if (assessment.achievedGoal || goalRT.effectfulReplies >= goalRT.goal.effectThreshold) {
389
+ goalRT.achieved = true;
390
+ console.log(`[heartbeat] 目标达成 (效果 ${goalRT.effectfulReplies}/${goalRT.goal.effectThreshold}) → RESTING: ${goalRT.goal.description}`);
391
+ this.setPhase('RESTING');
392
+ }
393
+ }
394
+ }
395
+ return;
396
+ }
397
+ }
398
+ // ===================== 运行期控制 / 观测 =====================
399
+ /** 设定新目标 (owner 可通过 RPC 注入). 会重置运行期状态 */
400
+ setGoal(goal) {
401
+ this.goalRT = this.makeGoalRuntime(goal);
402
+ this.backoffLevel = 0;
403
+ this.noEffectBackoffUntil = 0;
404
+ if (this.started && this.phase === 'RESTING')
405
+ this.setPhase('DISCOVERING');
406
+ console.log(`[heartbeat] 设定新目标: ${goal.description} (配额 ${goal.maxInitiations}, 效果阈值 ${goal.effectThreshold})`);
407
+ this.emitLifecycle();
408
+ }
409
+ setPhase(p) {
410
+ if (this.phase === p)
411
+ return;
412
+ this.phase = p;
413
+ this.emitLifecycle();
414
+ }
415
+ emitLifecycle() {
416
+ this.opts.onLifecycleChange?.(this.phase, this.getLifecycle());
417
+ }
418
+ /** 当前生命周期快照 */
419
+ getLifecycle() {
420
+ return {
421
+ phase: this.phase,
422
+ started: this.started,
423
+ livePeers: this.peerLiveness.size,
424
+ backoffLevel: this.backoffLevel,
425
+ socialIntervalMs: this.currentSocialInterval(),
426
+ noEffectBackoffUntil: this.noEffectBackoffUntil,
427
+ goal: this.goalRT
428
+ ? {
429
+ id: this.goalRT.goal.id,
430
+ description: this.goalRT.goal.description,
431
+ initiationsUsed: this.goalRT.initiationsUsed,
432
+ maxInitiations: this.goalRT.goal.maxInitiations,
433
+ effectfulReplies: this.goalRT.effectfulReplies,
434
+ effectThreshold: this.goalRT.goal.effectThreshold,
435
+ achieved: this.goalRT.achieved,
436
+ }
437
+ : undefined,
438
+ };
439
+ }
440
+ /** 调试: 当前 peer 存活表 */
441
+ getLiveness() {
442
+ return Array.from(this.peerLiveness.entries()).map(([publicKey, lastSeen]) => ({ publicKey, lastSeen }));
443
+ }
444
+ /** 调试: 上次主动发起时间 */
445
+ getLastInitiated(publicKey) {
446
+ return this.lastInitiated.get(publicKey);
447
+ }
448
+ // ===================== 兼容 HealthMonitor 契约 =====================
449
+ /** 供 HealthMonitor.checkHeartbeat 观测已发现的智能体 */
450
+ getDiscoveredAgents() {
451
+ return this.getLiveness().map((l) => ({ publicKey: l.publicKey, lastSeen: l.lastSeen, channels: [] }));
452
+ }
453
+ /** 供 HealthMonitor.checkHeartbeat 观测社交是否开启 */
454
+ isAntColonyEnabled() {
455
+ return this.isSocialEnabled();
456
+ }
457
+ }