agentworkshop 0.7.33 → 0.7.34
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/.output/nitro.json +1 -1
- package/.output/public/_nuxt/builds/latest.json +1 -1
- package/.output/public/_nuxt/builds/meta/adc365b6-f636-4a2b-9527-79819c1e874b.json +1 -0
- package/.output/server/chunks/_/nitro.mjs +699 -670
- package/.output/server/package.json +1 -1
- package/package.json +1 -1
- package/scripts/_dbg-harness-cancel-repro.mjs +81 -0
- package/scripts/_dbg-opslog-agent-e2e.mjs +5 -2
- package/scripts/production-closed-loop-e2e.mjs +7 -4
- package/server/services/workshop/agents/base-agent.ts +9 -0
- package/server/services/workshop/agents/codex-agent.ts +746 -729
- package/server/services/workshop/plugins/host.mjs +4 -7
- package/server/services/workshop/runtime/agent-runtime.ts +32 -3
- package/.output/public/_nuxt/builds/meta/291ef3e6-fa6a-4dee-8f46-05da1aeab815.json +0 -1
|
@@ -1,729 +1,746 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CodexAgentImpl — OpenAI Codex CLI(app-server)harness 的 AgentInterface 实现。
|
|
3
|
-
*
|
|
4
|
-
* 进程模型:每 Agent 一个 `codex app-server` 子进程(stdio NDJSON JSON-RPC v2,
|
|
5
|
-
* lazy spawn 跨消息复用);thread/start 建会话,turn/start 驱动回合。
|
|
6
|
-
*
|
|
7
|
-
* - prompt/工具面与 omp 全引擎同源(prompt-builder / host-tool-bridge);
|
|
8
|
-
* 工具经 stdio MCP 桥注入(config.toml 由 impl 装配 [mcp_servers.aw])
|
|
9
|
-
* - 事件映射:item/agentMessage/delta → delta;item/started → 🔧 status;
|
|
10
|
-
* turn/completed → artifact + done / error(codexErrorInfo 结构化)
|
|
11
|
-
* - HITL:item/commandExecution|fileChange/requestApproval(server→client 请求)
|
|
12
|
-
* → hitl-registry 登记 → respondHitl 应答(decision: accept/decline/cancel)
|
|
13
|
-
* - steer:turn 运行中 → turn/steer;无 active turn → 'deferred'
|
|
14
|
-
* - 上下文治理:thread/tokenUsage/updated 被动跟踪;越阈值 → thread/compact/start
|
|
15
|
-
*
|
|
16
|
-
* 协议权威:github.com/openai/codex codex-rs/app-server/README.md(v0.149 实测)。
|
|
17
|
-
*/
|
|
18
|
-
import { createLogger } from '../logger'
|
|
19
|
-
import { randomUUID } from 'node:crypto'
|
|
20
|
-
import { join } from 'node:path'
|
|
21
|
-
import { mkdirSync, writeFileSync, existsSync, readFileSync, copyFileSync } from 'node:fs'
|
|
22
|
-
import { tmpdir } from 'node:os'
|
|
23
|
-
import type { AgentEvent, AgentInterface, AgentInfo, AgentRunContext, AgentRunRequest } from './agent-interface'
|
|
24
|
-
import type { AgentContextStats } from '../types/task'
|
|
25
|
-
import { registerHarnessProcess, bindHarnessProcess, markHarnessProcessExit, killHarnessProcess } from './harness-process'
|
|
26
|
-
import { peerPrompt, systemManual, toolArgsPreview, workerPrompt } from './prompt-builder'
|
|
27
|
-
import { getHitlRegistry } from './hitl-registry'
|
|
28
|
-
import { harnessSettings } from '../settings'
|
|
29
|
-
import { StdioJsonRpcClient } from './adapters/stdio-jsonrpc'
|
|
30
|
-
import { BaseAgentImpl } from './base-agent'
|
|
31
|
-
import { generateMcpBridgeEnv } from './harness-env'
|
|
32
|
-
|
|
33
|
-
const log = createLogger('workshop.codex')
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
private
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
private
|
|
91
|
-
|
|
92
|
-
private
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
this.
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
this.client
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (!
|
|
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
|
-
const client = this.client
|
|
197
|
-
if (!client
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.
|
|
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
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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
|
-
const
|
|
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
|
-
const
|
|
539
|
-
const
|
|
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
|
-
this.pendingApprovals.
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
}
|
|
588
|
-
if (
|
|
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
|
-
this.
|
|
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
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
1
|
+
/**
|
|
2
|
+
* CodexAgentImpl — OpenAI Codex CLI(app-server)harness 的 AgentInterface 实现。
|
|
3
|
+
*
|
|
4
|
+
* 进程模型:每 Agent 一个 `codex app-server` 子进程(stdio NDJSON JSON-RPC v2,
|
|
5
|
+
* lazy spawn 跨消息复用);thread/start 建会话,turn/start 驱动回合。
|
|
6
|
+
*
|
|
7
|
+
* - prompt/工具面与 omp 全引擎同源(prompt-builder / host-tool-bridge);
|
|
8
|
+
* 工具经 stdio MCP 桥注入(config.toml 由 impl 装配 [mcp_servers.aw])
|
|
9
|
+
* - 事件映射:item/agentMessage/delta → delta;item/started → 🔧 status;
|
|
10
|
+
* turn/completed → artifact + done / error(codexErrorInfo 结构化)
|
|
11
|
+
* - HITL:item/commandExecution|fileChange/requestApproval(server→client 请求)
|
|
12
|
+
* → hitl-registry 登记 → respondHitl 应答(decision: accept/decline/cancel)
|
|
13
|
+
* - steer:turn 运行中 → turn/steer;无 active turn → 'deferred'
|
|
14
|
+
* - 上下文治理:thread/tokenUsage/updated 被动跟踪;越阈值 → thread/compact/start
|
|
15
|
+
*
|
|
16
|
+
* 协议权威:github.com/openai/codex codex-rs/app-server/README.md(v0.149 实测)。
|
|
17
|
+
*/
|
|
18
|
+
import { createLogger } from '../logger'
|
|
19
|
+
import { randomUUID } from 'node:crypto'
|
|
20
|
+
import { join } from 'node:path'
|
|
21
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, copyFileSync } from 'node:fs'
|
|
22
|
+
import { tmpdir } from 'node:os'
|
|
23
|
+
import type { AgentEvent, AgentInterface, AgentInfo, AgentRunContext, AgentRunRequest } from './agent-interface'
|
|
24
|
+
import type { AgentContextStats } from '../types/task'
|
|
25
|
+
import { registerHarnessProcess, bindHarnessProcess, markHarnessProcessExit, killHarnessProcess } from './harness-process'
|
|
26
|
+
import { peerPrompt, systemManual, toolArgsPreview, workerPrompt } from './prompt-builder'
|
|
27
|
+
import { getHitlRegistry } from './hitl-registry'
|
|
28
|
+
import { harnessSettings } from '../settings'
|
|
29
|
+
import { StdioJsonRpcClient } from './adapters/stdio-jsonrpc'
|
|
30
|
+
import { BaseAgentImpl } from './base-agent'
|
|
31
|
+
import { generateMcpBridgeEnv } from './harness-env'
|
|
32
|
+
|
|
33
|
+
const log = createLogger('workshop.codex')
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 默认回合停滞上限:整轮无任何事件(含推理模型的静默思考期)达到该时长才中止。
|
|
37
|
+
* 600s 会误杀健康的沉默推理(实测推理档静默 >10 分钟后被打断,任务 FAILED→重试→CANCELED);
|
|
38
|
+
* 事件到达即刷新计时,真实卡死仍会在该窗口内被发现。
|
|
39
|
+
*/
|
|
40
|
+
const CODEX_TURN_STALL_MS = 1_800_000
|
|
41
|
+
|
|
42
|
+
export interface CodexAgentConfig {
|
|
43
|
+
/** codex 可执行文件(默认取 harness.codex_command 设置) */
|
|
44
|
+
command?: string
|
|
45
|
+
/** 额外 CLI 参数(app-server 之前) */
|
|
46
|
+
args?: string[]
|
|
47
|
+
cwd?: string
|
|
48
|
+
/** 模型(如 gpt-5.2) */
|
|
49
|
+
model?: string
|
|
50
|
+
/** 审批策略(默认 on-request:沙箱内自由,越界请求审批 → HITL) */
|
|
51
|
+
approvalPolicy?: 'untrusted' | 'on-request' | 'never'
|
|
52
|
+
/** 沙箱(默认 workspace-write) */
|
|
53
|
+
sandbox?: 'read-only' | 'workspace-write' | 'danger-full-access'
|
|
54
|
+
/** CODEX_HOME(缺省进程环境;配置隔离/多账户时按 agent 指定;effort 未指定 home 时自动种子化) */
|
|
55
|
+
codexHome?: string
|
|
56
|
+
/** 思考 effort(model_reasoning_effort;需 CODEX_HOME 配置面,未指定 home 时自动种子化) */
|
|
57
|
+
effort?: string
|
|
58
|
+
/** 上下文窗口(usage 百分比计算) */
|
|
59
|
+
contextWindow?: number
|
|
60
|
+
/** 压缩阈值(0-1,默认 0.7) */
|
|
61
|
+
compactThreshold?: number
|
|
62
|
+
/**
|
|
63
|
+
* 回合停滞上限(ms):整轮无任何事件(推理模型静默思考也不产 delta)达到该时长才中止。
|
|
64
|
+
* 默认 1800000(30 分钟):codex 0.154 的推理档在负载下可静默思考 >10 分钟,
|
|
65
|
+
* 600s 会把健康的沉默推理误杀为 CODEX_TURN_STALLED(任务 FAILED→重试→CANCELED)。
|
|
66
|
+
* 有事件(delta/status/工具轨迹)即刷新计时,不放大真实卡死的发现延迟。
|
|
67
|
+
*/
|
|
68
|
+
promptTimeoutMs?: number
|
|
69
|
+
superviseTimeoutMs?: number
|
|
70
|
+
systemPromptPrefix?: string
|
|
71
|
+
scenarioPrompt?: string
|
|
72
|
+
agentId?: string
|
|
73
|
+
name?: string
|
|
74
|
+
role?: 'lead' | 'worker'
|
|
75
|
+
channelId?: string
|
|
76
|
+
token?: string
|
|
77
|
+
baseUrl?: string
|
|
78
|
+
mcpBridgePath?: string
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export class CodexAgentImpl extends BaseAgentImpl implements AgentInterface {
|
|
82
|
+
private readonly config: CodexAgentConfig
|
|
83
|
+
private agentInfo: AgentInfo | null = null
|
|
84
|
+
private readonly bridgeCtx = {
|
|
85
|
+
identity: { agentId: '', channelId: '', role: 'worker' as 'lead' | 'worker', name: 'agent' },
|
|
86
|
+
state: this.toolState,
|
|
87
|
+
getWorkspace: () => this.workspace,
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private agentRole: 'lead' | 'worker' = 'worker'
|
|
91
|
+
|
|
92
|
+
private client: StdioJsonRpcClient | null = null
|
|
93
|
+
private clientStarting: Promise<void> | null = null
|
|
94
|
+
private threadId: string | null = null
|
|
95
|
+
private turnId: string | null = null
|
|
96
|
+
|
|
97
|
+
// 回合状态
|
|
98
|
+
private turnActive = false
|
|
99
|
+
private deltaBuf = ''
|
|
100
|
+
private deltaTimer: ReturnType<typeof setTimeout> | null = null
|
|
101
|
+
private lastUsage: { input: number, at: number } | null = null
|
|
102
|
+
private compacting = false
|
|
103
|
+
private lastCompactAt = 0
|
|
104
|
+
/** 待应答审批(approval request id → JSON-RPC id) */
|
|
105
|
+
private pendingApprovals = new Map<string, { rpcId: string | number, timer: ReturnType<typeof setTimeout> | null }>()
|
|
106
|
+
|
|
107
|
+
constructor(config: Record<string, unknown> = {}) {
|
|
108
|
+
super({
|
|
109
|
+
agentId: typeof config.agentId === 'string' ? config.agentId : '',
|
|
110
|
+
name: typeof config.name === 'string' ? config.name : undefined,
|
|
111
|
+
role: config.role === 'lead' ? 'lead' : 'worker',
|
|
112
|
+
channelId: typeof config.channelId === 'string' ? config.channelId : '',
|
|
113
|
+
})
|
|
114
|
+
this.config = config as CodexAgentConfig
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
protected get harnessId(): string {
|
|
118
|
+
return 'codex'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected configRecord(): Record<string, unknown> {
|
|
122
|
+
return this.config
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async dispose(): Promise<void> {
|
|
126
|
+
const client = this.client
|
|
127
|
+
this.client = null
|
|
128
|
+
this.threadId = null
|
|
129
|
+
if (client) {
|
|
130
|
+
const pid = client.pid
|
|
131
|
+
await client.dispose().catch(() => {})
|
|
132
|
+
if (pid) markHarnessProcessExit(pid, null)
|
|
133
|
+
}
|
|
134
|
+
for (const [id, p] of this.pendingApprovals) {
|
|
135
|
+
if (p.timer) clearTimeout(p.timer)
|
|
136
|
+
this.pendingApprovals.delete(id)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getProcessInfo(): { pid: number, alive: boolean, command: string } | null {
|
|
141
|
+
const client = this.client
|
|
142
|
+
const pid = client?.pid
|
|
143
|
+
if (!pid || !client) return null
|
|
144
|
+
return { pid, alive: client.alive, command: 'codex app-server' }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
killProcess(): void {
|
|
148
|
+
const pid = this.client?.pid
|
|
149
|
+
if (pid) killHarnessProcess(pid)
|
|
150
|
+
else this.client?.kill()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
reconcileProcess(): void {
|
|
154
|
+
this.client?.reconcile()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
getContextStats(): AgentContextStats | null {
|
|
158
|
+
if (!this.lastUsage) return null
|
|
159
|
+
const window = this.config.contextWindow ?? null
|
|
160
|
+
return {
|
|
161
|
+
usedTokens: this.lastUsage.input,
|
|
162
|
+
contextWindow: window,
|
|
163
|
+
percent: window && window > 0 ? Math.min(1, this.lastUsage.input / window) : null,
|
|
164
|
+
compacting: this.compacting,
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** 实时注入:turn 运行中 → turn/steer;否则 'deferred'(消息保持 pending) */
|
|
169
|
+
async steer(text: string): Promise<'steer' | 'deferred'> {
|
|
170
|
+
const client = this.client
|
|
171
|
+
if (!client?.alive || !this.threadId || !this.turnActive) return 'deferred'
|
|
172
|
+
try {
|
|
173
|
+
await client.request('turn/steer', {
|
|
174
|
+
threadId: this.threadId,
|
|
175
|
+
input: [{ type: 'text', text }],
|
|
176
|
+
}, 15_000)
|
|
177
|
+
return 'steer'
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// 无 active turn / 引擎拒绝 → 消息保持 pending 由消费循环处理
|
|
181
|
+
return 'deferred'
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** HITL 应答:审批请求 → JSON-RPC 应答(decision) */
|
|
186
|
+
async respondHitl(kind: string, id: string, outcome: {
|
|
187
|
+
confirmed?: boolean
|
|
188
|
+
cancelled?: boolean
|
|
189
|
+
value?: string
|
|
190
|
+
response?: string
|
|
191
|
+
comment?: string
|
|
192
|
+
}): Promise<void> {
|
|
193
|
+
if (kind !== 'codex-approval') return
|
|
194
|
+
const pending = this.pendingApprovals.get(id)
|
|
195
|
+
if (!pending) throw new Error(`待办不存在或已处理: ${id}`)
|
|
196
|
+
const client = this.client
|
|
197
|
+
if (!client) throw new Error('codex 会话已关闭')
|
|
198
|
+
const decision = outcome.cancelled === true
|
|
199
|
+
? 'cancel'
|
|
200
|
+
: outcome.confirmed === true ? 'accept' : 'decline'
|
|
201
|
+
client.respond(pending.rpcId, { decision })
|
|
202
|
+
if (pending.timer) clearTimeout(pending.timer)
|
|
203
|
+
this.pendingApprovals.delete(id)
|
|
204
|
+
getHitlRegistry().resolve(kind, id, decision === 'cancel' ? 'cancelled' : 'answered')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** 上下文治理(post-settle):越阈值 → thread/compact/start */
|
|
208
|
+
async onTurnSettled(): Promise<void> {
|
|
209
|
+
const client = this.client
|
|
210
|
+
if (!client?.alive || !this.threadId || this.compacting) return
|
|
211
|
+
const stats = this.getContextStats()
|
|
212
|
+
const threshold = this.config.compactThreshold ?? 0.7
|
|
213
|
+
if (!stats?.percent || stats.percent < threshold) return
|
|
214
|
+
if (Date.now() - this.lastCompactAt < 5 * 60_000) return
|
|
215
|
+
this.compacting = true
|
|
216
|
+
this.lastCompactAt = Date.now()
|
|
217
|
+
try {
|
|
218
|
+
await client.request('thread/compact/start', { threadId: this.threadId }, 30_000)
|
|
219
|
+
log.info(`[CodexAgent:${this.selfAgentId}] 上下文压缩已触发(percent=${(stats.percent * 100).toFixed(0)}%)`)
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
log.warn(`[CodexAgent:${this.selfAgentId}] compact 发起失败(放行):`, err instanceof Error ? err.message : err)
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
this.compacting = false
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
protected async* workerTurn(request: AgentRunRequest, ctx: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown> {
|
|
230
|
+
const taskId = request.taskId ?? (request.message.metadata?.['x-aw-task-id'] as string | undefined)
|
|
231
|
+
if (!taskId) return
|
|
232
|
+
this.toolState.currentTaskId = taskId
|
|
233
|
+
try {
|
|
234
|
+
await this.ensureClient(ctx)
|
|
235
|
+
const taskText = request.message.parts.map((p) => {
|
|
236
|
+
if ('text' in p) return p.text
|
|
237
|
+
if ('data' in p) return JSON.stringify(p.data)
|
|
238
|
+
if ('url' in p) return p.url
|
|
239
|
+
if ('raw' in p) return p.raw
|
|
240
|
+
return ''
|
|
241
|
+
}).join('\n')
|
|
242
|
+
const prompt = workerPrompt({
|
|
243
|
+
agentName: this.agentName,
|
|
244
|
+
channelId: this.channelId,
|
|
245
|
+
taskId,
|
|
246
|
+
taskText,
|
|
247
|
+
memory: request.memory,
|
|
248
|
+
ctxPrefix: await this.contextPrefix(),
|
|
249
|
+
manual: systemManual(),
|
|
250
|
+
})
|
|
251
|
+
yield* this.streamTurn(prompt, taskId, this.config.promptTimeoutMs ?? CODEX_TURN_STALL_MS, ctx.signal)
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
this.toolState.currentTaskId = null
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
protected async* peerTurn(request: AgentRunRequest, ctx: AgentRunContext): AsyncGenerator<AgentEvent, void, unknown> {
|
|
259
|
+
await this.ensureClient(ctx)
|
|
260
|
+
const msg = request.message
|
|
261
|
+
const fromId = request.fromAgentId
|
|
262
|
+
?? (typeof msg.metadata?.['x-aw-from-label'] === 'string' ? msg.metadata['x-aw-from-label'] : undefined)
|
|
263
|
+
?? 'unknown'
|
|
264
|
+
const requireReply = msg.metadata?.['x-aw-require-reply'] === 'true'
|
|
265
|
+
const crossChannel = msg.metadata?.['x-aw-cross-channel'] === 'true'
|
|
266
|
+
this.toolState.replyContext = requireReply && request.fromAgentId && !crossChannel
|
|
267
|
+
? { fromId: request.fromAgentId, messageId: msg.messageId }
|
|
268
|
+
: null
|
|
269
|
+
const msgText = msg.parts.map((p) => {
|
|
270
|
+
if ('text' in p) return p.text
|
|
271
|
+
if ('data' in p) return JSON.stringify(p.data)
|
|
272
|
+
if ('url' in p) return p.url
|
|
273
|
+
if ('raw' in p) return p.raw
|
|
274
|
+
return ''
|
|
275
|
+
}).join('\n')
|
|
276
|
+
const prompt = peerPrompt({
|
|
277
|
+
agentName: this.agentName,
|
|
278
|
+
role: this.agentRole,
|
|
279
|
+
channelId: this.channelId,
|
|
280
|
+
ctxPrefix: await this.contextPrefix(),
|
|
281
|
+
manual: systemManual(),
|
|
282
|
+
memory: request.memory,
|
|
283
|
+
fromId,
|
|
284
|
+
messageId: msg.messageId,
|
|
285
|
+
requireReply,
|
|
286
|
+
isReply: typeof msg.metadata?.['x-aw-in-reply-to'] === 'string',
|
|
287
|
+
crossChannel,
|
|
288
|
+
fromChannel: typeof msg.metadata?.['x-aw-from-channel'] === 'string' ? String(msg.metadata['x-aw-from-channel']) : '',
|
|
289
|
+
msgText,
|
|
290
|
+
})
|
|
291
|
+
yield* this.streamTurn(prompt, undefined, this.config.promptTimeoutMs ?? CODEX_TURN_STALL_MS, ctx.signal)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// ===== 回合执行 =====
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* 流式回合:turn/start → 逐事件产出(delta/status)→ turn/completed 收口。
|
|
298
|
+
* artifact 在 done 前注入(item/completed agentMessage 聚合文本)。
|
|
299
|
+
*/
|
|
300
|
+
private async* streamTurn(prompt: string, taskId: string | undefined, timeoutMs: number, signal?: AbortSignal): AsyncGenerator<AgentEvent, void, unknown> {
|
|
301
|
+
const client = this.client
|
|
302
|
+
if (!client || !this.threadId) {
|
|
303
|
+
yield { kind: 'error', error: { code: 'CODEX_NOT_READY', message: 'codex 会话未就绪' } }
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
const queue: AgentEvent[] = []
|
|
307
|
+
let isDone = false
|
|
308
|
+
let resolveWait: (() => void) | null = null
|
|
309
|
+
let lastActivity = Date.now()
|
|
310
|
+
let agentText = ''
|
|
311
|
+
this.turnActive = true
|
|
312
|
+
this.turnId = null
|
|
313
|
+
|
|
314
|
+
const enqueue = (e: AgentEvent): void => {
|
|
315
|
+
if (e.kind === 'done' && !isDone) {
|
|
316
|
+
if (agentText.trim()) {
|
|
317
|
+
queue.push({
|
|
318
|
+
kind: 'artifact',
|
|
319
|
+
artifact: { artifactId: randomUUID(), name: 'output', parts: [{ text: agentText.trim() }] },
|
|
320
|
+
lastChunk: true,
|
|
321
|
+
totalChunks: 1,
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
this.turnActive = false
|
|
325
|
+
}
|
|
326
|
+
if (e.kind === 'done' || e.kind === 'error') {
|
|
327
|
+
isDone = true
|
|
328
|
+
this.turnActive = false
|
|
329
|
+
flushDelta()
|
|
330
|
+
}
|
|
331
|
+
queue.push(e)
|
|
332
|
+
lastActivity = Date.now()
|
|
333
|
+
resolveWait?.()
|
|
334
|
+
resolveWait = null
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const flushDelta = (): void => {
|
|
338
|
+
if (this.deltaTimer) {
|
|
339
|
+
clearTimeout(this.deltaTimer)
|
|
340
|
+
this.deltaTimer = null
|
|
341
|
+
}
|
|
342
|
+
if (!this.deltaBuf) return
|
|
343
|
+
const text = this.deltaBuf
|
|
344
|
+
this.deltaBuf = ''
|
|
345
|
+
queue.push({ kind: 'delta', delta: { text } })
|
|
346
|
+
}
|
|
347
|
+
const pushDelta = (text: string): void => {
|
|
348
|
+
this.deltaBuf += text
|
|
349
|
+
this.deltaTimer ??= setTimeout(() => {
|
|
350
|
+
this.deltaTimer = null
|
|
351
|
+
flushDelta()
|
|
352
|
+
}, 50)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// 通知订阅(审批请求处理器已在 startClient 挂接,此处只处理回合通知)
|
|
356
|
+
const unsub = client.onNotification((method, params) => {
|
|
357
|
+
const p = (params ?? {}) as Record<string, unknown>
|
|
358
|
+
lastActivity = Date.now()
|
|
359
|
+
if (method === 'item/agentMessage/delta') {
|
|
360
|
+
const delta = typeof p.delta === 'string' ? p.delta : ''
|
|
361
|
+
if (delta) {
|
|
362
|
+
this.turnActive = true
|
|
363
|
+
pushDelta(delta)
|
|
364
|
+
}
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
if (method === 'turn/started') {
|
|
368
|
+
const turn = p.turn as Record<string, unknown> | undefined
|
|
369
|
+
this.turnId = typeof turn?.id === 'string' ? turn.id : this.turnId
|
|
370
|
+
this.turnActive = true
|
|
371
|
+
enqueue({ kind: 'status', status: { state: 'WORKING', timestamp: new Date().toISOString() } })
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
if (method === 'item/started') {
|
|
375
|
+
const item = p.item as Record<string, unknown> | undefined
|
|
376
|
+
const t = String(item?.type ?? '')
|
|
377
|
+
if (['commandExecution', 'mcpToolCall', 'fileChange', 'dynamicToolCall', 'webSearch', 'collabToolCall'].includes(t)) {
|
|
378
|
+
const label = t === 'commandExecution'
|
|
379
|
+
? String(item?.command ?? 'command')
|
|
380
|
+
: String(item?.tool ?? item?.title ?? t)
|
|
381
|
+
enqueue({
|
|
382
|
+
kind: 'status',
|
|
383
|
+
status: {
|
|
384
|
+
state: 'WORKING',
|
|
385
|
+
message: {
|
|
386
|
+
messageId: randomUUID(),
|
|
387
|
+
contextId: this.channelId,
|
|
388
|
+
role: 'ROLE_AGENT',
|
|
389
|
+
parts: [{ text: `🔧 ${label}${toolArgsPreview(item?.input ?? item?.arguments ?? item?.command)}` }],
|
|
390
|
+
},
|
|
391
|
+
timestamp: new Date().toISOString(),
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
}
|
|
395
|
+
if (t === 'contextCompaction') this.compacting = true
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
if (method === 'item/completed') {
|
|
399
|
+
const item = p.item as Record<string, unknown> | undefined
|
|
400
|
+
if (String(item?.type ?? '') === 'agentMessage' && typeof item?.text === 'string') {
|
|
401
|
+
agentText += item.text
|
|
402
|
+
}
|
|
403
|
+
if (String(item?.type ?? '') === 'contextCompaction') this.compacting = false
|
|
404
|
+
return
|
|
405
|
+
}
|
|
406
|
+
if (method === 'thread/tokenUsage/updated') {
|
|
407
|
+
const usage = (p.usage ?? p.tokenUsage ?? p) as Record<string, unknown>
|
|
408
|
+
const input = Number(usage.input_tokens ?? usage.inputTokens ?? usage.total_tokens ?? usage.totalTokens)
|
|
409
|
+
if (Number.isFinite(input) && input > 0) this.lastUsage = { input, at: Date.now() }
|
|
410
|
+
return
|
|
411
|
+
}
|
|
412
|
+
if (method === 'turn/completed') {
|
|
413
|
+
const turn = (p.turn ?? {}) as Record<string, unknown>
|
|
414
|
+
const status = String(turn.status ?? 'completed')
|
|
415
|
+
if (status === 'failed') {
|
|
416
|
+
const err = (turn.error ?? {}) as Record<string, unknown>
|
|
417
|
+
const info = String(err.codexErrorInfo ?? '')
|
|
418
|
+
enqueue({
|
|
419
|
+
kind: 'error',
|
|
420
|
+
error: {
|
|
421
|
+
code: info ? `CODEX_${info}` : 'CODEX_LLM_ERROR',
|
|
422
|
+
message: String(err.message ?? 'codex 回合失败'),
|
|
423
|
+
},
|
|
424
|
+
})
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
// completed / interrupted:中断按 done 收口(消息按已处理落账)
|
|
428
|
+
enqueue({ kind: 'done', final: taskId ? { taskId } : undefined })
|
|
429
|
+
}
|
|
430
|
+
return
|
|
431
|
+
}
|
|
432
|
+
if (method === 'error') {
|
|
433
|
+
enqueue({
|
|
434
|
+
kind: 'error',
|
|
435
|
+
error: { code: 'CODEX_ERROR', message: String(p.message ?? p.error ?? JSON.stringify(params).slice(0, 300)) },
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
})
|
|
439
|
+
|
|
440
|
+
// abort 传导
|
|
441
|
+
const onAbort = (): void => {
|
|
442
|
+
log.warn(`[CodexAgent:${this.selfAgentId}] run 被 abort → turn/interrupt,taskId=${taskId ?? '-'}`)
|
|
443
|
+
void client.request('turn/interrupt', { threadId: this.threadId, ...(this.turnId ? { turnId: this.turnId } : {}) }, 15_000).catch(() => {})
|
|
444
|
+
setTimeout(() => {
|
|
445
|
+
if (!isDone) enqueue({ kind: 'done', final: taskId ? { taskId } : undefined })
|
|
446
|
+
}, 5000)
|
|
447
|
+
}
|
|
448
|
+
if (signal?.aborted) onAbort()
|
|
449
|
+
else signal?.addEventListener('abort', onAbort, { once: true })
|
|
450
|
+
|
|
451
|
+
// 投递 turn
|
|
452
|
+
try {
|
|
453
|
+
await client.request('turn/start', {
|
|
454
|
+
threadId: this.threadId,
|
|
455
|
+
input: [{ type: 'text', text: prompt }],
|
|
456
|
+
}, 60_000)
|
|
457
|
+
}
|
|
458
|
+
catch (err) {
|
|
459
|
+
unsub()
|
|
460
|
+
signal?.removeEventListener('abort', onAbort)
|
|
461
|
+
this.turnActive = false
|
|
462
|
+
yield {
|
|
463
|
+
kind: 'error',
|
|
464
|
+
error: {
|
|
465
|
+
code: 'CODEX_PROMPT_FAILED',
|
|
466
|
+
message: `codex turn/start 失败: ${err instanceof Error ? err.message : String(err)}`,
|
|
467
|
+
},
|
|
468
|
+
}
|
|
469
|
+
return
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
while (!isDone || queue.length > 0) {
|
|
474
|
+
if (queue.length === 0 && !isDone) {
|
|
475
|
+
const remaining = timeoutMs - (Date.now() - lastActivity)
|
|
476
|
+
if (remaining <= 0) {
|
|
477
|
+
void client.request('turn/interrupt', { threadId: this.threadId }, 15_000).catch(() => {})
|
|
478
|
+
enqueue({
|
|
479
|
+
kind: 'error',
|
|
480
|
+
error: { code: 'CODEX_TURN_STALLED', message: `回合停滞 ${Math.round(timeoutMs / 1000)}s 无事件,已中止` },
|
|
481
|
+
})
|
|
482
|
+
continue
|
|
483
|
+
}
|
|
484
|
+
await new Promise<void>((r) => {
|
|
485
|
+
resolveWait = r
|
|
486
|
+
setTimeout(() => {
|
|
487
|
+
resolveWait = null
|
|
488
|
+
r()
|
|
489
|
+
}, remaining + 100)
|
|
490
|
+
})
|
|
491
|
+
}
|
|
492
|
+
while (queue.length > 0) {
|
|
493
|
+
yield queue.shift()!
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
finally {
|
|
498
|
+
flushDelta()
|
|
499
|
+
unsub()
|
|
500
|
+
signal?.removeEventListener('abort', onAbort)
|
|
501
|
+
this.turnActive = false
|
|
502
|
+
this.turnId = null
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/** supervise 用:收齐整个回合的事件流(supervise 决策解析在调用方) */
|
|
507
|
+
protected async collectTurnEvents(prompt: string, timeoutMs: number, signal?: AbortSignal): Promise<AgentEvent[]> {
|
|
508
|
+
const events: AgentEvent[] = []
|
|
509
|
+
for await (const e of this.streamTurn(prompt, undefined, timeoutMs, signal)) {
|
|
510
|
+
events.push(e)
|
|
511
|
+
if (e.kind === 'error') break
|
|
512
|
+
}
|
|
513
|
+
return events
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// ===== HITL 登记 =====
|
|
517
|
+
|
|
518
|
+
private registerApprovalHitl(rpcId: string | number, itemId: string, kindLabel: 'command' | 'file', p: Record<string, unknown>): void {
|
|
519
|
+
const id = `codex-${itemId}`
|
|
520
|
+
if (this.pendingApprovals.has(id)) return
|
|
521
|
+
const title = kindLabel === 'command'
|
|
522
|
+
? `codex 命令审批:${String(p.command ?? '(未知命令)').slice(0, 200)}`
|
|
523
|
+
: `codex 文件变更审批:${String(p.cwd ?? p.grantRoot ?? '')}`
|
|
524
|
+
const registry = getHitlRegistry()
|
|
525
|
+
registry.register({
|
|
526
|
+
kind: 'codex-approval',
|
|
527
|
+
id,
|
|
528
|
+
agentId: this.selfAgentId,
|
|
529
|
+
agentName: this.agentName,
|
|
530
|
+
channelId: this.channelId,
|
|
531
|
+
pid: this.client?.pid,
|
|
532
|
+
method: 'confirm',
|
|
533
|
+
title,
|
|
534
|
+
detail: typeof p.reason === 'string' ? p.reason : undefined,
|
|
535
|
+
createdAt: new Date().toISOString(),
|
|
536
|
+
expiresAt: null,
|
|
537
|
+
})
|
|
538
|
+
const timeoutMs = harnessSettings().hitl_timeout_ms
|
|
539
|
+
const timer = timeoutMs > 0
|
|
540
|
+
? setTimeout(() => {
|
|
541
|
+
void this.respondHitl('codex-approval', id, { cancelled: true }).catch(() => {})
|
|
542
|
+
registry.resolve('codex-approval', id, 'expired')
|
|
543
|
+
}, timeoutMs)
|
|
544
|
+
: null
|
|
545
|
+
this.pendingApprovals.set(id, { rpcId, timer })
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
private registerUserInputHitl(rpcId: string | number, p: Record<string, unknown>): void {
|
|
549
|
+
const id = `codex-input-${randomUUID().slice(0, 8)}`
|
|
550
|
+
const questions = Array.isArray(p.questions) ? p.questions : []
|
|
551
|
+
const first = (questions[0] ?? {}) as Record<string, unknown>
|
|
552
|
+
const registry = getHitlRegistry()
|
|
553
|
+
registry.register({
|
|
554
|
+
kind: 'codex-approval',
|
|
555
|
+
id,
|
|
556
|
+
agentId: this.selfAgentId,
|
|
557
|
+
agentName: this.agentName,
|
|
558
|
+
channelId: this.channelId,
|
|
559
|
+
pid: this.client?.pid,
|
|
560
|
+
method: 'input',
|
|
561
|
+
title: String(first.question ?? first.header ?? 'codex 提问'),
|
|
562
|
+
detail: typeof p.reason === 'string' ? p.reason : undefined,
|
|
563
|
+
createdAt: new Date().toISOString(),
|
|
564
|
+
expiresAt: null,
|
|
565
|
+
})
|
|
566
|
+
const timeoutMs = harnessSettings().hitl_timeout_ms
|
|
567
|
+
const timer = timeoutMs > 0
|
|
568
|
+
? setTimeout(() => {
|
|
569
|
+
void this.respondUserInput(rpcId, id, { cancelled: true }).catch(() => {})
|
|
570
|
+
}, timeoutMs)
|
|
571
|
+
: null
|
|
572
|
+
this.pendingApprovals.set(id, { rpcId, timer })
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private async respondUserInput(rpcId: string | number, id: string, outcome: { cancelled?: boolean, value?: string }): Promise<void> {
|
|
576
|
+
const pending = this.pendingApprovals.get(id)
|
|
577
|
+
if (!pending) return
|
|
578
|
+
const client = this.client
|
|
579
|
+
if (!client) return
|
|
580
|
+
if (outcome.cancelled) {
|
|
581
|
+
client.respondError(rpcId, -32800, '人工取消')
|
|
582
|
+
getHitlRegistry().resolve('codex-approval', id, 'cancelled')
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
client.respond(rpcId, { answers: [{ answer: outcome.value ?? '' }] })
|
|
586
|
+
getHitlRegistry().resolve('codex-approval', id, 'answered')
|
|
587
|
+
}
|
|
588
|
+
if (pending.timer) clearTimeout(pending.timer)
|
|
589
|
+
this.pendingApprovals.delete(id)
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// ===== 客户端管理 =====
|
|
593
|
+
|
|
594
|
+
private async ensureClient(ctx: AgentRunContext): Promise<void> {
|
|
595
|
+
if (!this.workspace) this.workspace = ctx.workspace
|
|
596
|
+
if (!this.agentInfo) {
|
|
597
|
+
this.channelId = ctx.channelId
|
|
598
|
+
this.agentRole = ctx.role
|
|
599
|
+
this.refreshIdentity()
|
|
600
|
+
}
|
|
601
|
+
if (this.client?.alive && this.threadId) return
|
|
602
|
+
if (this.client && !this.client.alive) {
|
|
603
|
+
this.client = null
|
|
604
|
+
this.threadId = null
|
|
605
|
+
}
|
|
606
|
+
this.clientStarting ??= this.startClient().finally(() => {
|
|
607
|
+
this.clientStarting = null
|
|
608
|
+
})
|
|
609
|
+
await this.clientStarting
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
private async startClient(): Promise<void> {
|
|
613
|
+
const command = this.config.command ?? harnessSettings().codex_command
|
|
614
|
+
const client = new StdioJsonRpcClient({
|
|
615
|
+
name: 'codex',
|
|
616
|
+
command,
|
|
617
|
+
args: [...(this.config.args ?? []), 'app-server'],
|
|
618
|
+
cwd: this.config.cwd ?? process.cwd(),
|
|
619
|
+
env: generateMcpBridgeEnv({
|
|
620
|
+
agentId: this.selfAgentId,
|
|
621
|
+
token: this.config.token,
|
|
622
|
+
baseUrl: this.config.baseUrl,
|
|
623
|
+
bridgePath: this.config.mcpBridgePath,
|
|
624
|
+
extra: this.config.codexHome ? { CODEX_HOME: this.config.codexHome } : {},
|
|
625
|
+
}).engineEnv,
|
|
626
|
+
requestTimeoutMs: 60_000,
|
|
627
|
+
})
|
|
628
|
+
const pidRef = { pid: undefined as number | undefined }
|
|
629
|
+
client.onExit((code) => {
|
|
630
|
+
if (pidRef.pid) markHarnessProcessExit(pidRef.pid, code)
|
|
631
|
+
this.threadId = null
|
|
632
|
+
log.warn(`[CodexAgent:${this.selfAgentId}] codex app-server 退出(code=${code});下回合自动重生`)
|
|
633
|
+
})
|
|
634
|
+
await client.start()
|
|
635
|
+
pidRef.pid = client.pid
|
|
636
|
+
if (client.pid) {
|
|
637
|
+
registerHarnessProcess(client.pid, { harness: 'codex', command, args: ['app-server'] })
|
|
638
|
+
bindHarnessProcess(client.pid, { agentId: this.selfAgentId, channelId: this.channelId, name: this.agentName, role: this.agentRole })
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 握手
|
|
642
|
+
await client.request('initialize', {
|
|
643
|
+
clientInfo: { name: 'agentworkshop', title: 'AgentWorkShop', version: '0.8.0' },
|
|
644
|
+
}, 30_000)
|
|
645
|
+
client.notify('initialized', {})
|
|
646
|
+
|
|
647
|
+
// MCP 桥装配(per-agent CODEX_HOME 的 config.toml [mcp_servers.aw])
|
|
648
|
+
this.writeCodexHomeConfig()
|
|
649
|
+
|
|
650
|
+
// 建会话
|
|
651
|
+
const result = await client.request('thread/start', {
|
|
652
|
+
...(this.config.model ? { model: this.config.model } : {}),
|
|
653
|
+
cwd: this.config.cwd ?? process.cwd(),
|
|
654
|
+
approvalPolicy: this.config.approvalPolicy ?? 'on-request',
|
|
655
|
+
sandbox: this.config.sandbox ?? 'workspace-write',
|
|
656
|
+
}, 60_000) as Record<string, unknown>
|
|
657
|
+
const thread = (result?.thread ?? result) as Record<string, unknown>
|
|
658
|
+
this.threadId = typeof thread?.id === 'string' ? thread.id : null
|
|
659
|
+
if (!this.threadId) throw new Error('codex thread/start 未返回 thread id')
|
|
660
|
+
|
|
661
|
+
// 事件监听挂接(通知在 streamTurn 内按回合订阅;此处仅挂请求处理器)
|
|
662
|
+
client.onRequest((req) => {
|
|
663
|
+
if (req.method === 'item/commandExecution/requestApproval' || req.method === 'item/fileChange/requestApproval') {
|
|
664
|
+
const p = (req.params ?? {}) as Record<string, unknown>
|
|
665
|
+
this.registerApprovalHitl(req.id, String(p.itemId ?? randomUUID()), req.method === 'item/fileChange/requestApproval' ? 'file' : 'command', p)
|
|
666
|
+
}
|
|
667
|
+
else if (req.method === 'tool/requestUserInput') {
|
|
668
|
+
const p = (req.params ?? {}) as Record<string, unknown>
|
|
669
|
+
this.registerUserInputHitl(req.id, p)
|
|
670
|
+
}
|
|
671
|
+
else {
|
|
672
|
+
client.respondError(req.id, -32601, `方法不存在: ${req.method}`)
|
|
673
|
+
}
|
|
674
|
+
})
|
|
675
|
+
|
|
676
|
+
this.client = client
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* per-agent CODEX_HOME config.toml:注册 MCP 桥(required=true,桥挂了宁可失败)。
|
|
681
|
+
* 已有 config.toml(从全局目录种子化复制)→ 追加本段(保留用户的 provider/model
|
|
682
|
+
* 配置,自定义网关/密钥不丢失);全新目录 → 独立写入。
|
|
683
|
+
*
|
|
684
|
+
* 无条件种子化(不再仅当 effort 配置时才挂桥):MCP 桥是 codex worker 的 host
|
|
685
|
+
* 工具面(complete_task/ops_log/…)唯一入口;不挂桥的实例模型只能拿自带 shell
|
|
686
|
+
* 乱翻工作区,永远调不到 complete_task → 任务停滞被看门狗回收
|
|
687
|
+
* (实测 codex 任务终态 CANCELED、交付物却只落在频道消息里的根因)。
|
|
688
|
+
*/
|
|
689
|
+
private writeCodexHomeConfig(): void {
|
|
690
|
+
let home = this.config.codexHome
|
|
691
|
+
if (!home) {
|
|
692
|
+
// 自动种子化 per-agent CODEX_HOME(拷贝全局凭据/网关配置,保登录与自定义
|
|
693
|
+
// provider;追加 MCP 段 + 顶部 effort)
|
|
694
|
+
home = join(tmpdir(), `aw-codex-${this.selfAgentId.slice(0, 8)}`)
|
|
695
|
+
this.config.codexHome = home
|
|
696
|
+
for (const f of ['auth.json', 'config.toml', 'cc-switch-model-catalog.json']) {
|
|
697
|
+
try {
|
|
698
|
+
const src = join(process.env.HOME ?? process.env.USERPROFILE ?? '', '.codex', f)
|
|
699
|
+
if (existsSync(src)) copyFileSync(src, join(home, f))
|
|
700
|
+
}
|
|
701
|
+
catch { /* 单文件缺失忽略 */ }
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
mkdirSync(home, { recursive: true })
|
|
706
|
+
const configFile = join(home, 'config.toml')
|
|
707
|
+
const env = generateMcpBridgeEnv({ agentId: this.selfAgentId, token: this.config.token, baseUrl: this.config.baseUrl, bridgePath: this.config.mcpBridgePath })
|
|
708
|
+
const envLines = Object.entries(env.bridgeEnv).map(([k, v]) => ` ${k} = "${v}"`).join('\n')
|
|
709
|
+
const section = [
|
|
710
|
+
``,
|
|
711
|
+
`[mcp_servers.aw]`,
|
|
712
|
+
`command = ${JSON.stringify(process.execPath)}`,
|
|
713
|
+
`args = [${JSON.stringify(join(process.env.AW_PACKAGE_ROOT ?? process.cwd(), 'server', 'harness', 'aw-mcp-bridge.mjs'))}]`,
|
|
714
|
+
`required = true`,
|
|
715
|
+
``,
|
|
716
|
+
`[mcp_servers.aw.env]`,
|
|
717
|
+
envLines,
|
|
718
|
+
``,
|
|
719
|
+
].join('\n')
|
|
720
|
+
// effort 走 TOML 顶层键(model_reasoning_effort):必须位于任何 [table] 之前 → 顶部插入
|
|
721
|
+
const effortLine = this.config.effort
|
|
722
|
+
? `model_reasoning_effort = ${JSON.stringify(this.config.effort)}
|
|
723
|
+
`
|
|
724
|
+
: ''
|
|
725
|
+
if (existsSync(configFile)) {
|
|
726
|
+
const existing = readFileSync(configFile, 'utf-8')
|
|
727
|
+
const withoutDup = this.config.effort
|
|
728
|
+
? existing.replace(/^model_reasoning_effort = .*\n?/m, '')
|
|
729
|
+
: existing
|
|
730
|
+
if (withoutDup.includes('[mcp_servers.aw]')) {
|
|
731
|
+
if (effortLine && !withoutDup.startsWith(effortLine.trim())) {
|
|
732
|
+
writeFileSync(configFile, effortLine + withoutDup, 'utf-8')
|
|
733
|
+
}
|
|
734
|
+
return
|
|
735
|
+
}
|
|
736
|
+
writeFileSync(configFile, effortLine + withoutDup + section, 'utf-8')
|
|
737
|
+
}
|
|
738
|
+
else {
|
|
739
|
+
writeFileSync(configFile, effortLine + section, 'utf-8')
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
catch (err) {
|
|
743
|
+
log.warn(`[CodexAgent:${this.selfAgentId}] CODEX_HOME config 写入失败(信任全局配置):`, err instanceof Error ? err.message : err)
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|