ppxans-harness 2.4.0 → 3.2.0
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.
- package/LICENSE +201 -201
- package/README.md +218 -265
- package/bin/ppx-channels.js +2 -2
- package/bin/ppx-serve.js +5 -5
- package/bin/ppx-setup.js +124 -0
- package/bin/ppx-web.js +140 -0
- package/bin/ppx.js +2 -2
- package/config/identity.md +6 -6
- package/config/ishiki.md +16 -16
- package/config/ppx.json +15 -151
- package/config/ppx.json.example +143 -0
- package/package.json +17 -10
- package/skills/.usage.json +6 -0
- package/skills/agent-professional-training/SKILL.md +94 -0
- package/skills/brainstorm/SKILL.md +24 -0
- package/skills/cupid-lover-comms/SKILL.md +37 -0
- package/skills/debug/SKILL.md +26 -0
- package/skills/plan/SKILL.md +25 -0
- package/skills/ponytail/SKILL.md +25 -0
- package/skills/ppx-memory/SKILL.md +91 -0
- package/skills/ppx-memory/scripts/cli.js +192 -0
- package/skills/ppx-memory/scripts/experience.js +133 -0
- package/skills/ppx-memory/scripts/fact-store.js +842 -0
- package/skills/ppx-memory/scripts/l0.js +52 -0
- package/skills/ppx-memory/scripts/l2.js +146 -0
- package/skills/ppx-memory/scripts/l3.js +112 -0
- package/skills/ppx-memory/scripts/memory-ticker.js +238 -0
- package/skills/ppx-memory/scripts/pii.js +42 -0
- package/skills/ppx-memory/scripts/schema.js +80 -0
- package/skills/ppx-memory/scripts/session.js +398 -0
- package/skills/ppx-memory/scripts/similarity.js +43 -0
- package/skills/ppx-memory/scripts/store.js +116 -0
- package/skills/ppx-memory/scripts/wal.js +38 -0
- package/skills/ppx-selfheal/SKILL.md +24 -0
- package/skills/ppx-selfheal/scripts/cli.js +80 -0
- package/skills/ppx-selfheal/scripts/healer.js +184 -0
- package/skills/ppx-selfheal/scripts/logger.js +17 -0
- package/skills/ppx-selfheal/scripts/store.js +116 -0
- package/skills/prompt-depth-kit/SKILL.md +28 -0
- package/skills/session-naming/SKILL.md +36 -0
- package/skills/verify/SKILL.md +25 -0
- package/src/agent/index.js +1340 -717
- package/src/agent/prompts.js +47 -3
- package/src/aml-server.js +197 -151
- package/src/ans/eviction.js +123 -143
- package/src/ans/guard.js +159 -120
- package/src/ans/lifecycle.js +96 -93
- package/src/ans/proactive.js +112 -129
- package/src/ans/reward.js +95 -111
- package/src/ans/values.js +15 -15
- package/src/audit/audit-chain.js +43 -7
- package/src/audit/verifier.js +157 -120
- package/src/bus/circuit-breaker.js +9 -1
- package/src/bus/runtime-bus.js +107 -93
- package/src/channels/base.js +57 -34
- package/src/channels/feishu.js +118 -126
- package/src/channels/http.js +1090 -592
- package/src/channels/index.js +111 -110
- package/src/channels/log.js +29 -29
- package/src/channels/wechat-crypto.js +73 -74
- package/src/channels/wechat.js +191 -197
- package/src/channels/workspace.js +94 -0
- package/src/channels-cli.js +126 -124
- package/src/cli.js +129 -120
- package/src/commands/index.js +142 -0
- package/src/config/channels.js +137 -170
- package/src/config/index.js +277 -224
- package/src/config/placeholder.js +31 -0
- package/src/config/providers.js +148 -188
- package/src/config/settings.js +156 -182
- package/src/core/policy.js +69 -18
- package/src/core/trace.js +8 -6
- package/src/edit/editblock.js +266 -0
- package/src/edit/snapshot.js +67 -0
- package/src/evidence/index.js +153 -0
- package/src/evolve/playbook.js +9 -10
- package/src/hooks/index.js +113 -0
- package/src/llm/client.js +188 -446
- package/src/llm/dsml.js +74 -74
- package/src/llm/embedder.js +41 -35
- package/src/llm/fence.js +52 -105
- package/src/llm/index.js +4 -4
- package/src/llm/local-embedder.js +94 -0
- package/src/llm/presets.js +113 -0
- package/src/llm/pricing.js +93 -0
- package/src/llm/retry.js +73 -73
- package/src/llm/router.js +92 -97
- package/src/mcp/admin.js +326 -0
- package/src/mcp/client.js +487 -375
- package/src/mcp/http.js +203 -0
- package/src/mcp/index.js +116 -116
- package/src/mcp/server.js +392 -0
- package/src/mcp/tasks.js +133 -0
- package/src/memory/asset-hub.js +6 -11
- package/src/memory/canvas.js +2 -5
- package/src/memory/compaction.js +28 -28
- package/src/memory/experience.js +133 -122
- package/src/memory/fact-store.js +914 -698
- package/src/memory/failure-episode.js +20 -11
- package/src/memory/fork.js +17 -8
- package/src/memory/index.js +8 -6
- package/src/memory/l0.js +53 -52
- package/src/memory/l2.js +145 -130
- package/src/memory/l3.js +111 -111
- package/src/memory/legion-board.js +71 -0
- package/src/memory/memory-ticker.js +239 -240
- package/src/memory/session.js +398 -397
- package/src/memory/sqlite-store.js +581 -0
- package/src/mode/blackboard.js +49 -49
- package/src/mode/graph.js +42 -41
- package/src/mode/index.js +64 -64
- package/src/mode/legion.js +54 -51
- package/src/mode/plan-exec.js +50 -50
- package/src/mode/router.js +28 -40
- package/src/orchestrator/agent-worker.js +69 -69
- package/src/orchestrator/dag.js +90 -83
- package/src/orchestrator/experts.js +76 -0
- package/src/orchestrator/index.js +1 -1
- package/src/orchestrator/legion.js +179 -187
- package/src/orchestrator/supervisor.js +6 -8
- package/src/permissions/index.js +378 -0
- package/src/persona/index.js +28 -29
- package/src/plugin/builtin.js +313 -212
- package/src/plugin/context.js +80 -79
- package/src/plugin/index.js +62 -62
- package/src/plugin/v3.js +73 -0
- package/src/protocol/index.js +148 -0
- package/src/repomap/index.js +309 -0
- package/src/review/index.js +393 -0
- package/src/seam/registry.js +3 -0
- package/src/seam/shell.js +55 -55
- package/src/security/injection.js +79 -0
- package/src/selfheal/evolve.js +67 -67
- package/src/selfheal/healer.js +184 -167
- package/src/selfheal/run.js +9 -9
- package/src/server.js +63 -60
- package/src/services/diagnose.js +180 -0
- package/src/services/learning-service.js +9 -0
- package/src/services/memory-health.js +34 -6
- package/src/services/memory-service.js +42 -9
- package/src/services/triage.js +138 -0
- package/src/session/parts.js +76 -0
- package/src/session/projection.js +73 -0
- package/src/session/rollout.js +54 -0
- package/src/session/turn.js +137 -0
- package/src/skills/lint.js +72 -0
- package/src/skills/loader.js +231 -150
- package/src/skills/search.js +58 -0
- package/src/skills/verify.js +95 -100
- package/src/tools/advanced.js +388 -352
- package/src/tools/builtin.js +384 -297
- package/src/tools/catalog.js +283 -159
- package/src/tools/command-guard.js +112 -112
- package/src/tools/custom.js +47 -47
- package/src/tools/delegate.js +383 -297
- package/src/tools/document.js +254 -253
- package/src/tools/git.js +151 -0
- package/src/tools/governance.js +47 -20
- package/src/tools/index.js +16 -11
- package/src/tools/methods.js +178 -178
- package/src/tools/ocr.js +59 -59
- package/src/tools/sandbox-worker.js +40 -0
- package/src/tools/sandbox.js +92 -0
- package/src/tools/seam.js +162 -125
- package/src/tools/selfmod.js +196 -176
- package/src/tools/v3.js +225 -0
- package/src/tools/vad.js +176 -0
- package/src/tools/voice.js +238 -0
- package/src/utils/async.js +14 -0
- package/src/utils/config-file.js +53 -0
- package/src/utils/crashguard.js +88 -0
- package/src/utils/http.js +53 -0
- package/src/utils/id.js +8 -0
- package/src/utils/json-state.js +33 -0
- package/src/utils/logger.js +17 -17
- package/src/utils/ndjson.js +25 -0
- package/src/utils/pii.js +42 -42
- package/src/utils/rate-limit.js +50 -0
- package/src/utils/schema.js +80 -0
- package/src/utils/similarity.js +43 -0
- package/src/utils/store.js +170 -108
- package/src/utils/text.js +15 -15
- package/src/utils/trace.js +153 -153
- package/src/utils/wal.js +39 -0
- package/src/utils/winutf8.js +16 -15
- package/src/wiki/index.js +170 -0
package/src/agent/index.js
CHANGED
|
@@ -1,717 +1,1340 @@
|
|
|
1
|
-
// src/agent/index.js - Agent 引擎 (皮皮虾核心) v0.2 含工具调用
|
|
2
|
-
import { ensureUTF8Console } from "../utils/winutf8.js";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import os from "node:os";
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
//
|
|
12
|
-
import {
|
|
13
|
-
//
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
// (
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// (
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
this.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
this.
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
this.
|
|
76
|
-
this.
|
|
77
|
-
this.
|
|
78
|
-
this.
|
|
79
|
-
this.
|
|
80
|
-
this.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
this.
|
|
87
|
-
this.
|
|
88
|
-
|
|
89
|
-
this.
|
|
90
|
-
|
|
91
|
-
this.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
this.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
this.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
this.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
this.
|
|
124
|
-
|
|
125
|
-
//
|
|
126
|
-
this.
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
//
|
|
131
|
-
this.
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
this.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
this.
|
|
157
|
-
|
|
158
|
-
//
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
"
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
//
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
if (
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
//
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
const
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (this.
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
);
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
//
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
if (this.
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
//
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
1
|
+
// src/agent/index.js - Agent 引擎 (皮皮虾核心) v0.2 含工具调用
|
|
2
|
+
import { ensureUTF8Console } from "../utils/winutf8.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
import { TOOL_ERROR_PREFIX } from "../tools/index.js";
|
|
9
|
+
import { normalizeCommand } from "../tools/command-guard.js";
|
|
10
|
+
// 重构第一刀 (2026-09-14): 工具循环执行策略抽至 src/core/policy.js
|
|
11
|
+
// (探索熔断/重复检测/溢出降档/错误重试/结果裁剪/循环驱动), 重新导出保持测试与外部兼容
|
|
12
|
+
import { runToolLoop, LLM_FAILED_HINT } from "../core/policy.js";
|
|
13
|
+
// 重构第三刀 (2026-09-14): 结构化事件流 traceId 贯穿 (AsyncLocalStorage), 关键路径埋点
|
|
14
|
+
import { EventTracer, runWithTrace, currentTrace } from "../core/trace.js";
|
|
15
|
+
// 重构第二刀 (2026-09-14): 记忆升降级 + 自我学习收敛为独立服务, agent 只保留薄委托
|
|
16
|
+
import { MemoryService } from "../services/memory-service.js";
|
|
17
|
+
import { LearningService } from "../services/learning-service.js";
|
|
18
|
+
export { isOverflowError as _isOverflowError, trimToolResult, toToolContent } from "../core/policy.js";
|
|
19
|
+
import { logicalDay } from "../utils/store.js";
|
|
20
|
+
import { loadConfig } from "../config/index.js";
|
|
21
|
+
// 成本折算 (2026-10-03l): usageStats 加金额维度 + 支出预算闸门 (增强框架第 8 条: 预算控制)
|
|
22
|
+
import { estimateCost } from "../llm/pricing.js";
|
|
23
|
+
import { info, warn, error, debug } from "../utils/logger.js";
|
|
24
|
+
import { Context, compose, loadPlugins } from "../plugin/index.js";
|
|
25
|
+
import { builtinPlugins, resolveLLM, resolveAllLLMs, isUsableProvider } from "../plugin/builtin.js";
|
|
26
|
+
// v3.0 (codex 对齐): 新层插件 (permissions/hooks/commands/evidence/protocol)
|
|
27
|
+
import { v3Plugins } from "../plugin/v3.js";
|
|
28
|
+
// P2-3: 占位符判定与路由共用唯一真相源, 避免"启动告警"和"实际选模型"两套口径漂移
|
|
29
|
+
import { isPlaceholder, hasPlaceholderField } from "../config/placeholder.js";
|
|
30
|
+
import { registerMcpTools } from "../mcp/index.js";
|
|
31
|
+
import { Auditor } from "../audit/verifier.js";
|
|
32
|
+
import { classifyFailure } from "../memory/failure-episode.js";
|
|
33
|
+
// ANS 独立模块 (可更换): 价值对齐 / 自主任务生成 / 生命周期
|
|
34
|
+
import { Lifecycle } from "../ans/lifecycle.js";
|
|
35
|
+
import { suggestProactive, markTaskDone } from "../ans/proactive.js";
|
|
36
|
+
import { record as rewardRecord, status as rewardStatus } from "../ans/reward.js";
|
|
37
|
+
import { scan as evictionScan, status as evictionStatus } from "../ans/eviction.js";
|
|
38
|
+
import { installGuard, guardStatus, installGuardOnCatalog } from "../ans/guard.js";
|
|
39
|
+
import { scanInjection, wrapUntrusted, reportSuspicious, stripProtoKeys } from "../security/injection.js";
|
|
40
|
+
import { SkillLoader } from "../skills/loader.js";
|
|
41
|
+
import { EvolutionEngine } from "../selfheal/evolve.js";
|
|
42
|
+
// 重构 (2026-09-15): 历史/上下文管理 + 提示词构建从 PPXAgent 类抽出为 mixin
|
|
43
|
+
// (context.js: 历史裁剪/token 预算/会话压缩; prompts.js: 技能清单/核心价值/DSML/画像/多模态)
|
|
44
|
+
import { contextMethods } from "./context.js";
|
|
45
|
+
import { promptMethods } from "./prompts.js";
|
|
46
|
+
import { TurnProjection } from "../session/projection.js";
|
|
47
|
+
|
|
48
|
+
const ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), "..", "..");
|
|
49
|
+
// 会话历史条数/token 预算已迁到 config.memory (max_history_items / history_token_budget)
|
|
50
|
+
// (上下文窗口常量 DEFAULT_CONTEXT_WINDOW / DEFAULT_CONTEXT_RATIO 与辅助 LLM 超时 AUX_LLM_TIMEOUT_MS
|
|
51
|
+
// 已随历史管理迁至 src/agent/context.js)
|
|
52
|
+
// (工具循环阈值 DEFAULT_MAX_TOOL_ROUNDS / EXPLORE_TOOLS 等已迁至 src/core/policy.js)
|
|
53
|
+
|
|
54
|
+
// (工具结果裁剪 trimToolResult / 溢出判定 isOverflowError / LLM 失败提示 LLM_FAILED_HINT
|
|
55
|
+
// 已迁至 src/core/policy.js, 本文件经 re-export 保持兼容)
|
|
56
|
+
// (多模态视觉 content 注入 visionUserContent 已迁至 src/agent/prompts.js)
|
|
57
|
+
|
|
58
|
+
// P2-2 降级提示标记: 追加在用户可见回复末尾的哨兵串 (历史/记忆写入前按它剥离)
|
|
59
|
+
const FALLBACK_NOTICE_TAG = "\n\n> ⚠ ";
|
|
60
|
+
|
|
61
|
+
export class PPXAgent {
|
|
62
|
+
constructor({ root = ROOT, configFile = null, plugins = [], dataDir = null, globalDataDir = null } = {}) {
|
|
63
|
+
this.root = root;
|
|
64
|
+
// dataDir 可覆盖: 显式参数 > PPX_DATA_DIR 环境变量 > 默认目录
|
|
65
|
+
// 默认目录: 包装在 node_modules 里时外置到 ~/.ppx (防卸载丢数据), 否则 root/data
|
|
66
|
+
this.dataDir = dataDir || process.env.PPX_DATA_DIR || this._defaultDataDir(root);
|
|
67
|
+
// 全局共享数据目录 (跨 agent 共享经验等): 显式参数 > PPX_AGENT_GLOBAL_DATA_DIR > 本地 dataDir
|
|
68
|
+
this.globalDataDir = globalDataDir || process.env.PPX_AGENT_GLOBAL_DATA_DIR || this.dataDir;
|
|
69
|
+
this.config = this._loadConfig(configFile);
|
|
70
|
+
this.userName = this.config.user?.name || "兄弟";
|
|
71
|
+
|
|
72
|
+
// 装配顺序固定: 插件容器 → 内置/用户插件 → 服务绑定 → ANS 接线 → 起始状态 → 启动动作
|
|
73
|
+
// (2026-09-18 重构: 原 120 行单块构造函数按阶段拆为私有方法, 顺序与语义完全不变)
|
|
74
|
+
this._initPluginContainer(plugins);
|
|
75
|
+
this._bindServices();
|
|
76
|
+
this._wireANS();
|
|
77
|
+
this._initTurnState();
|
|
78
|
+
this._initEngines();
|
|
79
|
+
this._initServices();
|
|
80
|
+
this._initStartup();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ---- 装配阶段 1: 插件容器与插件装配 (一切皆插件) ----
|
|
84
|
+
_initPluginContainer(plugins) {
|
|
85
|
+
// P2⑧: 顶层 ctx 为 full-access 基座 (内置插件可信), 用户插件默认 restricted
|
|
86
|
+
this.ctx = new Context(null, { access: "full-access" });
|
|
87
|
+
this.ctx.provide("root", this.root);
|
|
88
|
+
this.ctx.provide("dataDir", this.dataDir);
|
|
89
|
+
this.ctx.provide("globalDataDir", this.globalDataDir);
|
|
90
|
+
this.ctx.provide("config", this.config);
|
|
91
|
+
this.ctx.provide("userName", this.userName);
|
|
92
|
+
this.ctx.provide("agent", this);
|
|
93
|
+
// 装配顺序: 内置插件 → 用户插件目录(声明式) → 构造函数传入插件(编程式)
|
|
94
|
+
const pluginsDir = path.join(this.root, this.config.plugins?.dir || "plugins");
|
|
95
|
+
compose(this.ctx, [...builtinPlugins, ...v3Plugins, ...loadPlugins(pluginsDir), ...plugins]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---- 装配阶段 2: 从 ctx 取服务, 设置公开属性 (向后兼容, 外部代码不变) ----
|
|
99
|
+
_bindServices() {
|
|
100
|
+
this.healer = this.ctx.consume("healer");
|
|
101
|
+
this.health = this.ctx.consume("health");
|
|
102
|
+
this.persona = this.ctx.consume("persona");
|
|
103
|
+
this.facts = this.ctx.consume("facts");
|
|
104
|
+
this.experience = this.ctx.consume("experience");
|
|
105
|
+
this.sessionStore = this.ctx.consume("sessions");
|
|
106
|
+
this.memory = this.ctx.consume("memory");
|
|
107
|
+
this.llm = this.ctx.consume("llm");
|
|
108
|
+
this.allProviders = this.ctx.consume("allProviders");
|
|
109
|
+
// 可选分层路由 (2026-10-02): config.model_routing.aux 指定辅助任务 (记忆提取/摘要/压缩等
|
|
110
|
+
// 非主对话调用) 的厂商 id; 不配置 = 辅助调用跟随主模型, 零配置零门槛。
|
|
111
|
+
this.auxLLM = null;
|
|
112
|
+
{
|
|
113
|
+
const auxId = this.config.model_routing?.aux;
|
|
114
|
+
if (auxId) {
|
|
115
|
+
const hit = (this.allProviders || []).find((c) => c.providerId === auxId);
|
|
116
|
+
if (hit) this.auxLLM = hit;
|
|
117
|
+
else warn(`model_routing.aux="${auxId}" 未匹配任何可用 provider, 辅助调用回落主模型`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
this.l0 = this.ctx.consume("l0");
|
|
121
|
+
this.scenes = this.ctx.consume("scenes");
|
|
122
|
+
this.personaStore = this.ctx.consume("personaStore");
|
|
123
|
+
this.traces = this.ctx.consume("traces");
|
|
124
|
+
// 故障病历 (2026-10-03 接线): 原先 evolvePlugin 提供了却零消费 —— 失败只写经验库,
|
|
125
|
+
// 本模块的"结构化病历 + 同类检索"从未生效。现于 _runTool 接入读写闭环。
|
|
126
|
+
this.failures = this.ctx.consume("failures");
|
|
127
|
+
// 记忆管线健康监控 (2026-10-03 接线): 与 failures 同属"装配了但零消费"的预留件 ——
|
|
128
|
+
// 没人 record() 时 status() 永远 healthy, 降级建议形同虚设。现由 MemoryService 记账与消费。
|
|
129
|
+
this.memoryHealth = this.ctx.consume("memoryHealth");
|
|
130
|
+
// 重构第三刀: 结构化事件流 (记忆升降级/工具失败/spawn/自愈触发), 独立于工具轨迹
|
|
131
|
+
this.tracer = new EventTracer(this.dataDir);
|
|
132
|
+
this.bus = this.ctx.consume("bus");
|
|
133
|
+
this.tools = this.ctx.consume("tools");
|
|
134
|
+
// 2026-10-03 接线: 语境 Playbook (evolve 引擎) —— prompts._playbookPrompt 注入 system
|
|
135
|
+
// prompt, LearningService.refine 失败提炼时自动 ADD bullet (原 provide 后零消费)
|
|
136
|
+
this.playbook = this.ctx.consume("playbook");
|
|
137
|
+
this.scheduler = this.ctx.consume("scheduler");
|
|
138
|
+
this.toolsEnabled = this.ctx.consume("toolsEnabled");
|
|
139
|
+
// v3.0 (codex 对齐): 权限引擎 / 钩子链 / 命令注册表 / 目标看板 / 协议总线
|
|
140
|
+
this.hooks = this.ctx.consume("hooks");
|
|
141
|
+
this.permissions = this.ctx.consume("permissions");
|
|
142
|
+
this.commands = this.ctx.consume("commands");
|
|
143
|
+
this.goalBoard = this.ctx.consume("goalBoard");
|
|
144
|
+
this.protocolBus = this.ctx.consume("protocolBus");
|
|
145
|
+
// ZCode 工具能力门接线 (2026-10-02 吸收): 工具声明式能力 → 权限引擎
|
|
146
|
+
// 开关: config.agent.capability_gate (默认开); auto_approve_high_risk (默认关)
|
|
147
|
+
// ⚠ 必须在 permissions/tools 都消费之后 (2026-10-03 自测发现: 原先放在消费前被静默跳过)
|
|
148
|
+
if (this.permissions && this.tools && typeof this.tools.getCapability === "function") {
|
|
149
|
+
this.permissions.getCapability = (n) => this.tools.getCapability(n);
|
|
150
|
+
this.permissions.capabilityGate = this.config.agent?.capability_gate !== false;
|
|
151
|
+
this.permissions.autoApproveHighRisk = !!this.config.agent?.auto_approve_high_risk;
|
|
152
|
+
}
|
|
153
|
+
// ZCode 使用统计对齐 (2026-10-02): 会话级 token/调用次数记账 (零侵入包装 provider)
|
|
154
|
+
// 2026-10-03l: 加 cost (USD) 维度 + budget.usd 支出上限 (超限后 chat/chatStream 拒绝继续烧钱)
|
|
155
|
+
this.usageStats = { calls: 0, tokens: 0, cost: 0, byModel: {} };
|
|
156
|
+
this._budgetExceeded = false;
|
|
157
|
+
this._installUsageTracking();
|
|
158
|
+
// 待审批映射 (codex approval flow): id -> { req, resolve, timer }
|
|
159
|
+
this._pendingApprovals = new Map();
|
|
160
|
+
this._approvalSeq = 0;
|
|
161
|
+
// B2: 审批缓存 (codex ApprovalStore 语义) — 会话内相同命令批准后不重复 ask
|
|
162
|
+
// key = `${tool}:${normalizeCommand(command)}`; 只存批准结果, 拒绝/超时不入缓存
|
|
163
|
+
this._approvalCache = new Map();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ---- 装配阶段 3: ANS 自治接线 (免疫闸门 / Reward 闭环 / 排泄自治) ----
|
|
167
|
+
_wireANS() {
|
|
168
|
+
// ⑧ 免疫系: 全局闸门挂到总线命令通道 (拦截+审计)
|
|
169
|
+
this.__guard = installGuard(this, { allowList: this.config.agent?.guardAllowList || [] });
|
|
170
|
+
// ⑦ Reward 闭环: 订阅总线工具成败, 自动更新行为倾向 (EWMA)
|
|
171
|
+
this.bus?.on("tool/result", (ev) => {
|
|
172
|
+
const { name, ok } = ev.payload || {};
|
|
173
|
+
if (name) { try { rewardRecord(this, { tool: name, ok: !!ok }); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } }
|
|
174
|
+
});
|
|
175
|
+
// P0 (2026-09-15): 免疫闸门接入工具执行收口 (修 MERGE-REPORT 遗留 P2 —— guard 之前只盖总线命令,
|
|
176
|
+
// 工具走 catalog 绕过全局闸门)。共享同一 state: approveGuard 一次授权同时作用于总线+工具。
|
|
177
|
+
try {
|
|
178
|
+
this.__guardOnCatalog = installGuardOnCatalog(this.tools, this.__guard);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
warn(`[guard] 工具收口接入失败: ${e.message}`);
|
|
181
|
+
}
|
|
182
|
+
// ⑤排泄自治: 每日扫描长期记忆做冗余识别/冷热分层 (幂等注册, 不重复)
|
|
183
|
+
// 2026-09-18 配合 Scheduler 重启恢复修复: 持久化恢复的 eviction-daily 无 action 闭包
|
|
184
|
+
// (JSON 序列化丢函数), 直接复用会走 onFire 兜底变成"记一条备忘"而非真正扫描 ——
|
|
185
|
+
// 故检测到"同名的无 action 恢复任务"时先移除再重挂真闭包。
|
|
186
|
+
try {
|
|
187
|
+
const jobs = this.scheduler?.jobs || [];
|
|
188
|
+
const stale = jobs.find((j) => j.name === "eviction-daily" && typeof j.action !== "function");
|
|
189
|
+
if (stale) this.scheduler?.remove(stale.id);
|
|
190
|
+
const hasE = jobs.some((j) => j.name === "eviction-daily" && typeof j.action === "function");
|
|
191
|
+
if (!hasE) this.scheduler?.add({ name: "eviction-daily", cron: "02:00", type: "daily", action: () => { try { evictionScan(this); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } } });
|
|
192
|
+
} catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
193
|
+
// 首次启动跑一次排遗扫描 (预热治理状态)
|
|
194
|
+
try { evictionScan(this); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
195
|
+
this._warnMissingCloudApi(); // 发布首启引导: 未配云端 key 时明确提示
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ---- 装配阶段 4: 单轮对话运行态 (通知/中断/工具事件) ----
|
|
199
|
+
_initTurnState() {
|
|
200
|
+
this._notifyCb = null;
|
|
201
|
+
this._onToolEvent = null; // 工具事件回调
|
|
202
|
+
this._toolCallSeq = 0; // 工具调用序号: 给 start/done 事件生成唯一 id, 供 UI 精确配对
|
|
203
|
+
this._interrupted = false;
|
|
204
|
+
this._interruptedSessions = new Set(); // v3.0.1 (P1#3): 按会话中断, 并发会话不串台
|
|
205
|
+
this._turnCbs = new Map(); // v3.0.1 (P1#4): traceId -> { onTool, onStep }, 并发流式回调互不覆盖
|
|
206
|
+
this._turnsUsedTools = new Set(); // v3.0.1 (P1#4): 用过工具的 traceId 集合 (替代单布尔标志)
|
|
207
|
+
this._turnFallbacks = new Map(); // v3.0.1 (P1#4): traceId -> 降级事实 (替代单槽 _lastFallback)
|
|
208
|
+
this._lastTurnUsedTools = false; // 兼容保留: 无 trace 上下文的裸调用路径
|
|
209
|
+
this._lastFallback = null; // P2-2: 最近一次 provider 降级事实 (在本轮内有效, 用完即清)
|
|
210
|
+
this._mcp = null; // MCP 连接句柄 (connectMcp 后赋值)
|
|
211
|
+
this._proactiveTimer = null; // 主动任务生成定时器
|
|
212
|
+
// v3.1 首片 (P1#8): Turn 投影层接入 —— SessionStore 仍是唯一事实源,
|
|
213
|
+
// 本层只投影每轮生命周期 (open/closed/aborted), 纯可观测, config.agent.turn_projection=false 可旁路
|
|
214
|
+
this.turnProjection = new TurnProjection();
|
|
215
|
+
this.turnProjection.enabled = this.config.agent?.turn_projection !== false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---- 装配阶段 5: 长期状态引擎 (生命周期 / 进化 / 审计 / 技能) ----
|
|
219
|
+
_initEngines() {
|
|
220
|
+
// 生命周期 (ANS 独立模块): born → growing → mature → evolving / reproducing
|
|
221
|
+
// v1.0.7 持久化: 状态落盘 data/memory/lifecycle.json, 跨进程/重启不归零 (P1)
|
|
222
|
+
this.lifecycle = new Lifecycle({ file: path.join(this.dataDir, "memory", "lifecycle.json") });
|
|
223
|
+
this.evolve = new EvolutionEngine(this, this.config.agent?.evolve || {});
|
|
224
|
+
// Auditor (P0①): 唯一“已验证写回”通道 + 已验证账本 (data/audit/verified.json)
|
|
225
|
+
this.auditor = new Auditor({ ledgerPath: path.join(this.dataDir, "audit", "verified.json") });
|
|
226
|
+
// 方法技能目录 (Superpowers 吸收): 供 _context 注入技能清单, LLM 按需 load_skill
|
|
227
|
+
try { this.skills = new SkillLoader(path.join(this.root, "skills")); } catch { this.skills = null; }
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ---- 装配阶段 6: 业务服务 (记忆协调 / 自我学习, 依赖注入) ----
|
|
231
|
+
_initServices() {
|
|
232
|
+
// 重构第二刀: 记忆协调服务 + 自我学习服务 (依赖注入, llm 用闭包实时取当前 provider)
|
|
233
|
+
// 2026-10-02: 辅助服务优先走 model_routing.aux (可选, 未配置回落主模型)
|
|
234
|
+
this.memorySvc = new MemoryService({
|
|
235
|
+
getLlm: () => this.auxLLM || this.llm,
|
|
236
|
+
facts: this.facts,
|
|
237
|
+
scenes: this.scenes,
|
|
238
|
+
personaStore: this.personaStore,
|
|
239
|
+
experience: this.experience,
|
|
240
|
+
lifecycle: this.lifecycle,
|
|
241
|
+
tracer: this.tracer,
|
|
242
|
+
health: this.memoryHealth, // 记忆管线健康度: 每步 record, 降级时跳过 LLM 压缩/提炼
|
|
243
|
+
});
|
|
244
|
+
this.learningSvc = new LearningService({
|
|
245
|
+
getLlm: () => this.auxLLM || this.llm,
|
|
246
|
+
traces: this.traces,
|
|
247
|
+
skills: this.skills,
|
|
248
|
+
experience: this.experience,
|
|
249
|
+
lifecycle: this.lifecycle,
|
|
250
|
+
auditor: this.auditor,
|
|
251
|
+
tracer: this.tracer,
|
|
252
|
+
toolNames: () => this._toolNames(),
|
|
253
|
+
runTool: (name, args) => this.tools.call(name, args, { agent: this }),
|
|
254
|
+
playbook: this.playbook, // 2026-10-03 接线: 教训沉淀为 playbook bullets
|
|
255
|
+
});
|
|
256
|
+
// 注入 LLM 摘要器/提炼器 (依赖 service, 装配后注入)
|
|
257
|
+
this.memory.summarizer = (raw) => this.memorySvc.summarizeMemory(raw);
|
|
258
|
+
this.memory.setExtractor((u, a, related) => this.memorySvc.extractMemory(u, a, related));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---- 装配阶段 7: 启动动作 (禁用工具 / MCP 自动连接 / 首次画像) ----
|
|
262
|
+
_initStartup() {
|
|
263
|
+
// 应用 tools.disabled: 从 config/ppx.json 读取需禁用的工具, 启动时禁用 (设置 UI 写盘生效)
|
|
264
|
+
this._applyDisabledTools();
|
|
265
|
+
// 应用工具披露策略: 只把核心工具 schema 发给 LLM, 其余按需加载 (省上下文)
|
|
266
|
+
this._applyToolExposure();
|
|
267
|
+
|
|
268
|
+
// 可选: 启动时自动连接 MCP 服务器 (config.mcp.auto_connect = true 时非阻塞连接)
|
|
269
|
+
if (this.config.mcp?.auto_connect && this.config.mcp.servers?.length) {
|
|
270
|
+
this.connectMcp()
|
|
271
|
+
.then((n) => { if (n) info(`[mcp] 自动连接 ${n} 个 MCP 工具`); })
|
|
272
|
+
.catch((e) => warn(`[mcp] 自动连接失败: ${e.message}`));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// 首次启动生成 L3 画像 (零依赖高频词统计, 同步快, 不调 LLM)
|
|
276
|
+
this._maybeRefreshPersona();
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// 默认数据目录: 包装在 node_modules 里(全局/本地安装)时外置到 ~/.ppx, 否则 root/data
|
|
280
|
+
_defaultDataDir(root) {
|
|
281
|
+
if (String(root).includes("node_modules")) return path.join(os.homedir(), ".ppx");
|
|
282
|
+
return path.join(root, "data");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 主动通知 + 中断 API
|
|
286
|
+
setNotify(cb) { this._notifyCb = typeof cb === "function" ? cb : null; }
|
|
287
|
+
// 工具调用过程可视化 - 回调 (tool名, 参数, 耗时, 状态) 供 Web UI 推送
|
|
288
|
+
setToolEvent(cb) { this._onToolEvent = typeof cb === "function" ? cb : null; }
|
|
289
|
+
// turn/step 分层: 推理轮次事件 (每轮工具循环发一次 step), 供军团 worker 上报进度
|
|
290
|
+
setStepEvent(cb) { this._onStepEvent = typeof cb === "function" ? cb : null; }
|
|
291
|
+
notify(message) { if (this._notifyCb) { try { this._notifyCb(String(message)); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } } }
|
|
292
|
+
// v3.0.1 (P1#3): 中断支持按会话 —— 带 sessionKey 只中断该会话 (HTTP 并发场景不串台);
|
|
293
|
+
// 无 key 全局中断 (CLI 单会话兼容)。
|
|
294
|
+
interrupt(sessionKey) {
|
|
295
|
+
if (sessionKey) this._interruptedSessions.add(sessionKey);
|
|
296
|
+
else this._interrupted = true;
|
|
297
|
+
}
|
|
298
|
+
// 每轮对话开始复位: 清本会话中断 + 全局标志 (兼容 CLI /stop → 下一轮继续的既有语义)
|
|
299
|
+
clearInterrupt(sessionKey) {
|
|
300
|
+
if (sessionKey) this._interruptedSessions.delete(sessionKey);
|
|
301
|
+
else this._interruptedSessions.clear();
|
|
302
|
+
this._interrupted = false;
|
|
303
|
+
}
|
|
304
|
+
isInterrupted(sessionKey) {
|
|
305
|
+
if (this._interrupted) return true;
|
|
306
|
+
if (sessionKey) return this._interruptedSessions.has(sessionKey);
|
|
307
|
+
return this._interruptedSessions.size > 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// v3.0.1 (P1#4): 工具/step 事件路由 —— 优先当前 trace 注册的按轮回调 (并发流式会话互不覆盖),
|
|
311
|
+
// 无按轮回调时兜底全局回调 (setToolEvent/setStepEvent, Web UI 全局面板用)
|
|
312
|
+
_emitTool(ev) {
|
|
313
|
+
const per = this._turnCbs.get(currentTrace()?.traceId);
|
|
314
|
+
const cb = (per && per.onTool) || this._onToolEvent;
|
|
315
|
+
if (cb) { try { cb(ev); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } }
|
|
316
|
+
}
|
|
317
|
+
_emitStep(ev) {
|
|
318
|
+
const per = this._turnCbs.get(currentTrace()?.traceId);
|
|
319
|
+
const cb = (per && per.onStep) || this._onStepEvent;
|
|
320
|
+
if (cb) { try { cb(ev); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } }
|
|
321
|
+
}
|
|
322
|
+
// 取走当前轮的降级事实 (按 traceId, 无则回落全局单槽), 取后即清
|
|
323
|
+
_takeFallback() {
|
|
324
|
+
const tid = currentTrace()?.traceId;
|
|
325
|
+
if (tid && this._turnFallbacks.has(tid)) {
|
|
326
|
+
const fb = this._turnFallbacks.get(tid);
|
|
327
|
+
this._turnFallbacks.delete(tid);
|
|
328
|
+
return fb;
|
|
329
|
+
}
|
|
330
|
+
const fb = this._lastFallback;
|
|
331
|
+
this._lastFallback = null;
|
|
332
|
+
return fb;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 只读模式 (SDD 审查者, 吸收 Superpowers): 禁用一切修改/执行类工具, 只能读/查
|
|
336
|
+
// 供 spawn_agent review 循环的审查者角色 (worker 经 PPX_AGENT_READONLY=1 触发)
|
|
337
|
+
enableReadonlyMode() {
|
|
338
|
+
const disabled = [
|
|
339
|
+
"run_command", "write_file", "code_act", "create_skill",
|
|
340
|
+
"memory_add", "add_schedule",
|
|
341
|
+
"scene_create", "scene_describe", "spawn_agent", "refine_skill",
|
|
342
|
+
"refine", // v1.0.8: refine 会写经验库, 只读审查者也不应触发
|
|
343
|
+
];
|
|
344
|
+
for (const t of disabled) { try { this.tools.disable(t); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } }
|
|
345
|
+
this.readonly = true;
|
|
346
|
+
return this;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
_loadConfig(configFile) {
|
|
350
|
+
return loadConfig(this.root, configFile);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// P1#9: LLM 结构化记忆提炼 (实现已迁 src/services/memory-service.js, 此处分发保持公共 API)
|
|
354
|
+
async _extractMemory(user, assistant, existing = []) {
|
|
355
|
+
return this.memorySvc.extractMemory(user, assistant, existing);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 辅助 LLM 调用前置健康探测 (v1.0.7): 模型不可用 (本地服务未运行/远端不可达) 时快速跳过,
|
|
359
|
+
// 不发起 10s 超时等待 — 本地未运行的 health() 是 ECONNREFUSED 毫秒级失败, 开销可忽略
|
|
360
|
+
// (记忆/学习方法已用 service 内部版本, 此处保留供 _maybeCompact 等使用)
|
|
361
|
+
async _auxLlmReady() {
|
|
362
|
+
if (!this.llm) return false;
|
|
363
|
+
if (typeof this.llm.health !== "function") return true;
|
|
364
|
+
try { return await this.llm.health(); } catch { return false; }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 用 LLM 把旧对话浓缩成语义摘要 (实现已迁 memory-service, 此处分发)
|
|
368
|
+
async _summarizeMemory(raw) {
|
|
369
|
+
return this.memorySvc.summarizeMemory(raw);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// P1: LLM 查询扩展 (实现已迁 memory-service, 此处分发)
|
|
373
|
+
async _expandQuery(q) {
|
|
374
|
+
return this.memorySvc.expandQuery(q);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// P1: 语义记忆检索 (实现已迁 memory-service, 此处分发)
|
|
378
|
+
async _memoryQuery(q, { limit = 5, scope = null } = {}) {
|
|
379
|
+
return this.memorySvc.query(q, { limit, scope });
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// 重置某会话历史 (新会话): 删除事件日志
|
|
383
|
+
resetSession(sessionKey) { this.sessionStore.delete(sessionKey || "default"); }
|
|
384
|
+
|
|
385
|
+
// 对话主入口 (含工具调用循环)
|
|
386
|
+
async chat(userMsg, { persist = true, sessionKey = "default", mode = null } = {}) {
|
|
387
|
+
// 重构第三刀: 入口生成 traceId, 记忆/工具/学习子调用自动继承 (AsyncLocalStorage)
|
|
388
|
+
return runWithTrace(async () => {
|
|
389
|
+
// 支出预算闸门 (2026-10-03l): 超限后拒绝继续烧钱, 提示如何调额
|
|
390
|
+
if (this._budgetExceeded) return this._budgetMessage();
|
|
391
|
+
const tid = currentTrace()?.traceId;
|
|
392
|
+
// v3.1 首片 (P1#8): Turn 投影 begin (纯可观测, 失败不影响主链路)
|
|
393
|
+
const turnInfo = this.turnProjection.begin(sessionKey, userMsg);
|
|
394
|
+
let turnEnded = false;
|
|
395
|
+
const _endTurn = (interrupted) => {
|
|
396
|
+
if (turnEnded) return;
|
|
397
|
+
turnEnded = true;
|
|
398
|
+
const endInfo = interrupted ? this.turnProjection.abort(sessionKey) : this.turnProjection.complete(sessionKey);
|
|
399
|
+
if (turnInfo) {
|
|
400
|
+
this.bus?.emit("chat/turn/end", { sessionKey, ...turnInfo, ...endInfo, interrupted }, { source: "agent.chat" });
|
|
401
|
+
this.protocolBus?.eq?.push?.({ type: "TASK_TURN_ENDED", payload: { sessionKey, ...turnInfo, ...endInfo, interrupted } });
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
try {
|
|
405
|
+
this.clearInterrupt(sessionKey); // 新一轮对话开始, 复位本会话的上一轮中断状态
|
|
406
|
+
this.bus?.emit("chat/user", { userMsg, sessionKey }, { source: "agent.chat" });
|
|
407
|
+
if (turnInfo) {
|
|
408
|
+
this.bus?.emit("chat/turn/begin", { sessionKey, ...turnInfo }, { source: "agent.chat" });
|
|
409
|
+
this.protocolBus?.eq?.push?.({ type: "TASK_TURN_BEGAN", payload: { sessionKey, ...turnInfo } });
|
|
410
|
+
}
|
|
411
|
+
let reply;
|
|
412
|
+
// 内核自主决策: 高置信简单指令本地处理, 不调 LLM
|
|
413
|
+
const local = (this.config.agent?.localIntent !== false) ? await this._localIntent(userMsg) : null;
|
|
414
|
+
if (local) {
|
|
415
|
+
reply = local;
|
|
416
|
+
} else {
|
|
417
|
+
// 模式分发: 编排策略可插拔 (react/single, 未来 plan-exec/multi-agent/graph 等)
|
|
418
|
+
const modeName = mode || this.config.agent?.mode || "react";
|
|
419
|
+
try {
|
|
420
|
+
reply = await this.ctx.consume("modes").run(modeName, this, userMsg, { sessionKey });
|
|
421
|
+
} catch (e) {
|
|
422
|
+
error("LLM 调用失败:", e.message);
|
|
423
|
+
reply = LLM_FAILED_HINT(e.message);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// v3.0.1 (P1#4): 按轮读取工具使用标记 (traceId 路由), 无 trace 时回落兼容单槽
|
|
428
|
+
const usedTools = tid ? this._turnsUsedTools.delete(tid) : this._lastTurnUsedTools;
|
|
429
|
+
if (!tid) this._lastTurnUsedTools = false;
|
|
430
|
+
if (this._notifyCb && usedTools) this.notify("[done] 任务完成 (工具执行)。");
|
|
431
|
+
|
|
432
|
+
if (persist) {
|
|
433
|
+
// 会话落盘 + L0/L1 记忆写入 + 记忆升降级协调 (afterTurn: L2 场景归档/经验学习/L3 画像刷新)
|
|
434
|
+
await this._persistTurn(sessionKey, userMsg, reply, { afterTurn: true });
|
|
435
|
+
}
|
|
436
|
+
// 生命周期推进: 每次对话计数, 阶段转换 born→growing→mature
|
|
437
|
+
this.bus?.emit("chat/reply", { reply }, { source: "agent.chat" });
|
|
438
|
+
this._lifecycleTick();
|
|
439
|
+
this.evolve && this.evolve.tick();
|
|
440
|
+
// P2-2: 本轮若发生过 provider 降级, 在回复末尾附可见提示
|
|
441
|
+
// (放在 persist 之后 —— 写入会话历史/记忆的始终是模型原文, 提示只对当轮用户可见)
|
|
442
|
+
const fb = this._takeFallback();
|
|
443
|
+
if (fb) {
|
|
444
|
+
reply = String(reply ?? "") + this._fallbackNotice(fb);
|
|
445
|
+
this.notify(`[降级] ${fb.from} → ${fb.to}`);
|
|
446
|
+
}
|
|
447
|
+
// v3.1 首片 (P1#8): 本轮被中断 → turn 标 aborted, 否则正常 closed
|
|
448
|
+
_endTurn(this.isInterrupted(sessionKey));
|
|
449
|
+
return reply;
|
|
450
|
+
} finally {
|
|
451
|
+
// v3.0.1 (P1#4): 无论成功/失败都清本轮状态, 防长跑进程按 trace 泄漏
|
|
452
|
+
if (tid) { this._turnsUsedTools.delete(tid); this._turnFallbacks.delete(tid); }
|
|
453
|
+
// v3.1 首片: 异常路径 turn 未正常结束 → 标 aborted 收口
|
|
454
|
+
_endTurn(true);
|
|
455
|
+
}
|
|
456
|
+
}, { sessionKey, channel: "chat", userMsg: String(userMsg).slice(0, 200) });
|
|
457
|
+
}
|
|
458
|
+
// 单轮落库公共路径 (chat / chatStream 共用, 2026-09-18 重构去重):
|
|
459
|
+
// 会话事件日志追加 → L0/L1 记忆写入 → (可选) 记忆升降级协调 (L2 归档/经验学习/L3 画像刷新)
|
|
460
|
+
// 注意: 写入的始终是"模型原文" —— 用户可见的降级提示由调用方在落库之后再拼接。
|
|
461
|
+
async _persistTurn(sessionKey, userMsg, assistantText, { afterTurn = false } = {}) {
|
|
462
|
+
this._pushTurn(sessionKey, String(userMsg), assistantText);
|
|
463
|
+
await this.memory.recordTurn(userMsg, assistantText);
|
|
464
|
+
if (afterTurn) {
|
|
465
|
+
this.bus?.emit("memory/record", { userMsg, reply: assistantText }, { source: "agent.chat" });
|
|
466
|
+
this.memorySvc.afterTurn(userMsg, assistantText);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// 生命周期: 每次对话计数 + 阶段转换 (委托 ans/lifecycle 模块)
|
|
471
|
+
_lifecycleTick() {
|
|
472
|
+
this.lifecycle.tick();
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// 生命周期摘要 (可观测, 委托 ans/lifecycle 模块)
|
|
476
|
+
lifecycleStatus() {
|
|
477
|
+
return this.lifecycle.status();
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Reward 行为倾向可观测 (⑦内分泌)
|
|
481
|
+
rewardStatus() {
|
|
482
|
+
return rewardStatus(this);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 排泄治理可观测 (⑤遗忘-归档)
|
|
486
|
+
evictionStatus() {
|
|
487
|
+
return evictionStatus(this);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// 立即手动触发一次记忆治理扫描 (冗余识别 + 冷热分层)
|
|
491
|
+
runMemoryEviction() {
|
|
492
|
+
return evictionScan(this);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// 免疫闸门可观测 (⑧安全治理)
|
|
496
|
+
guardStatus() {
|
|
497
|
+
return guardStatus(this);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// 单次审批: 放行一个危险命令 verb (一次用完自动失效)
|
|
501
|
+
approveGuard(verb) {
|
|
502
|
+
return this.__guard ? this.__guard.approveOnce(String(verb)) : null;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
// 流式对话: 返回 { text, history } 或回调 onDelta 推送增量
|
|
507
|
+
// 支持工具循环: 若消息触发工具调用, 走 _llmWithTools (触发 onTool 事件推送工具活动),
|
|
508
|
+
// 最终结果作为一次 delta 推送; 否则走 streamChat 逐字流式 [P1#7]
|
|
509
|
+
async chatStream(userMsg, { sessionKey = "default", onDelta, onTool, onStep } = {}) {
|
|
510
|
+
// 重构第三刀: 入口生成 traceId (无 LLM 降级 chat 时嵌套新 trace, 独立可追踪)
|
|
511
|
+
return runWithTrace(async () => {
|
|
512
|
+
if (!this.llm) return this.chat(userMsg, { sessionKey });
|
|
513
|
+
// 支出预算闸门 (2026-10-03l): 超限后不再发请求, 直接以提示文案收尾本轮流
|
|
514
|
+
if (this._budgetExceeded) { const m = this._budgetMessage(); if (onDelta) onDelta(m); return m; }
|
|
515
|
+
this.clearInterrupt(sessionKey); // 新一轮对话开始, 复位本会话中断状态
|
|
516
|
+
// 内核自主决策: 高置信简单指令本地处理
|
|
517
|
+
const local = (this.config.agent?.localIntent !== false) ? await this._localIntent(userMsg) : null;
|
|
518
|
+
if (local) { onDelta && onDelta(local); return local; }
|
|
519
|
+
const system = this._context(userMsg);
|
|
520
|
+
const history = await this._loadHistory(sessionKey);
|
|
521
|
+
const messages = [{ role: "system", content: system }, ...history, { role: "user", content: this._userContent(userMsg) }];
|
|
522
|
+
|
|
523
|
+
// 多模态路由: 消息含图片时优先 vision provider (否则图片发到文本后端无意义)
|
|
524
|
+
const hasImage = messages.some((m) => Array.isArray(m.content) && m.content.some((c) => c && c.type === "image_url"));
|
|
525
|
+
const activeLLM = hasImage ? (this._visionLLM() || this.llm) : this.llm;
|
|
526
|
+
|
|
527
|
+
// v3.0.1 (P1#4): 按轮回调注册 (traceId 路由) —— 并发流式会话互不覆盖,
|
|
528
|
+
// 替代原 prev/restore 单槽模式 (并发下 A 结束会抹掉 B 的回调, B 结束会泄漏 A 的回调)
|
|
529
|
+
const tid = currentTrace()?.traceId;
|
|
530
|
+
if (tid && (onTool || onStep)) {
|
|
531
|
+
this._turnCbs.set(tid, {
|
|
532
|
+
onTool: onTool ? (ev) => { try { onTool(ev); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } } : null,
|
|
533
|
+
onStep: onStep ? (ev) => { try { onStep(ev); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } } : null,
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// v3.1 首片 (P1#8): Turn 投影 begin (纯可观测)
|
|
538
|
+
const turnInfo = this.turnProjection.begin(sessionKey, userMsg);
|
|
539
|
+
let turnEnded = false;
|
|
540
|
+
const _endTurn = (interrupted) => {
|
|
541
|
+
if (turnEnded) return;
|
|
542
|
+
turnEnded = true;
|
|
543
|
+
const endInfo = interrupted ? this.turnProjection.abort(sessionKey) : this.turnProjection.complete(sessionKey);
|
|
544
|
+
if (turnInfo) {
|
|
545
|
+
this.bus?.emit("chat/turn/end", { sessionKey, ...turnInfo, ...endInfo, interrupted }, { source: "agent.chatStream" });
|
|
546
|
+
this.protocolBus?.eq?.push?.({ type: "TASK_TURN_ENDED", payload: { sessionKey, ...turnInfo, ...endInfo, interrupted } });
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
let reply;
|
|
551
|
+
try {
|
|
552
|
+
try {
|
|
553
|
+
// 无工具开启: 直连后端可逐字流式 (恢复打字机效果); 有工具时走工具循环保轨迹完整 [复审 P2]
|
|
554
|
+
if (!this.toolsEnabled && activeLLM.supportsStream) {
|
|
555
|
+
reply = await activeLLM.streamChat(messages, {
|
|
556
|
+
onDelta: (d) => { onDelta && onDelta(d); },
|
|
557
|
+
});
|
|
558
|
+
} else {
|
|
559
|
+
reply = await this._llmWithTools(messages, activeLLM);
|
|
560
|
+
if (onDelta) onDelta(reply);
|
|
561
|
+
}
|
|
562
|
+
} catch (e) {
|
|
563
|
+
warn("chatStream 失败, 降级非流式 chat:", e.message);
|
|
564
|
+
reply = await this.chat(userMsg, { sessionKey });
|
|
565
|
+
if (onDelta) onDelta(reply);
|
|
566
|
+
} finally {
|
|
567
|
+
// v3.0.1 (P1#4): 清本轮按回注册 (原 prev/restore 模式已废弃)
|
|
568
|
+
if (tid) this._turnCbs.delete(tid);
|
|
569
|
+
this._takeFallback(); // 丢弃本轨降级事实 (chatStream 不向用户展示降级提示)
|
|
570
|
+
}
|
|
571
|
+
// 落库用模型原文 (剥掉本轮降级提示), 与 chat 共用同一路径
|
|
572
|
+
await this._persistTurn(sessionKey, userMsg, this._stripFallbackNotice(reply));
|
|
573
|
+
// v3.1 首片 (P1#8): 本轮被中断 → turn 标 aborted, 否则正常 closed
|
|
574
|
+
_endTurn(this.isInterrupted(sessionKey));
|
|
575
|
+
return reply;
|
|
576
|
+
} catch (e) {
|
|
577
|
+
// v3.1 首片: 异常路径 turn 收口为 aborted 后原样抛出 (chatStream 自身无兜底, 保持原语义)
|
|
578
|
+
throw e;
|
|
579
|
+
} finally {
|
|
580
|
+
_endTurn(true);
|
|
581
|
+
}
|
|
582
|
+
}, { sessionKey, channel: "chatStream" });
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// 多 provider 回退: 依次尝试, 失败切下一个
|
|
586
|
+
// 多 provider 并发健康探测 + 回退: 只对可用 provider 调用, 避免串行等待 180s 超时
|
|
587
|
+
async _llmWithFallback(seedMessages) {
|
|
588
|
+
let clients = (this.allProviders || []).length ? this.allProviders : [this.llm];
|
|
589
|
+
// 多模态路由: 消息含图片 (image_url 块) 时, 优先 vision provider, 避免图片发到文本后端浪费
|
|
590
|
+
const hasImage = seedMessages.some((m) => Array.isArray(m.content) && m.content.some((c) => c && c.type === "image_url"));
|
|
591
|
+
if (hasImage) {
|
|
592
|
+
const visionClients = clients.filter((c) => c.vision);
|
|
593
|
+
if (visionClients.length) clients = visionClients;
|
|
594
|
+
else info("消息含图片但无 vision provider, 图片将无法被模型理解 (请配置 vision: true 的 provider)");
|
|
595
|
+
}
|
|
596
|
+
// 工具类任务: 优先原生 tool_calls 后端 (http)。
|
|
597
|
+
// openclaw 是完整 agent 运行时, 会拒绝围栏协议(视为伪协议); 实测 http 原生 tool_calls 全链路通过。
|
|
598
|
+
// v2.5.0: 外部引擎底座已移除, 仅 http 后端 (原生 tool_calls + 文本工具修复)。
|
|
599
|
+
if (this.toolsEnabled) {
|
|
600
|
+
const native = clients.filter((c) => c.supportsNativeToolCalls);
|
|
601
|
+
const fence = clients.filter((c) => !c.supportsNativeToolCalls);
|
|
602
|
+
if (native.length) clients = [...native, ...fence];
|
|
603
|
+
}
|
|
604
|
+
if (clients.length > 1) {
|
|
605
|
+
try {
|
|
606
|
+
const states = await Promise.all(clients.map((c) => c.health ? c.health() : Promise.resolve(true)));
|
|
607
|
+
const healthy = clients.filter((_, i) => states[i]);
|
|
608
|
+
if (healthy.length) clients = healthy;
|
|
609
|
+
else info("所有 provider 健康探测失败, 按原配置顺序尝试兜底");
|
|
610
|
+
} catch (e) {
|
|
611
|
+
warn("health 探测异常, 按原顺序回退:", e.message);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
let lastErr = null;
|
|
615
|
+
// v1.0.8 修复 (P2-2): 记录"本轮发生了降级切换"这一事实并对外广播。
|
|
616
|
+
// 原实现只在日志里 warn 一句, 用户侧完全静默 —— 拿到的是备用模型的回答却无从感知。
|
|
617
|
+
// 注意: **不改动返回值** (回退语义本身保持透明, chaos 测试锁死了成功时返回原始文本)。
|
|
618
|
+
const failed = [];
|
|
619
|
+
for (const client of clients) {
|
|
620
|
+
try {
|
|
621
|
+
const out = await this._llmWithTools(seedMessages, client);
|
|
622
|
+
if (failed.length) {
|
|
623
|
+
// v3.0.1 (P1#4): 降级事实按轮存 (traceId 路由), 并发会话各记各的
|
|
624
|
+
const fb = {
|
|
625
|
+
from: failed[0].model,
|
|
626
|
+
to: client.model,
|
|
627
|
+
reason: failed[0].message,
|
|
628
|
+
chain: failed.map((f) => f.model),
|
|
629
|
+
ts: Date.now(),
|
|
630
|
+
};
|
|
631
|
+
const _tid = currentTrace()?.traceId;
|
|
632
|
+
if (_tid) this._turnFallbacks.set(_tid, fb); else this._lastFallback = fb;
|
|
633
|
+
this.bus?.emit("llm/fallback", fb, { source: "agent._llmWithFallback" });
|
|
634
|
+
warn(`provider 降级: ${fb.from} 不可用 → 已切至 ${client.model} (${failed[0].message})`);
|
|
635
|
+
}
|
|
636
|
+
return out;
|
|
637
|
+
} catch (e) {
|
|
638
|
+
lastErr = e;
|
|
639
|
+
failed.push({ model: client.model, message: e.message });
|
|
640
|
+
warn("provider 失败, 切换下一个:", client.model, e.message);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
throw lastErr || new Error("所有 provider 均失败");
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// 降级原因"人话化": 原始错误常带整段 JSON 报错体 (含 key 片段/内部字段),
|
|
647
|
+
// 既不适合直接给用户看, 也没必要。这里归一到几类常见故障。
|
|
648
|
+
_shortReason(msg) {
|
|
649
|
+
const s = String(msg ?? "");
|
|
650
|
+
if (/40[13]|unauthor|api.?key|authentication|invalid_request_error/i.test(s)) return "鉴权失败 (key 无效或已过期)";
|
|
651
|
+
if (/429|rate.?limit|too many requests|quota|insufficient/i.test(s)) return "限流或额度不足";
|
|
652
|
+
if (/timeout|timed out|abort|ETIMEDOUT/i.test(s)) return "请求超时";
|
|
653
|
+
if (/ECONNREFUSED|ENOTFOUND|ECONNRESET|fetch failed|socket hang up/i.test(s)) return "连接失败";
|
|
654
|
+
if (/\b50[0-9]\b/.test(s)) return "服务端错误";
|
|
655
|
+
const brief = s.replace(/\{[\s\S]*$/, "").replace(/\s+/g, " ").trim();
|
|
656
|
+
return (brief || "调用失败").slice(0, 60);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// 降级提示文案 (用户可见) —— 由 chat/chatStream 在回复末尾追加
|
|
660
|
+
_fallbackNotice(fb) {
|
|
661
|
+
if (!fb) return "";
|
|
662
|
+
const chain = fb.chain && fb.chain.length > 1 ? ` (失败链: ${fb.chain.join(" → ")})` : "";
|
|
663
|
+
return `${FALLBACK_NOTICE_TAG}主模型 ${fb.from} 不可用: ${this._shortReason(fb.reason)}。本轮回答已自动切换到 ${fb.to}${chain}。`;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 从回复中剥掉降级提示: 保证写入会话历史/记忆的是模型原文, 提示只面向当轮用户可见
|
|
667
|
+
_stripFallbackNotice(text) {
|
|
668
|
+
const s = String(text ?? "");
|
|
669
|
+
const i = s.indexOf(FALLBACK_NOTICE_TAG);
|
|
670
|
+
return i === -1 ? s : s.slice(0, i).trimEnd();
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// B2: 审批缓存 key 计算 — 仅命令类工具参与 (非命令工具改参数不放行, 防止漏审对象被缓存)
|
|
674
|
+
// 复用 command-guard 的 normalizeCommand (去引号防绕过 + 合并空白), 防 `echo a` / `echo "a"` 变体
|
|
675
|
+
// 造成同命令不同 key 或异命令同 key 导致漏审。返回 null = 不缓存该工具。
|
|
676
|
+
_approvalCacheKey(name, args) {
|
|
677
|
+
if (!/run_command|shell|exec|bash|code_act/i.test(String(name))) return null;
|
|
678
|
+
const cmd = args?.command ?? args?.cmd ?? args?.code ?? args?.script;
|
|
679
|
+
if (typeof cmd !== "string" || !cmd.trim()) return null;
|
|
680
|
+
const norm = normalizeCommand(cmd);
|
|
681
|
+
return norm ? `${name}:${norm}` : null;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// 统一工具执行入口 (http 原生 tool_calls + 文本工具调用修复) [P0#1]
|
|
685
|
+
// v1.0.7: 移除未使用的 llmInstance 死参数, 所有工具执行统一走此入口 (trace/事件只此一份)
|
|
686
|
+
// 2026-09-17 体检修复: start/done 事件带唯一 callId。
|
|
687
|
+
// 原先 Web UI 只能按"工具名"匹配起止事件, 同一轮里出现两个 read_file 时,
|
|
688
|
+
// 后到的事件会回填到前一张卡片上 (public/app.js 旧实现注释里也自述了这个缺陷)。
|
|
689
|
+
// v3.0 (codex 对齐): PreToolUse 钩子(可否决/改参) → 权限引擎(deny/ask/allow) → 执行 → PostToolUse 钩子
|
|
690
|
+
async _runTool(name, args) {
|
|
691
|
+
const t0 = Date.now();
|
|
692
|
+
// v3.0.1 (P1#4): 按轮标记"用过工具" (traceId 路由), 并发会话互不串台; 无 trace 时回落单槽
|
|
693
|
+
const _tid = currentTrace()?.traceId;
|
|
694
|
+
if (_tid) this._turnsUsedTools.add(_tid); else this._lastTurnUsedTools = true;
|
|
695
|
+
// 原型污染消毒: 剥离 __proto__/constructor/prototype 键 (LLM JSON 可携带恶意键)
|
|
696
|
+
const stripped = stripProtoKeys(args);
|
|
697
|
+
if (stripped.stripped.length) {
|
|
698
|
+
this.tracer.event("security/proto-stripped", { tool: name, keys: stripped.stripped });
|
|
699
|
+
args = stripped.clean;
|
|
700
|
+
}
|
|
701
|
+
const callId = `t${++this._toolCallSeq}-${t0.toString(36)}`;
|
|
702
|
+
|
|
703
|
+
// ① 准入: PreToolUse 钩子链 (可否决/改参) + 权限引擎 (deny/ask/审批缓存)
|
|
704
|
+
const admit = await this._admitToolCall(name, args, callId, t0);
|
|
705
|
+
if (!admit.ok) return admit.error;
|
|
706
|
+
args = admit.args;
|
|
707
|
+
|
|
708
|
+
this.bus?.emit("tool/call", { name, args, callId }, { source: "agent._runTool" });
|
|
709
|
+
this._emitTool({ type: "start", id: callId, tool: name, args, ts: Date.now() });
|
|
710
|
+
// let 而非 const: 后处理(注入标注/病历回灌)与 PostToolUse 钩子都会追加内容
|
|
711
|
+
// (2026-09-18 修复: 原为 const, 钩子追加时抛 "Assignment to constant variable" 被 catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } 静默吞掉)
|
|
712
|
+
let result = await this.tools.call(name, args, { agent: this, timeoutMs: Number(this.config.agent?.tool_timeout_ms) || 0 });
|
|
713
|
+
const ok = !result.startsWith(TOOL_ERROR_PREFIX);
|
|
714
|
+
|
|
715
|
+
// ② 结果加工: 注入扫描 → 事件/轨迹落库 → 故障病历回灌 (均可能改写 result)
|
|
716
|
+
result = this._processToolOutcome(name, args, callId, result, ok, t0);
|
|
717
|
+
|
|
718
|
+
// ③ PostToolUse 钩子链 (可附加上下文, 追加在结果尾部)
|
|
719
|
+
if (this.hooks && ok) {
|
|
720
|
+
try {
|
|
721
|
+
const h = await this.hooks.emit("PostToolUse", { tool: name, args, result, callId });
|
|
722
|
+
// 2026-09-18 修复 (第二处): 原实现 map h.results 里的 r.additionalContext,
|
|
723
|
+
// 但 results 条目结构是 { result: res }, 恒取到 undefined —— 收集结果实际在
|
|
724
|
+
// h.additionalContext (hooks/index.js 统一收集)。原特性从未生效。
|
|
725
|
+
const extra = (Array.isArray(h?.additionalContext) ? h.additionalContext : [])
|
|
726
|
+
.filter(Boolean).map(String).join("\n");
|
|
727
|
+
if (extra) result = result + "\n" + extra;
|
|
728
|
+
} catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
729
|
+
}
|
|
730
|
+
this._emitToolDone(callId, name, args, ok, Date.now() - t0, result);
|
|
731
|
+
return result;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// 准入链 (从 _runTool 抽出, 原 136 行中占 50 行): 钩子 + 权限, 返回 { ok, args } 或 { ok:false, error }
|
|
735
|
+
async _admitToolCall(name, args, callId, t0) {
|
|
736
|
+
// --- v3.0: PreToolUse 钩子链 (claude-code 语义: 可否决/可改参) ---
|
|
737
|
+
if (this.hooks) {
|
|
738
|
+
try {
|
|
739
|
+
const h = await this.hooks.emit("PreToolUse", { tool: name, args, callId });
|
|
740
|
+
if (h && h.blocked) {
|
|
741
|
+
const msg = `[hook] 工具 ${name} 被 PreToolUse 钩子否决: ${h.reason || "无理由"}`;
|
|
742
|
+
this.tracer.event("tool/hook-blocked", { tool: name, reason: h.reason });
|
|
743
|
+
this._emitToolDone(callId, name, args, false, Date.now() - t0, msg);
|
|
744
|
+
return { ok: false, error: TOOL_ERROR_PREFIX + msg };
|
|
745
|
+
}
|
|
746
|
+
if (h && h.args && typeof h.args === "object") args = h.args; // 钩子改参
|
|
747
|
+
} catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } // 钩子自身异常不阻断主链
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// --- v3.0: 权限引擎 (codex AskForApproval + SandboxPolicy + 规则链) ---
|
|
751
|
+
if (this.permissions) {
|
|
752
|
+
try {
|
|
753
|
+
const perm = await this.permissions.check(name, args, { callId });
|
|
754
|
+
if (perm.decision === "deny") {
|
|
755
|
+
const msg = `[permission] 工具 ${name} 被拒绝: ${perm.reason || "命中 deny 规则"}`;
|
|
756
|
+
this.tracer.event("tool/perm-denied", { tool: name, reason: perm.reason });
|
|
757
|
+
this._emitToolDone(callId, name, args, false, Date.now() - t0, msg);
|
|
758
|
+
return { ok: false, error: TOOL_ERROR_PREFIX + msg };
|
|
759
|
+
}
|
|
760
|
+
if (perm.decision === "ask") {
|
|
761
|
+
// B2: 审批缓存 (codex ApprovalStore 语义) — 会话内相同命令已批准过则不重复 ask
|
|
762
|
+
// 仅命令类工具参与; 缓存 key = `${tool}:${normalizeCommand(command)}`
|
|
763
|
+
// 命中 = 本次会话已显式批准, 视为放行; 拒绝/超时永不入缓存
|
|
764
|
+
const cacheKey = this._approvalCacheKey(name, args);
|
|
765
|
+
const cached = cacheKey && this.config.agent?.approval_cache !== false && this._approvalCache.get(cacheKey);
|
|
766
|
+
let upd = null;
|
|
767
|
+
if (cached) {
|
|
768
|
+
this.tracer.event("approval/cache-hit", { tool: name, key: cacheKey });
|
|
769
|
+
upd = {}; // 视为已批准
|
|
770
|
+
} else {
|
|
771
|
+
upd = await this._requestApproval({ tool: name, args, reason: perm.reason });
|
|
772
|
+
// 仅批准写入缓存 (拒绝/超时永不入缓存, 防止漏审命令被放行)
|
|
773
|
+
if (upd && cacheKey) this._approvalCache.set(cacheKey, perm.reason || "");
|
|
774
|
+
}
|
|
775
|
+
if (upd && typeof upd === "object" && upd.updatedInput) args = upd.updatedInput;
|
|
776
|
+
if (!upd) {
|
|
777
|
+
const msg = `[permission] 工具 ${name} 审批被拒绝或超时`;
|
|
778
|
+
this.tracer.event("tool/perm-rejected", { tool: name });
|
|
779
|
+
this._emitToolDone(callId, name, args, false, Date.now() - t0, msg);
|
|
780
|
+
return { ok: false, error: TOOL_ERROR_PREFIX + msg };
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
} catch (e) {
|
|
784
|
+
// 2026-10-03 修复 (P1): 原实现吞异常后放行 (fail-open) —— 权限引擎自己崩了
|
|
785
|
+
// 防线反而全开, 与 permissions 内核 fail-closed (应答者异常 → deny) 口径相反。
|
|
786
|
+
// 改为 fail-closed: 引擎异常 = 防线失效, 拒绝执行并给可判读错误。
|
|
787
|
+
const msg = `[permission] 权限引擎异常, 已拒绝执行工具 ${name}: ${e && typeof e.message === "string" ? e.message : String(e)}`;
|
|
788
|
+
this.tracer.event("tool/perm-error", { tool: name });
|
|
789
|
+
this._emitToolDone(callId, name, args, false, Date.now() - t0, msg);
|
|
790
|
+
return { ok: false, error: TOOL_ERROR_PREFIX + msg };
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
return { ok: true, args };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// 结果加工 (从 _runTool 抽出): 注入扫描 → 事件/轨迹 → 故障病历回灌; 返回可能被改写的 result
|
|
797
|
+
_processToolOutcome(name, args, callId, result, ok, t0) {
|
|
798
|
+
// 提示注入防线 (2026-10-03 红队驱动): 工具输出 = 不可信数据。
|
|
799
|
+
// 疑似注入 → 原文外包"不可信"标注 + 安全事件上 tracer (数据不删, 指令不执行语义靠标注传达给模型)。
|
|
800
|
+
try {
|
|
801
|
+
if (ok && typeof result === "string" && result.length > 20 && !result.startsWith("{")) {
|
|
802
|
+
const scan = scanInjection(result);
|
|
803
|
+
if (scan.suspicious) {
|
|
804
|
+
reportSuspicious(name, scan, { tracer: this.tracer });
|
|
805
|
+
result = wrapUntrusted(name, result, scan);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
} catch { /* 扫描异常不阻断工具结果 */ }
|
|
809
|
+
if (name === "spawn_agent") this.tracer.event("agent/spawn", { args });
|
|
810
|
+
this.bus?.emit("tool/result", {
|
|
811
|
+
name,
|
|
812
|
+
callId,
|
|
813
|
+
ok,
|
|
814
|
+
args,
|
|
815
|
+
durationMs: Date.now() - t0,
|
|
816
|
+
error: ok ? null : result.slice(0, 300),
|
|
817
|
+
}, { source: "agent._runTool" });
|
|
818
|
+
this.traces.record({
|
|
819
|
+
tool: name,
|
|
820
|
+
args,
|
|
821
|
+
result: result.slice(0, 800),
|
|
822
|
+
ok,
|
|
823
|
+
durationMs: Date.now() - t0,
|
|
824
|
+
error: ok ? null : result,
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
// --- 故障病历闭环 (2026-10-03 接线: 该模块此前"能力就绪、链路未接", 零消费) ---
|
|
828
|
+
// 写: 本次失败落成结构化病历 (工具 / 错误文本 / 归类 / trace 引用)
|
|
829
|
+
// 读: 检索历史同类故障, 把"已知根因 + 修法"附在错误结果之后, 模型不必从零推理
|
|
830
|
+
// 顺序: 先 search 再 record —— 否则本次失败会命中自己
|
|
831
|
+
if (!ok && this.failures) {
|
|
832
|
+
try {
|
|
833
|
+
const errText = String(result).slice(0, 300);
|
|
834
|
+
const past = this.failures.search({ tool: name, error: errText, limit: 2 });
|
|
835
|
+
const ep = this.failures.record({ tool: name, error: errText, category: classifyFailure(errText), traceRef: callId });
|
|
836
|
+
// 有根因/修法档案的直接给结论; 只有原始错误的给"复发计数"信号
|
|
837
|
+
const withFix = past.filter((p) => p.fix || p.rootCause);
|
|
838
|
+
if (withFix.length) {
|
|
839
|
+
const lines = withFix.map((p) => `- [${p.tool}/${p.category}] ${p.rootCause || p.error}${p.fix ? ` → 修法: ${p.fix}` : ""}`);
|
|
840
|
+
result = result + "\n【历史同类故障·供参考】\n" + lines.join("\n");
|
|
841
|
+
this.tracer.event("failure/history-hit", { tool: name, hits: lines.length, mode: "prescription", episodeId: ep.id });
|
|
842
|
+
} else if (past.length) {
|
|
843
|
+
result = result + `\n【复发警告】同类失败历史已有 ${past.length} 次记录 (工具 ${name}, 归类 ${ep.category}), 请勿重复相同调用, 换策略或先排查环境。`;
|
|
844
|
+
this.tracer.event("failure/history-hit", { tool: name, hits: past.length, mode: "repeat-warning", episodeId: ep.id });
|
|
845
|
+
}
|
|
846
|
+
} catch { /* 病历读写异常不阻断工具结果 */ }
|
|
847
|
+
}
|
|
848
|
+
return result;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
// v3.0: 统一 done 事件发射 (原内联三处, 收敛为一个私有方法)
|
|
852
|
+
_emitToolDone(callId, name, args, ok, durationMs, result) {
|
|
853
|
+
this._emitTool({ type: "done", id: callId, tool: name, args, ok, durationMs, result: String(result == null ? "" : result).slice(0, 300), ts: Date.now() });
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// v3.0 (codex approval flow): 产生审批请求并等待 Web UI/通道裁决
|
|
857
|
+
// 返回: { updatedInput? } 表示批准; null 表示拒绝/超时
|
|
858
|
+
_requestApproval({ tool, args, reason }) {
|
|
859
|
+
return new Promise((resolve) => {
|
|
860
|
+
const id = `ap_${++this._approvalSeq}_${Date.now().toString(36)}`;
|
|
861
|
+
const kind = /run_command|shell|exec|bash/.test(tool) ? "bash"
|
|
862
|
+
: /edit|write|apply_patch|create_file/.test(tool) ? "edit"
|
|
863
|
+
: /plan/.test(tool) ? "plan" : "tool";
|
|
864
|
+
const summary = args?.command || args?.cmd || args?.path || args?.file_path || JSON.stringify(args || {}).slice(0, 160);
|
|
865
|
+
const req = { id, kind, tool, args, summary: String(summary || ""), reason: reason || "", ts: Date.now() };
|
|
866
|
+
const timeoutMs = Number(this.config.agent?.approval_timeout_ms) || 120000;
|
|
867
|
+
const entry = { req, resolve, timer: null };
|
|
868
|
+
entry.timer = setTimeout(() => {
|
|
869
|
+
this._pendingApprovals.delete(id);
|
|
870
|
+
resolve(null); // 超时视为拒绝 (codex: 等待审批超时不静默放行)
|
|
871
|
+
}, timeoutMs);
|
|
872
|
+
this._pendingApprovals.set(id, entry);
|
|
873
|
+
// 事件外投: SSE onApproval / tracer / 协议总线
|
|
874
|
+
this.tracer.event("approval/requested", { id, tool, kind });
|
|
875
|
+
this.protocolBus?.eq?.push?.({ type: "APPROVAL_REQUESTED", payload: req });
|
|
876
|
+
if (this._onApprovalEvent) { try { this._onApprovalEvent(req); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } }
|
|
877
|
+
this.bus?.emit("approval/requested", req, { source: "agent.approval" });
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// v3.0: 审批裁决入口 (HTTP POST /api/approvals/:id 调用)
|
|
882
|
+
// 返回 true 表示存在该审批且已裁决
|
|
883
|
+
resolveApproval(id, decision) {
|
|
884
|
+
const entry = this._pendingApprovals.get(id);
|
|
885
|
+
if (!entry) return false;
|
|
886
|
+
clearTimeout(entry.timer);
|
|
887
|
+
this._pendingApprovals.delete(id);
|
|
888
|
+
const approved = decision === "approve";
|
|
889
|
+
this.tracer.event("approval/resolved", { id, decision });
|
|
890
|
+
this.protocolBus?.eq?.push?.({ type: "APPROVAL_RESOLVED", payload: { id, decision } });
|
|
891
|
+
entry.resolve(approved ? {} : null);
|
|
892
|
+
return true;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
// v3.0: 待审批清单 (HTTP GET /api/approvals/pending)
|
|
896
|
+
pendingApprovals() {
|
|
897
|
+
return [...this._pendingApprovals.values()].map((e) => e.req);
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// LLM + 工具调用循环 (带 provider 回退)
|
|
901
|
+
// 重构第一刀 (2026-09-14): 循环驱动/探索熔断/重复检测/溢出降档/错误重试
|
|
902
|
+
// 全部收敛到 src/core/policy.js runToolLoop, 此处只注入依赖, 策略可独立测试/替换。
|
|
903
|
+
async _llmWithTools(seedMessages, llmInstance = this.llm) {
|
|
904
|
+
return runToolLoop({
|
|
905
|
+
seedMessages,
|
|
906
|
+
llm: llmInstance,
|
|
907
|
+
tools: this.toolsEnabled ? this.tools.toOpenAI() : [],
|
|
908
|
+
config: this.config,
|
|
909
|
+
// v3.0.1 (P1#3): 从 ALS trace 取当前会话 key, 只响应该会话的中断 (并发不串台)
|
|
910
|
+
isInterrupted: () => this.isInterrupted(currentTrace()?.sessionKey),
|
|
911
|
+
onStep: (ev) => this._emitStep(ev),
|
|
912
|
+
runTool: (name, args) => this._runTool(name, args),
|
|
913
|
+
shrinkMessages: (messages, budget) => this._shrinkMessagesForOverflow(messages, budget),
|
|
914
|
+
histTokenCap: () => this._histTokenCap(),
|
|
915
|
+
onEvent: (type, payload) => { this._onPolicyEvent(type, payload); this.tracer.event(type, payload); },
|
|
916
|
+
// v1.6.0 第四刀: 超时重试决策 (幂等才重试) + 超时预算查询 (事件采集)
|
|
917
|
+
isIdempotentTool: (name) => this._toolIdempotent(name),
|
|
918
|
+
toolTimeoutOf: (name) => this._toolTimeoutOf(name),
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// 政策事件回流: 默认转 tracer 埋点; 对会沉淀智力的关键事件 (B3 反思闸门拦停) 额外写入经验库
|
|
923
|
+
// 反思闭环: 硬拒绝类失败不只"这次不重试", 还自动记住"以后不这么干"。
|
|
924
|
+
// 幂等: Experience.learn 本身去重 (同 lesson 命中 uses+1), 高频触发只会强化不会写放大。
|
|
925
|
+
_onPolicyEvent(type, payload = {}) {
|
|
926
|
+
try {
|
|
927
|
+
if (type === "tool/self_review_stop" && this.experience) {
|
|
928
|
+
this.experience.learn({
|
|
929
|
+
task: "工具调用被反思闸门拦停",
|
|
930
|
+
outcome: "硬拒绝类错误 (黑名单拦截/审批被拒/权限拒绝/DENY_HINT), 盲目重试或改写命令会绕过安全闸门",
|
|
931
|
+
lesson: "硬拒绝类错误不应重试或改写命令绕过 — 停下说明原因或请用户调整 security 配置。工具: " + String(payload?.reason || ""),
|
|
932
|
+
tags: ["auto-self-review", "tool-denied"],
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
} catch { /* 经验沉淀失败不阻断主链 (不因记教训崩掉干活) */ }
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// 工具是否幂等 (可安全超时重试): 读取工具元数据, 未声明默认 true (只读/查询类)
|
|
939
|
+
_toolIdempotent(name) {
|
|
940
|
+
try {
|
|
941
|
+
const m = this.tools && typeof this.tools.metaOf === "function" ? this.tools.metaOf(name) : null;
|
|
942
|
+
return m ? !!m.idempotent : true;
|
|
943
|
+
} catch { return true; }
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// 工具超时预算 (ms, 0/无声明 = 用全局默认; 事件采集用)
|
|
947
|
+
_toolTimeoutOf(name) {
|
|
948
|
+
try {
|
|
949
|
+
const m = this.tools && typeof this.tools.metaOf === "function" ? this.tools.metaOf(name) : null;
|
|
950
|
+
return m ? (m.timeoutMs > 0 ? m.timeoutMs : (Number(this.config.agent?.tool_timeout_ms) || 0)) : (Number(this.config.agent?.tool_timeout_ms) || 0);
|
|
951
|
+
} catch { return Number(this.config.agent?.tool_timeout_ms) || 0; }
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// 工具原始结果 → 人类可读文本 (仅供 _localIntent 直接回给用户时使用)
|
|
955
|
+
// v1.0.8 修复 (P2-1): 原实现 `return \`[工具] ${await this.tools.call(...)}\`` 直接拼字符串,
|
|
956
|
+
// 把内部标记 `[工具]`、错误前缀与原始 JSON (如 {"ok":true,"id":"..."}) 原封不动喷给用户。
|
|
957
|
+
// 这一层做"外向化": 错误 → 可读失败语; JSON → 抽取载荷字段; 数组 → 逐项罗列; 其余原样。
|
|
958
|
+
// 注意: 这是**用户可见文本**的专用通道, 模型侧上下文里的工具结果不走这里 (保持原始保真)。
|
|
959
|
+
_humanToolResult(raw) {
|
|
960
|
+
const s = String(raw ?? "").trim();
|
|
961
|
+
if (!s) return "(没有返回内容)";
|
|
962
|
+
if (s.startsWith(TOOL_ERROR_PREFIX)) {
|
|
963
|
+
return "没办成: " + s.slice(TOOL_ERROR_PREFIX.length).trim();
|
|
964
|
+
}
|
|
965
|
+
if (s.startsWith("{") || s.startsWith("[")) {
|
|
966
|
+
let obj;
|
|
967
|
+
try { obj = JSON.parse(s); } catch { return s; }
|
|
968
|
+
if (Array.isArray(obj)) {
|
|
969
|
+
return obj.length
|
|
970
|
+
? obj.map((x) => (typeof x === "string" ? x : (x && (x.name || x.path || x.title)) || JSON.stringify(x))).join("\n")
|
|
971
|
+
: "(空)";
|
|
972
|
+
}
|
|
973
|
+
if (obj && typeof obj === "object") {
|
|
974
|
+
if (obj.error) return "没办成: " + String(obj.error);
|
|
975
|
+
for (const k of ["text", "content", "message", "result", "data", "output"]) {
|
|
976
|
+
const v = obj[k];
|
|
977
|
+
if (typeof v === "string" && v.trim()) return v;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
return s; // 结构无法识别: 原样返回, 不丢信息
|
|
981
|
+
}
|
|
982
|
+
return s;
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// 离线工具路由: 无 LLM 时识别简单工具指令
|
|
986
|
+
// ---- 内核自主决策: 本地意图预判层 (P2-7) ----
|
|
987
|
+
// 高置信简单指令(问候/时间/记忆/明确工具)本地处理, 不调 LLM, 省成本更快
|
|
988
|
+
async _localIntent(userMsg) {
|
|
989
|
+
const m = String(userMsg).trim();
|
|
990
|
+
// 纯问候/告别/感谢 (短句, 高置信)
|
|
991
|
+
const greet = /^(你好|您好|嗨|哈喽|hello|hi|在吗|早上好|晚上好|下午好|再见|拜拜|谢谢|感谢|辛苦了|good\s*(mom|afternoon|evening)|thanks?|bye)\s*[!。??]*$/i;
|
|
992
|
+
if (greet.test(m)) {
|
|
993
|
+
if (/再见|拜拜|bye/i.test(m)) return "再见兄弟, 有事随时喊我。";
|
|
994
|
+
if (/谢谢|感谢|辛苦/i.test(m)) return "客气啥, 应该的。";
|
|
995
|
+
return "在的兄弟, 说。";
|
|
996
|
+
}
|
|
997
|
+
// 时间/日期
|
|
998
|
+
if (/^(现在)?(几点|时间|日期|几号|今天|星期几)[!?。?]*$/i.test(m)) {
|
|
999
|
+
return `现在是 ${this._humanToolResult(await this.tools.call("get_time", {}))}`;
|
|
1000
|
+
}
|
|
1001
|
+
// 记忆查询: 你记得XXX / 上次聊过XXX (P1: LLM 查询扩展 + RRF 融合补语义召回)
|
|
1002
|
+
if (/^(你)?(记得|还记得|上次聊过|关于)[::]?\s*(.+)/i.test(m)) {
|
|
1003
|
+
const q = m.replace(/^(你)?(记得|还记得|上次聊过|关于)[::]?\s*/i, "").trim();
|
|
1004
|
+
const res = await this._memoryQuery(q || m, { limit: 3 });
|
|
1005
|
+
return res.length ? "我记得:\n" + res.map(r => `- ${r.content}`).join("\n") : `(记忆里没找到关于"${q || m}"的)`;
|
|
1006
|
+
}
|
|
1007
|
+
// 记住 XX
|
|
1008
|
+
const add = m.match(/^记住[::]\s*(.+)$/i);
|
|
1009
|
+
if (add) {
|
|
1010
|
+
const content = add[1].trim();
|
|
1011
|
+
const r = String(await this.tools.call("memory_add", { content }) ?? "");
|
|
1012
|
+
if (r.startsWith(TOOL_ERROR_PREFIX) || /"error"\s*:/.test(r)) return "没记上: " + this._humanToolResult(r);
|
|
1013
|
+
return `好, 记下了: ${content}`;
|
|
1014
|
+
}
|
|
1015
|
+
// 读文件 / 列目录 (明确工具指令)
|
|
1016
|
+
const read = m.match(/^读文件\s+(.+)$/i);
|
|
1017
|
+
if (read) return this._humanToolResult(await this.tools.call("read_file", { path: read[1].trim() }));
|
|
1018
|
+
const list = m.match(/^列出?\s+(\S+)?$/i);
|
|
1019
|
+
if (list) return this._humanToolResult(await this.tools.call("list_dir", { path: list[1] || "." }));
|
|
1020
|
+
return null;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// ---- Session Replay: 从 l0 原始日志恢复某会话历史 (跨天/崩溃续跑) ----
|
|
1024
|
+
replaySession(sessionKey = "default", { days = 7, limit = 40 } = {}) {
|
|
1025
|
+
const msgs = [];
|
|
1026
|
+
const now = new Date();
|
|
1027
|
+
for (let d = days - 1; d >= 0; d--) {
|
|
1028
|
+
const day = logicalDay(new Date(now.getTime() - d * 86400000));
|
|
1029
|
+
const recs = this.l0.read(day, 2000);
|
|
1030
|
+
for (const r of recs) {
|
|
1031
|
+
if (r.sessionKey !== sessionKey) continue;
|
|
1032
|
+
if (r.role === "user" || r.role === "assistant") msgs.push({ role: r.role, content: r.content, ts: r.timestamp });
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
msgs.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
1036
|
+
return msgs.slice(-limit).map(({ role, content }) => ({ role, content }));
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// 离线工具路由已并入 _localIntent (超集), 不再单独保留
|
|
1040
|
+
|
|
1041
|
+
// 工具名清单 (给 verifyLesson 做幻觉接地)
|
|
1042
|
+
_toolNames() {
|
|
1043
|
+
try {
|
|
1044
|
+
if (this.tools && typeof this.tools.listDetailed === "function") return this.tools.listDetailed().map((t) => t.name);
|
|
1045
|
+
if (this.tools && typeof this.tools.names === "function") return this.tools.names();
|
|
1046
|
+
} catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
1047
|
+
return [];
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// 用户主动经验学习 (实现已迁 memory-service, 此处分发)
|
|
1051
|
+
_learnFromTurn(userMsg, reply) {
|
|
1052
|
+
return this.memorySvc.learnFromTurn(userMsg, reply);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// P3: 自我进化闭环 - 失败→经验 (实现已迁 learning-service, 此处分发保持公共 API)
|
|
1056
|
+
async refine({ limit = 20 } = {}) {
|
|
1057
|
+
return this.learningSvc.refine({ limit });
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
// P2: 自我进化闭环 (下) - 成功→技能 (实现已迁 learning-service, 此处分发保持公共 API)
|
|
1061
|
+
async refineSkill({ limit = 50, minFreq = 2 } = {}) {
|
|
1062
|
+
return this.learningSvc.refineSkill({ limit, minFreq });
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
// 用中自进化 (实现已迁 learning-service, 此处分发保持公共 API)
|
|
1066
|
+
async upgradeSkill(id, { minUses = 3, limit = 40 } = {}) {
|
|
1067
|
+
return this.learningSvc.upgradeSkill(id, { minUses, limit });
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// L2 场景归档 (实现已迁 memory-service, 此处分发)
|
|
1071
|
+
_archiveScenes() {
|
|
1072
|
+
return this.memorySvc.archiveScenes();
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
// 可观测: 聚合各层状态 (记忆 L0-L3 / 轨迹 / 工具 / 经验 / 自愈)
|
|
1076
|
+
// 顶层展平 traces.stats() 字段 (count/failed/failRate/slowTools), 向后兼容 web 前端
|
|
1077
|
+
stats() {
|
|
1078
|
+
const sessions = this.sessionStore ? this.sessionStore.list() : [];
|
|
1079
|
+
const eventsTotal = sessions.reduce((a, s) => a + (s.count || 0), 0);
|
|
1080
|
+
const tools = this.tools ? this.tools.listDetailed() : [];
|
|
1081
|
+
return {
|
|
1082
|
+
...(this.traces && typeof this.traces.stats === "function" ? this.traces.stats() : {}),
|
|
1083
|
+
agent: {
|
|
1084
|
+
name: this.config.agent?.name || "ppx",
|
|
1085
|
+
mode: this.config.agent?.mode || "react",
|
|
1086
|
+
llm: this.llm ? (this.llm.backend || this.llm.model || "configured") : "none",
|
|
1087
|
+
},
|
|
1088
|
+
memory: {
|
|
1089
|
+
l0: { events_total: eventsTotal, sessions: sessions.length },
|
|
1090
|
+
l1: this.facts && this.facts.stats ? this.facts.stats() : {},
|
|
1091
|
+
l2: this.scenes && this.scenes.count ? { scenes: this.scenes.count() } : {},
|
|
1092
|
+
l3: this.personaStore && this.personaStore.stats ? this.personaStore.stats() : {},
|
|
1093
|
+
...(this.memory && this.memory.stats ? this.memory.stats() : {}),
|
|
1094
|
+
},
|
|
1095
|
+
tools: {
|
|
1096
|
+
total: tools.length,
|
|
1097
|
+
enabled: tools.filter((t) => t.enabled).length,
|
|
1098
|
+
list: tools.map((t) => ({ name: t.name, enabled: t.enabled, category: t.category })),
|
|
1099
|
+
},
|
|
1100
|
+
// 策略订阅者熔断状态 (2026-09-17 接入 circuit-breaker 后可观测): 只列非闭合项, 正常时为空数组
|
|
1101
|
+
policyGuard: this.tools && typeof this.tools.policyStatus === "function"
|
|
1102
|
+
? this.tools.policyStatus().filter((s) => s.state && s.state !== "closed")
|
|
1103
|
+
: [],
|
|
1104
|
+
skills: this.skills && typeof this.skills.list === "function"
|
|
1105
|
+
? this.skills.list().map((s) => ({ id: s.id, description: s.description }))
|
|
1106
|
+
: [],
|
|
1107
|
+
mcp: this._mcp ? { connected: true, count: this._mcp.count || 0 } : { connected: false, count: 0 },
|
|
1108
|
+
experience: this.experience ? { lessons: this.experience.lessons.length } : {},
|
|
1109
|
+
health: this.health || null,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
// 主动任务生成 (ANS 自主性): 扫描 L1 记忆里的待办/偏好, 生成主动提醒 (委托 ans/proactive 模块)
|
|
1114
|
+
// 返回可直接投递的文本 (保持兼容); 结构化数据见 startProactiveTicker 回调的 payload
|
|
1115
|
+
async proactiveSuggest() {
|
|
1116
|
+
const out = await suggestProactive(this);
|
|
1117
|
+
return out ? out.text : null;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// 标记待办完成 (ANS): 之后不再主动提醒 (窗口去重 + 完成跟踪)
|
|
1121
|
+
proactiveMarkDone(id) {
|
|
1122
|
+
return markTaskDone(this, id);
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// 启动主动任务生成定时器 (config.agent.proactive.enabled 才启用)
|
|
1126
|
+
// 回调契约: cb(payload) → { ts, items: [{content, importance, source, id}], text }
|
|
1127
|
+
startProactiveTicker(cb) {
|
|
1128
|
+
const cfg = this.config.agent?.proactive;
|
|
1129
|
+
if (!cfg || !cfg.enabled) return null;
|
|
1130
|
+
const ms = Number(cfg.interval_ms) || 3600000;
|
|
1131
|
+
// 2026-09-18 修复 (P2): suggestProactive 耗时超过 interval 时会重叠执行 (扫描/LLM 调用堆积),
|
|
1132
|
+
// 加防重入闸门: 上一轮未结束直接跳过本轮。
|
|
1133
|
+
let _proactiveRunning = false;
|
|
1134
|
+
this._proactiveTimer = setInterval(async () => {
|
|
1135
|
+
if (_proactiveRunning) return;
|
|
1136
|
+
_proactiveRunning = true;
|
|
1137
|
+
try {
|
|
1138
|
+
const payload = await suggestProactive(this);
|
|
1139
|
+
if (payload && typeof cb === "function") { try { cb(payload); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } }
|
|
1140
|
+
} catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); } finally { _proactiveRunning = false; }
|
|
1141
|
+
}, ms);
|
|
1142
|
+
info(`[proactive] 主动任务生成已启动 (每 ${Math.round(ms / 60000)} 分钟)`);
|
|
1143
|
+
return this._proactiveTimer;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
stopProactiveTicker() {
|
|
1147
|
+
if (this._proactiveTimer) { clearInterval(this._proactiveTimer); this._proactiveTimer = null; }
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
// 接入 MCP 服务器: 连接 + 注册工具到 catalog (可选能力, 显式调用)
|
|
1151
|
+
// servers 缺省用 config.mcp.servers; 返回注册的工具数
|
|
1152
|
+
async connectMcp(servers = null) {
|
|
1153
|
+
const list = servers || (this.config.mcp && this.config.mcp.servers) || [];
|
|
1154
|
+
if (!list.length) return 0;
|
|
1155
|
+
const r = await registerMcpTools(this.tools, list);
|
|
1156
|
+
this._mcp = r;
|
|
1157
|
+
return r.count;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// 发布首启引导: 未配置任何可用模型时明确提示 (不阻断运行)
|
|
1161
|
+
// 默认本地优先 (agent.model_preference=local), 有本地模型或云端 key 即不告警
|
|
1162
|
+
// v1.0.8 修复 (P2-3): 原判定自己手搓了一套"有本地/有云端key"逻辑, 既不看占位符也不看 model,
|
|
1163
|
+
// 于是模板里 lmstudio(base_url=127.0.0.1, model=YOUR_LOCAL_MODEL_NAME) 会把告警吞掉 ——
|
|
1164
|
+
// 用户端表现是"启动一切正常, 但每句话都在偷偷降级"。现改为直接复用路由的可用判定,
|
|
1165
|
+
// 让"启动告警"与"实际选模型"共用一个口径, 并明确指出哪些条目还是占位符。
|
|
1166
|
+
_warnMissingCloudApi() {
|
|
1167
|
+
const provs = (this.config && this.config.providers) || [];
|
|
1168
|
+
const usable = provs.filter((p) => { try { return isUsableProvider(p); } catch { return false; } });
|
|
1169
|
+
if (usable.length) return;
|
|
1170
|
+
// 全都不可用: 先点出"看着配了其实是模板占位"的条目, 再给通用引导
|
|
1171
|
+
const stubbed = provs.filter((p) => hasPlaceholderField(p));
|
|
1172
|
+
if (stubbed.length) {
|
|
1173
|
+
const detail = stubbed
|
|
1174
|
+
.map((p) => `${p.id || "?"}[${["model", "base_url", "api_key"].filter((f) => isPlaceholder(p[f])).join("/")}]`)
|
|
1175
|
+
.join(", ");
|
|
1176
|
+
warn(`以下 provider 仍是模板占位符, 已按"未配置"处理: ${detail}。请替换为真实值后重启。`);
|
|
1177
|
+
}
|
|
1178
|
+
warn(
|
|
1179
|
+
"未检测到任何可用模型。皮皮虾默认本地优先(agent.model_preference=local), 需至少满足一项:\n" +
|
|
1180
|
+
" 1) 启动本地模型服务 (LM Studio 等, 127.0.0.1 即识别) **并把 model 改成该服务里真实存在的模型名**\n" +
|
|
1181
|
+
" 2) 配置云端大模型 API key: OPENAI_API_KEY | DEEPSEEK_API_KEY | VOLCENGINE_API_KEY(需填 endpoint) | DASHSCOPE_API_KEY\n" +
|
|
1182
|
+
" 3) 或显式设 agent.model_preference=cloud 后走云端 key\n" +
|
|
1183
|
+
"详见 README「快速开始」模型接入节。"
|
|
1184
|
+
);
|
|
1185
|
+
}
|
|
1186
|
+
// 热重载提供方: 从 config/ppx.json 重建 LLM 客户端列表
|
|
1187
|
+
reloadProviders() {
|
|
1188
|
+
this.config = this._loadConfig(null);
|
|
1189
|
+
this.llm = resolveLLM(this.config);
|
|
1190
|
+
this.allProviders = resolveAllLLMs(this.config);
|
|
1191
|
+
// v3.0.1 (P1#7): 热重载产生的是全新客户端实例, 必须重新包装使用统计,
|
|
1192
|
+
// 否则旧包装随旧实例废弃, 新实例的 chat 调用不再计数 (usageStats 静默失效)
|
|
1193
|
+
this._installUsageTracking();
|
|
1194
|
+
info(`[providers] 热重载完成: ${this.allProviders.length} 个客户端`);
|
|
1195
|
+
return { llm: this.llm ? (this.llm.backend || this.llm.model) : null, count: this.allProviders.length };
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// 热重载通用设置 (用户名/安全/agent 预设): 重新加载 config + 更新内存字段
|
|
1199
|
+
// 不重建 LLM 客户端 (settings 不涉及 provider), 只让 user.name/security/agent 生效
|
|
1200
|
+
reloadSettings() {
|
|
1201
|
+
this.config = this._loadConfig(null);
|
|
1202
|
+
this.userName = this.config.user?.name || "兄弟";
|
|
1203
|
+
this.ctx.provide("userName", this.userName);
|
|
1204
|
+
// 应用 tools.disabled 变更 (设置 UI 启停工具后立即生效)
|
|
1205
|
+
this._applyDisabledTools();
|
|
1206
|
+
this._applyToolExposure(); // 披露策略也随设置热重载 (progressive/core 可在线调整)
|
|
1207
|
+
info(`[settings] 热重载完成: 用户=${this.userName}, 模式=${this.config.agent?.mode || "react"}`);
|
|
1208
|
+
return { userName: this.userName, mode: this.config.agent?.mode || "react" };
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// 应用 config.tools.disabled: 禁用列表中的工具 (启停工具 UI 写盘后生效)
|
|
1212
|
+
_applyDisabledTools() {
|
|
1213
|
+
try {
|
|
1214
|
+
const disabled = Array.isArray(this.config.tools?.disabled) ? this.config.tools.disabled : [];
|
|
1215
|
+
if (!disabled.length) return;
|
|
1216
|
+
let n = 0;
|
|
1217
|
+
for (const name of disabled) {
|
|
1218
|
+
try { if (this.tools.disable(name)) n += 1; } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
1219
|
+
}
|
|
1220
|
+
if (n) info(`[tools] 已禁用 ${n} 个工具: ${disabled.join(", ")}`);
|
|
1221
|
+
} catch { /* 工具未就绪时静默跳过 */ }
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// 应用工具披露策略 (2026-10-03, 上下文工程): 只把核心工具的完整 schema 发给 LLM, 其余按需加载。
|
|
1225
|
+
// 与 enabled 正交: 未披露的工具仍可被 catalog.call 调用 (内部链路/测试不受影响), 只是不进 tools 参数。
|
|
1226
|
+
// 动机: 59 工具的 JSON schema 实测约 6725 tok/请求, 而单个任务通常只用 3–5 个。
|
|
1227
|
+
_applyToolExposure() {
|
|
1228
|
+
try {
|
|
1229
|
+
const cfg = this.config.tools || {};
|
|
1230
|
+
if (cfg.progressive === false) {
|
|
1231
|
+
this.tools.setExposure(null); // 旧行为: 全量披露
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
const core = Array.isArray(cfg.core) && cfg.core.length ? cfg.core : null;
|
|
1235
|
+
if (!core) {
|
|
1236
|
+
this.tools.setExposure(null);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
const known = new Set(this.tools.list());
|
|
1240
|
+
const effective = core.filter((n) => known.has(n));
|
|
1241
|
+
this.tools.setExposure(effective);
|
|
1242
|
+
const hidden = this.tools.hiddenFromLLM().length;
|
|
1243
|
+
if (hidden) info(`[tools] 渐进披露: ${effective.length} 个核心工具进 LLM schema, 其余 ${hidden} 个按需加载`);
|
|
1244
|
+
} catch { /* 工具未就绪时静默跳过 */ }
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// ZCode 使用统计对齐: 包装所有 provider 的 chat/apiChat, 累计 token/调用次数 (零侵入)
|
|
1248
|
+
_installUsageTracking() {
|
|
1249
|
+
const clients = new Set([...(this.allProviders || []), this.llm].filter(Boolean));
|
|
1250
|
+
for (const c of clients) {
|
|
1251
|
+
if (!c || c.__ppxUsageWrapped) continue;
|
|
1252
|
+
c.__ppxUsageWrapped = true;
|
|
1253
|
+
for (const meth of ["apiChat", "chat"]) {
|
|
1254
|
+
if (typeof c[meth] !== "function") continue;
|
|
1255
|
+
const orig = c[meth].bind(c);
|
|
1256
|
+
c[meth] = async (msgs, opts) => {
|
|
1257
|
+
const r = await orig(msgs, opts);
|
|
1258
|
+
try {
|
|
1259
|
+
const u = r?.usage;
|
|
1260
|
+
const tk = (u?.total_tokens ?? (u?.prompt_tokens || 0) + (u?.completion_tokens || 0)) || 0;
|
|
1261
|
+
this.usageStats.calls++;
|
|
1262
|
+
this.usageStats.tokens += tk;
|
|
1263
|
+
const m = c.model || "unknown";
|
|
1264
|
+
this.usageStats.byModel[m] ??= { calls: 0, tokens: 0, cost: 0 };
|
|
1265
|
+
this.usageStats.byModel[m].calls++;
|
|
1266
|
+
this.usageStats.byModel[m].tokens += tk;
|
|
1267
|
+
// 成本折算 (2026-10-03l): 内置价格表前缀匹配, config.budget.model_prices 可覆盖;
|
|
1268
|
+
// 未知模型 cost 记 0 (不编数字), 想纳入预算就显式配价格
|
|
1269
|
+
let cost = 0;
|
|
1270
|
+
try { cost = estimateCost(m, u || {}, this.config?.budget?.model_prices); } catch { /* 价格异常不影响主链 */ }
|
|
1271
|
+
if (cost > 0) {
|
|
1272
|
+
this.usageStats.cost = Math.round(((this.usageStats.cost || 0) + cost) * 1e6) / 1e6;
|
|
1273
|
+
this.usageStats.byModel[m].cost = Math.round(((this.usageStats.byModel[m].cost || 0) + cost) * 1e6) / 1e6;
|
|
1274
|
+
this._checkBudget();
|
|
1275
|
+
}
|
|
1276
|
+
} catch { /* 统计失败不阻塞调用 */ }
|
|
1277
|
+
return r;
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// 支出预算判定 (2026-10-03l): config.budget.usd (USD, 0/缺省 = 不限), 达限后置位。
|
|
1284
|
+
// 闸门只拦 chat/chatStream 用户入口 (后台 refine/记忆提炼是小额, 不拦以免自学习链路断粮);
|
|
1285
|
+
// 按进程累计, 重启归零 —— 跨进程持久预算属外部计量职责, 这里诚实不做。
|
|
1286
|
+
_checkBudget() {
|
|
1287
|
+
const cap = Number(this.config?.budget?.usd);
|
|
1288
|
+
if (!Number.isFinite(cap) || cap <= 0 || this._budgetExceeded) return;
|
|
1289
|
+
if ((this.usageStats.cost || 0) < cap) return;
|
|
1290
|
+
this._budgetExceeded = true;
|
|
1291
|
+
this.tracer?.event("budget/exceeded", { cost: this.usageStats.cost, budgetUsd: cap });
|
|
1292
|
+
this.bus?.emit("budget/exceeded", { cost: this.usageStats.cost, budgetUsd: cap }, { source: "agent._checkBudget" });
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// 预算耗尽的用户可见文案
|
|
1296
|
+
_budgetMessage() {
|
|
1297
|
+
const cap = Number(this.config?.budget?.usd);
|
|
1298
|
+
return `[预算耗尽] 本进程累计支出 $${(this.usageStats.cost || 0).toFixed(4)} 已达上限 $${(Number.isFinite(cap) ? cap : 0).toFixed(2)},已停止继续调用模型。调整 config/ppx.json 的 budget.usd(0 或删除 = 不限)后重启生效。`;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
shutdown() {
|
|
1302
|
+
this.stopProactiveTicker();
|
|
1303
|
+
// 使用统计落盘 (ZCode 使用统计对齐): data/usage-stats.json (含 cost 金额维度 + 预算上限快照)
|
|
1304
|
+
try {
|
|
1305
|
+
const usagePayload = { updated: new Date().toISOString(), ...this.usageStats };
|
|
1306
|
+
const cap = Number(this.config?.budget?.usd);
|
|
1307
|
+
if (Number.isFinite(cap) && cap > 0) usagePayload.budget_usd = cap;
|
|
1308
|
+
fs.writeFileSync(path.join(this.dataDir, "usage-stats.json"),
|
|
1309
|
+
JSON.stringify(usagePayload, null, 2));
|
|
1310
|
+
} catch { /* 落盘失败不阻塞退出 */ }
|
|
1311
|
+
this._mcp?.close?.();
|
|
1312
|
+
// 释放内嵌数据库句柄 (SQLite 后端必需: 不关会导致文件被占用, 无法迁移/清理)
|
|
1313
|
+
try { this.facts?.close?.(); } catch { /* JSON 后端无 close, 静默跳过 */ }
|
|
1314
|
+
// 2026-10-03: WAL 模式落盘兜底 —— 未达阈值的增量在退出前 compact 进快照 (防进程退出丢增量)
|
|
1315
|
+
try { this.facts?.flush?.(); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
1316
|
+
// v1.0.8: 清理军团子进程 (spawn_agent 派生的 worker), 防后台残留
|
|
1317
|
+
if (this._legion && typeof this._legion.shutdownAll === "function") {
|
|
1318
|
+
try { this._legion.shutdownAll(); } catch (e) { debug(`[agent/index] 已忽略异常: ${e && e.message ? e.message : e}`); }
|
|
1319
|
+
}
|
|
1320
|
+
this.memory._saveState?.();
|
|
1321
|
+
this.scheduler?.shutdown?.(); // ⑤/②: 清定时器防进程挂起
|
|
1322
|
+
this.healer.markClean();
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// 重构 (2026-09-15): 历史/上下文管理 + 提示词构建以 mixin 方式挂回 prototype
|
|
1327
|
+
// (行为与拆前完全一致, 实例方法与调用方不受影响; 测试走 agent._xxx 不感知拆分)
|
|
1328
|
+
Object.assign(PPXAgent.prototype, contextMethods, promptMethods);
|
|
1329
|
+
|
|
1330
|
+
ensureUTF8Console();
|
|
1331
|
+
if (process.argv[1] && process.argv[1].endsWith("src/agent/index.js")) {
|
|
1332
|
+
const agent = new PPXAgent();
|
|
1333
|
+
console.log(`皮皮虾 就绪 | 记忆:${agent.facts.count()}条 | 工具:${agent.tools.list().join(",")} | 自愈:${agent.health.fixes.length ? "修复" + agent.health.fixes.length + "项" : "OK"}`);
|
|
1334
|
+
process.stdin.on("data", async (d) => {
|
|
1335
|
+
const line = d.toString().trim();
|
|
1336
|
+
if (["quit", "exit"].includes(line)) { agent.shutdown(); process.exit(0); }
|
|
1337
|
+
const r = await agent.chat(line);
|
|
1338
|
+
console.log("\n" + r + "\n");
|
|
1339
|
+
});
|
|
1340
|
+
}
|