j-cli 12.9.9

A fast CLI tool for alias management, daily reports, and productivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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
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
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
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
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
use super::super::app::types::{PlanDecision, StreamMsg, ToolResultMsg};
use super::super::error::ChatError;
use super::super::hook::{HookContext, HookEvent, HookManager};
use super::super::storage::{ChatMessage, ToolCallItem};
use super::api::{build_request_with_tools, call_openai_non_stream_lenient, create_openai_client};
use super::compact;
use super::config::{AgentLoopConfig, AgentLoopSharedState};
use crate::command::chat::constants::{ROLE_ASSISTANT, ROLE_TOOL, ROLE_USER};
use crate::command::chat::tools::Tool;
use crate::command::chat::tools::compact::CompactTool;
use crate::util::log::{write_error_log, write_info_log};

/// 流式响应中逐步聚合的工具调用片段(按 chunk index 聚合 id/name/arguments)
struct StreamingToolCallPart {
    call_id: String,
    function_name: String,
    function_arguments: String,
}
use crate::util::safe_lock;
use async_openai::types::chat::ChatCompletionTools;
use futures::StreamExt;
use rand::Rng;
use std::sync::{Arc, Mutex, mpsc};

/// process_tool_calls 所需的通道和共享状态
struct ToolCallContext<'a> {
    stream_msg_sender: &'a mpsc::Sender<StreamMsg>,
    tool_result_receiver: &'a mpsc::Receiver<ToolResultMsg>,
    pending_user_messages: &'a Arc<Mutex<Vec<ChatMessage>>>,
    hook_manager: &'a HookManager,
    supports_vision: bool,
    ui_messages: &'a Arc<Mutex<Vec<ChatMessage>>>,
    streaming_content: &'a Arc<Mutex<String>>,
    #[allow(dead_code)]
    invoked_skills: &'a compact::InvokedSkillsMap,
    session_id: &'a str,
}

/// 后台 Agent 循环:支持多轮工具调用
pub async fn run_main_agent_loop(
    config: AgentLoopConfig,
    shared: AgentLoopSharedState,
    mut messages: Vec<ChatMessage>,
    tools: Vec<ChatCompletionTools>,
    system_prompt_fn: Arc<dyn Fn() -> Option<String> + Send + Sync>,
    tx: mpsc::Sender<StreamMsg>,
    tool_result_rx: mpsc::Receiver<ToolResultMsg>,
) {
    let AgentLoopConfig {
        provider,
        max_llm_rounds,
        compact_config,
        hook_manager,
        cancel_token,
    } = config;
    let AgentLoopSharedState {
        streaming_content,
        pending_user_messages,
        background_manager: _,
        todo_manager,
        ui_messages,
        estimated_context_tokens,
        invoked_skills,
        session_id,
    } = shared;

    let client = create_openai_client(&provider);

    let tool_ctx = ToolCallContext {
        stream_msg_sender: &tx,
        tool_result_receiver: &tool_result_rx,
        pending_user_messages: &pending_user_messages,
        hook_manager: &hook_manager,
        supports_vision: provider.supports_vision,
        ui_messages: &ui_messages,
        streaming_content: &streaming_content,
        invoked_skills: &invoked_skills,
        session_id: &session_id,
    };

    write_info_log(
        "agent_loop",
        &format!(
            "agent loop 启动: max_llm_rounds={}, model={}, tools_count={}",
            max_llm_rounds,
            provider.model,
            tools.len()
        ),
    );
    if !tools.is_empty() {
        let tool_names: Vec<&str> = tools
            .iter()
            .filter_map(|t| {
                if let async_openai::types::chat::ChatCompletionTools::Function(f) = t {
                    Some(f.function.name.as_str())
                } else {
                    None
                }
            })
            .collect();
        write_info_log(
            "agent_loop",
            &format!("可用工具列表: [{}]", tool_names.join(", ")),
        );
    } else {
        write_info_log("agent_loop", "警告: tools 列表为空,LLM 将无法调用任何工具");
    }

    let mut final_round_idx: usize = 0;
    'round: for round_idx in 0..max_llm_rounds {
        final_round_idx = round_idx;
        write_info_log(
            "agent_loop",
            &format!(
                "========== 第 {} 轮开始 (max={}) ==========",
                round_idx, max_llm_rounds
            ),
        );

        // 每轮重新构建 system prompt(从磁盘读取最新配置)
        let mut system_prompt = system_prompt_fn();

        // 每轮开始时从待处理队列中 drain 用户在 agent loop 期间输入的新消息
        let pending_count_before = safe_lock(&pending_user_messages, "agent::pending_count").len();
        drain_pending_user_messages(&mut messages, &pending_user_messages);
        if pending_count_before > 0 {
            write_info_log(
                "agent_loop",
                &format!("drain 了 {} 条用户增量消息", pending_count_before),
            );
        }

        // ── Layer 1: micro_compact(替换旧 tool results)──
        // ── Layer 2: if tokens > threshold → auto_compact(LLM 摘要)──
        // abort 语义统一:abort PreMicroCompact = 中止整个 compact 子管线(包括 auto_compact)
        if compact_config.enabled {
            let mut compact_aborted = false;

            // ★ PreMicroCompact hook
            if hook_manager.has_hooks_for(HookEvent::PreMicroCompact) {
                let ctx = HookContext {
                    event: HookEvent::PreMicroCompact,
                    messages: Some(messages.clone()),
                    model: Some(provider.model.clone()),
                    session_id: Some(session_id.clone()),
                    ..Default::default()
                };
                if let Some(result) = hook_manager.execute(HookEvent::PreMicroCompact, ctx)
                    && result.is_stop()
                {
                    write_info_log(
                        "PreMicroCompact hook",
                        "compact 子管线被 hook 中止(跳过 micro + auto)",
                    );
                    compact_aborted = true;
                }
            }

            if !compact_aborted {
                compact::micro_compact(
                    &mut messages,
                    compact_config.keep_recent,
                    &compact_config.micro_compact_exempt_tools,
                );

                // ★ PostMicroCompact hook
                if hook_manager.has_hooks_for(HookEvent::PostMicroCompact) {
                    let ctx = HookContext {
                        event: HookEvent::PostMicroCompact,
                        messages: Some(messages.clone()),
                        session_id: Some(session_id.clone()),
                        ..Default::default()
                    };
                    if let Some(result) = hook_manager.execute(HookEvent::PostMicroCompact, ctx)
                        && let Some(new_msgs) = result.messages
                    {
                        messages = new_msgs;
                    }
                }

                if compact::estimate_tokens(&messages) > compact_config.token_threshold {
                    write_info_log(
                        "agent_loop",
                        "auto_compact triggered (token threshold exceeded)",
                    );

                    // ★ PreAutoCompact hook
                    let mut protected_context: Option<String> = None;
                    if hook_manager.has_hooks_for(HookEvent::PreAutoCompact) {
                        let ctx = HookContext {
                            event: HookEvent::PreAutoCompact,
                            messages: Some(messages.clone()),
                            system_prompt: system_prompt.clone(),
                            model: Some(provider.model.clone()),
                            session_id: Some(session_id.clone()),
                            ..Default::default()
                        };
                        if let Some(result) = hook_manager.execute(HookEvent::PreAutoCompact, ctx) {
                            if result.is_stop() {
                                write_info_log("PreAutoCompact hook", "auto_compact 被 hook 中止");
                                compact_aborted = true;
                            }
                            if let Some(ac) = result.additional_context {
                                protected_context = Some(ac);
                            }
                        }
                    }

                    if !compact_aborted {
                        if let Err(e) = compact::auto_compact(
                            &mut messages,
                            &provider,
                            &invoked_skills,
                            &session_id,
                            protected_context.as_deref(),
                        )
                        .await
                        {
                            write_error_log("agent_loop", &format!("auto_compact failed: {}", e));
                        } else {
                            // ★ PostAutoCompact hook
                            if hook_manager.has_hooks_for(HookEvent::PostAutoCompact) {
                                let ctx = HookContext {
                                    event: HookEvent::PostAutoCompact,
                                    messages: Some(messages.clone()),
                                    session_id: Some(session_id.clone()),
                                    ..Default::default()
                                };
                                if let Some(result) =
                                    hook_manager.execute(HookEvent::PostAutoCompact, ctx)
                                    && let Some(new_msgs) = result.messages
                                {
                                    messages = new_msgs;
                                }
                            }
                        }
                    }
                }
            }
        }

        // 检查是否有待办事项(递增轮数计数器,供内置 todo_nag hook 判断)
        todo_manager.increment_turn();

        // 清空流式内容缓冲(每轮开始时)
        {
            let mut stream_buf = safe_lock(&streaming_content, "agent::streaming_content_clear");
            stream_buf.clear();
        }

        // 记录请求输入日志
        {
            let mut log_content = String::new();
            if let Some(ref system_prompt) = system_prompt {
                log_content.push_str(&format!("[System] {}\n", system_prompt));
            }
            for msg in &messages {
                match msg.role.as_str() {
                    ROLE_ASSISTANT => {
                        if !msg.content.is_empty() {
                            log_content.push_str(&format!("[Assistant] {}\n", msg.content));
                        }
                        if let Some(ref tool_calls) = msg.tool_calls {
                            for tool_call in tool_calls {
                                log_content.push_str(&format!(
                                    "[Assistant/ToolCall] {}: {}\n",
                                    tool_call.name, tool_call.arguments
                                ));
                            }
                        }
                    }
                    ROLE_TOOL => {
                        let id = msg.tool_call_id.as_deref().unwrap_or("?");
                        let tool_name = msg
                            .tool_calls
                            .as_ref()
                            .and_then(|tool_calls| tool_calls.first())
                            .map(|tool_call| tool_call.name.as_str())
                            .unwrap_or("unknown");
                        log_content.push_str(&format!(
                            "[Tool/Result({} with id `{}`)] result:\n{}\n",
                            tool_name, id, msg.content
                        ));
                    }
                    ROLE_USER => {
                        log_content.push_str(&format!("[User] {}\n", msg.content));
                    }
                    other => {
                        log_content.push_str(&format!("[{}] {}\n", other, msg.content));
                    }
                }
            }
            write_info_log("Chat 请求", &log_content);
        }

        // ★ PreLlmRequest hook(可修改 messages 和 system_prompt)
        if hook_manager.has_hooks_for(HookEvent::PreLlmRequest) {
            let ctx = HookContext {
                event: HookEvent::PreLlmRequest,
                messages: Some(messages.clone()),
                system_prompt: system_prompt.clone(),
                model: Some(provider.model.clone()),
                session_id: Some(session_id.clone()),
                cwd: std::env::current_dir()
                    .map(|p| p.display().to_string())
                    .unwrap_or_else(|_| ".".to_string()),
                ..Default::default()
            };
            if let Some(result) = hook_manager.execute(HookEvent::PreLlmRequest, ctx) {
                if result.is_stop() {
                    let _ = tx.send(StreamMsg::Error(ChatError::HookAborted));
                    return;
                }
                if let Some(new_msgs) = result.messages {
                    messages = new_msgs;
                }
                if let Some(new_prompt) = result.system_prompt {
                    system_prompt = Some(new_prompt);
                }
                if let Some(inject) = result.inject_messages {
                    messages.extend(inject);
                }
            }
        }

        // 更新实际上下文 token 估算值(供 UI 显示)
        {
            let tokens = compact::estimate_tokens(&messages);
            if let Ok(mut ct) = estimated_context_tokens.lock() {
                *ct = tokens;
            }
        }

        // 记录本轮请求的消息统计
        {
            let has_images = messages
                .iter()
                .any(|m| m.images.as_ref().is_some_and(|imgs| !imgs.is_empty()));
            write_info_log(
                "agent_loop",
                &format!(
                    "{} 轮请求: messages={}, has_images={}, supports_vision={}",
                    round_idx,
                    messages.len(),
                    has_images,
                    provider.supports_vision
                ),
            );
        }

        let request = match build_request_with_tools(
            &provider,
            &messages,
            tools.clone(),
            system_prompt.as_deref(),
        ) {
            Ok(req) => {
                write_info_log("agent_loop", "build_request_with_tools 成功");
                req
            }
            Err(e) => {
                let _ = tx.send(StreamMsg::Error(e));
                return;
            }
        };

        // ── 指数退避重试循环:包裹整个流式请求+读取过程 ──
        // retry_attempt 从 1 开始,每次创建流或读流失败后自增并重试
        let mut retry_attempt: u32 = 0;

        'api_retry: loop {
            retry_attempt += 1;

            // ── 创建流式请求(可重试)──
            write_info_log(
                "agent_loop",
                &format!("开始创建流式请求 (attempt={})...", retry_attempt),
            );
            let mut stream = match client.chat().create_stream(request.clone()).await {
                Ok(s) => {
                    write_info_log("agent_loop", "流式请求创建成功");
                    s
                }
                Err(e) => {
                    let err = ChatError::from(e);
                    write_error_log("Chat API 流式请求创建", &err.to_string());
                    if let Some(policy) = retry_policy_for(&err)
                        && retry_attempt <= policy.max_attempts
                    {
                        let delay_ms =
                            backoff_delay_ms(retry_attempt, policy.base_ms, policy.cap_ms);
                        write_info_log(
                            "agent_loop",
                            &format!(
                                "流式创建失败,{}ms 后重试 ({}/{})",
                                delay_ms, retry_attempt, policy.max_attempts
                            ),
                        );
                        let _ = tx.send(StreamMsg::Retrying {
                            attempt: retry_attempt,
                            max_attempts: policy.max_attempts,
                            delay_ms,
                            error: err.display_message(),
                        });
                        tokio::select! {
                            _ = tokio::time::sleep(std::time::Duration::from_millis(delay_ms)) => {
                                continue 'api_retry;
                            }
                            _ = cancel_token.cancelled() => {
                                let _ = tx.send(StreamMsg::Cancelled);
                                return;
                            }
                        }
                    }
                    let _ = tx.send(StreamMsg::Error(err));
                    return;
                }
            };

            // ── 读取流式响应 ──
            let mut finish_reason: Option<async_openai::types::chat::FinishReason> = None;
            let mut assistant_text = String::new();
            // 手动收集 tool_calls:按 index 聚合 (id, name, arguments)
            let mut active_tool_call_parts: std::collections::BTreeMap<u32, StreamingToolCallPart> =
                std::collections::BTreeMap::new();
            let mut deserialize_failed = false;
            // 流式读取中途遇到 tool_call_id 不一致的请求错误
            let mut needs_compact_for_tool_id_mismatch = false;
            // 流式读取中途遇到的可重试错误
            let mut stream_retriable_error: Option<ChatError> = None;

            let mut received_chunks: u32 = 0;

            'stream: loop {
                tokio::select! {
                    result = stream.next() => {
                        match result {
                            Some(Ok(response)) => {
                                received_chunks += 1;
                                // 记录前几个 chunk 的原始信息,便于调试
                                if received_chunks <= 3 {
                                    let choices_debug: Vec<String> = response.choices.iter().map(|choice| {
                                        format!(
                                            "idx={}, finish_reason={:?}, has_content={}, has_tool_calls={}",
                                            choice.index,
                                            choice.finish_reason,
                                            choice.delta.content.is_some(),
                                            choice.delta.tool_calls.is_some(),
                                        )
                                    }).collect();
                                    write_info_log(
                                        "stream_chunk",
                                        &format!("chunk #{}: choices=[{}]", received_chunks, choices_debug.join("; ")),
                                    );
                                }
                                for choice in &response.choices {
                                    if let Some(ref content) = choice.delta.content {
                                        assistant_text.push_str(content);
                                        let mut stream_buf = safe_lock(&streaming_content, "agent::stream_chunk");
                                        stream_buf.push_str(content);
                                        drop(stream_buf);
                                        let _ = tx.send(StreamMsg::Chunk);
                                    }
                                    // 尝试直接读取 tool_calls(若 async-openai 能反序列化)
                                    if let Some(ref toolcall_chunks) = choice.delta.tool_calls {
                                        for chunk in toolcall_chunks {
                                            let entry =
                                                active_tool_call_parts.entry(chunk.index).or_insert_with(|| {
                                                    StreamingToolCallPart {
                                                        call_id: chunk.id.clone().unwrap_or_default(),
                                                        function_name: String::new(),
                                                        function_arguments: String::new(),
                                                    }
                                                });
                                            if entry.call_id.is_empty()
                                                && let Some(ref id) = chunk.id {
                                                    entry.call_id = id.clone();
                                                }
                                            if let Some(ref tool_function) = chunk.function {
                                                if let Some(ref name) = tool_function.name {
                                                    entry.function_name.push_str(name);
                                                }
                                                if let Some(ref args) = tool_function.arguments {
                                                    entry.function_arguments.push_str(args);
                                                }
                                            }
                                        }
                                    }
                                    if let Some(ref finish_reason_val) = choice.finish_reason {
                                        finish_reason = Some(*finish_reason_val);
                                    }
                                }
                            }
                            Some(Err(e)) => {
                                let error_str = format!("{}", e);
                                write_error_log("Chat API 流式响应 error", &error_str);
                                // 检测是否是 tool_calls 反序列化错误(Gemini 等不返回 chunk index)
                                if error_str.contains("missing field `index`")
                                    || error_str.contains("tool_calls")
                                {
                                    // 标记需要用非流式重做,跳出流式循环
                                    deserialize_failed = true;
                                    break 'stream;
                                }
                                let err = ChatError::from(e);
                                // 检测 tool_call_id 不一致错误(API 返回 "tool_call_id ... not found")
                                // 这通常是消息历史损坏导致的,通过压缩上下文并重试可恢复
                                if matches!(&err, ChatError::ApiBadRequest(msg) if msg.contains("tool_call_id")) {
                                    write_error_log(
                                        "Chat API 流式响应",
                                        &format!("检测到 tool_call_id 不一致错误,将压缩上下文后重试: {}", err),
                                    );
                                    needs_compact_for_tool_id_mismatch = true;
                                    break 'stream;
                                }
                                // 可重试错误:记录后跳出流式循环,由外层决策是否重试
                                if retry_policy_for(&err).is_some() {
                                    stream_retriable_error = Some(err);
                                    break 'stream;
                                }
                                // 不可重试:直接报错退出
                                write_error_log("Chat API 流式响应(不可重试)", &err.to_string());
                                let _ = tx.send(StreamMsg::Error(err));
                                return;
                            }
                            None => {
                                write_info_log("agent_loop", "流式结束 (stream returned None)");
                                break 'stream;
                            }
                        }
                    }
                    _ = cancel_token.cancelled() => {
                        let _ = tx.send(StreamMsg::Cancelled);
                        return;
                    }
                }
            }

            // ── 处理 tool_call_id 不一致错误:压缩上下文后重试本轮 ──
            if needs_compact_for_tool_id_mismatch {
                write_info_log(
                    "agent_loop",
                    "tool_call_id 不一致错误:将执行 auto_compact 压缩上下文后重试",
                );
                // 清空已积累的部分内容
                {
                    let mut stream_buf =
                        safe_lock(&streaming_content, "agent::tool_id_error_clear");
                    stream_buf.clear();
                }
                // 通过 auto_compact 重建干净的上下文(摘要 + 全新消息结构,无孤立引用)
                if compact_config.enabled {
                    if let Err(e) = compact::auto_compact(
                        &mut messages,
                        &provider,
                        &invoked_skills,
                        &session_id,
                        None,
                    )
                    .await
                    {
                        write_error_log(
                            "agent_loop",
                            &format!("tool_call_id 恢复时 auto_compact 失败: {}", e),
                        );
                        let _ = tx.send(StreamMsg::Error(ChatError::Other(format!(
                            "消息历史损坏且自动修复失败: {}",
                            e
                        ))));
                        return;
                    }
                    continue 'round;
                } else {
                    // compact 未启用,无法恢复
                    let _ = tx.send(StreamMsg::Error(ChatError::Other(
                        "消息历史中 tool_call_id 不一致,且 compact 未启用,无法自动恢复"
                            .to_string(),
                    )));
                    return;
                }
            }

            // ── 处理流式读取中途的可重试错误 ──
            if let Some(err) = stream_retriable_error {
                write_error_log("Chat API 流式响应(将重试)", &err.to_string());
                if let Some(policy) = retry_policy_for(&err)
                    && retry_attempt <= policy.max_attempts
                {
                    // 清空已积累的部分内容,重新开始本轮请求
                    {
                        let mut stream_buf =
                            safe_lock(&streaming_content, "agent::stream_retry_clear");
                        stream_buf.clear();
                    }
                    let delay_ms = backoff_delay_ms(retry_attempt, policy.base_ms, policy.cap_ms);
                    write_info_log(
                        "agent_loop",
                        &format!(
                            "流式中断,{}ms 后重试 ({}/{})",
                            delay_ms, retry_attempt, policy.max_attempts
                        ),
                    );
                    let _ = tx.send(StreamMsg::Retrying {
                        attempt: retry_attempt,
                        max_attempts: policy.max_attempts,
                        delay_ms,
                        error: err.display_message(),
                    });
                    tokio::select! {
                        _ = tokio::time::sleep(std::time::Duration::from_millis(delay_ms)) => {
                            continue 'api_retry;
                        }
                        _ = cancel_token.cancelled() => {
                            let _ = tx.send(StreamMsg::Cancelled);
                            return;
                        }
                    }
                }
                // 重试次数耗尽
                let _ = tx.send(StreamMsg::Error(err));
                return;
            }

            // 记录流式回复日志
            if !assistant_text.is_empty() {
                write_info_log("Sprite 回复", &assistant_text);
            }

            write_info_log(
                "agent_loop",
                &format!(
                    "流式循环结束: finish_reason={:?}, assistant_text_len={}, active_tool_call_parts={}, deserialize_failed={}",
                    finish_reason,
                    assistant_text.len(),
                    active_tool_call_parts.len(),
                    deserialize_failed
                ),
            );

            // 如果流式遇到 tool_calls 反序列化错误,或者流式返回空响应(finish_reason=None 且无有效内容),
            // fallback 到非流式获取完整响应。
            // 常见场景:某些 API 对多模态+流式组合返回空 choices,需要非流式重试。
            let stream_empty = finish_reason.is_none()
                && assistant_text.is_empty()
                && active_tool_call_parts.is_empty();
            write_info_log(
                "agent_loop",
                &format!(
                    "流式结果分析: stream_empty={}, deserialize_failed={}, received_chunks={}",
                    stream_empty, deserialize_failed, received_chunks
                ),
            );
            if deserialize_failed || stream_empty {
                if stream_empty {
                    write_info_log(
                        "agent_loop",
                        &format!(
                            "流式返回空响应 (chunks={}, finish_reason=None, 无内容),fallback 到非流式重试",
                            received_chunks
                        ),
                    );
                }
                // 清空流式内容(切换到非流式)
                {
                    let mut stream_buf = safe_lock(&streaming_content, "agent::fallback_clear");
                    stream_buf.clear();
                }
                // 使用宽松反序列化的非流式调用(兼容非标准 finish_reason),同样支持重试
                let fallback_result = loop {
                    let create_fut = call_openai_non_stream_lenient(&provider, &request);
                    let result = tokio::select! {
                        result = create_fut => result,
                        _ = cancel_token.cancelled() => {
                            let _ = tx.send(StreamMsg::Cancelled);
                            return;
                        }
                    };
                    match result {
                        Ok(r) => break r,
                        Err(e) => {
                            write_error_log("Sprite API fallback 非流式", &e.to_string());
                            if let Some(policy) = retry_policy_for(&e)
                                && retry_attempt <= policy.max_attempts
                            {
                                let delay_ms =
                                    backoff_delay_ms(retry_attempt, policy.base_ms, policy.cap_ms);
                                write_info_log(
                                    "agent_loop",
                                    &format!(
                                        "fallback 非流式失败,{}ms 后重试 ({}/{})",
                                        delay_ms, retry_attempt, policy.max_attempts
                                    ),
                                );
                                let _ = tx.send(StreamMsg::Retrying {
                                    attempt: retry_attempt,
                                    max_attempts: policy.max_attempts,
                                    delay_ms,
                                    error: e.display_message(),
                                });
                                tokio::select! {
                                    _ = tokio::time::sleep(std::time::Duration::from_millis(delay_ms)) => {
                                        retry_attempt += 1;
                                        continue;
                                    }
                                    _ = cancel_token.cancelled() => {
                                        let _ = tx.send(StreamMsg::Cancelled);
                                        return;
                                    }
                                }
                            }
                            let _ = tx.send(StreamMsg::Error(e));
                            return;
                        }
                    }
                };

                write_info_log(
                    "agent_loop",
                    &format!(
                        "fallback 非流式结果: has_tool_calls={}, has_content={}, finish_reason={:?}",
                        fallback_result.has_tool_calls(),
                        fallback_result
                            .content
                            .as_ref()
                            .map(|content| content.len())
                            .unwrap_or(0),
                        fallback_result.finish_reason
                    ),
                );

                if fallback_result.has_tool_calls()
                    && let Some(tool_items) = fallback_result.tool_calls
                {
                    if tool_items.is_empty() {
                        write_info_log("agent_loop", "fallback tool_calls 为空列表,break 'round");
                        break 'round;
                    }
                    let assistant_text: String =
                        fallback_result.content.clone().unwrap_or_default();
                    match process_tool_calls(tool_items, assistant_text, &mut messages, &tool_ctx) {
                        Ok(result) => {
                            // ── Layer 3: compact tool 触发 ──
                            if result.compact_requested && compact_config.enabled {
                                let _ = compact::auto_compact(
                                    &mut messages,
                                    &provider,
                                    &invoked_skills,
                                    &session_id,
                                    None,
                                )
                                .await;
                            }
                            // ── Plan 被批准且清空上下文 ──
                            if let Some(ref plan_content) = result.plan_with_context_clear {
                                write_info_log(
                                    "agent_loop",
                                    "Clearing context after plan approval",
                                );
                                // TODO 这里怎么能直接这样粗暴清空.... 你要有优先级去保留,起码用户消息是要保留的,参考 micro_compact 的处理
                                messages.clear();
                                if let Ok(mut shared) = ui_messages.lock() {
                                    shared.clear();
                                }
                                let plan_msg = ChatMessage {
                                    role: ROLE_USER.to_string(),
                                    content: format!(
                                        "以下计划已获批准,请按计划执行:\n\n{}",
                                        plan_content
                                    ),
                                    tool_calls: None,
                                    tool_call_id: None,
                                    images: None,
                                };
                                messages.push(plan_msg.clone());
                                push_ui(&ui_messages, plan_msg);
                            }
                            continue 'round;
                        }
                        Err(e) => {
                            write_error_log(
                                "agent_loop",
                                &format!("process_tool_calls failed: {}", e),
                            );
                            return;
                        }
                    }
                }

                // 普通文本回复(或非标准 finish_reason 如 network_error)
                if let Some(ref content) = fallback_result.content
                    && !content.is_empty()
                {
                    write_info_log("Sprite 回复", content);
                    let mut stream_buf = safe_lock(&streaming_content, "agent::fallback_content");
                    stream_buf.push_str(content);
                    drop(stream_buf);
                    let _ = tx.send(StreamMsg::Chunk);
                }
                // 非标准 finish_reason 且无内容时,报告错误
                if let Some(ref reason) = fallback_result.finish_reason
                    && !matches!(
                        reason.as_str(),
                        "stop" | "length" | "tool_calls" | "content_filter" | "function_call"
                    )
                    && fallback_result
                        .content
                        .as_deref()
                        .unwrap_or_default()
                        .is_empty()
                {
                    let error_msg = ChatError::AbnormalFinish(reason.clone());
                    write_error_log("Sprite API fallback 非流式", &error_msg.to_string());
                    let _ = tx.send(StreamMsg::Error(error_msg));
                    return;
                }

                // fallback 非流式正常结束,但如果有用户增量消息则继续循环
                let has_pending =
                    !safe_lock(&pending_user_messages, "agent::pending_check_fallback").is_empty();
                write_info_log(
                    "agent_loop",
                    &format!("fallback 正常结束,pending_user_messages={}", has_pending),
                );
                if has_pending {
                    flush_streaming_as_message(&streaming_content, &mut messages, &ui_messages);
                    write_info_log("agent_loop", "有用户增量消息,continue 'round");
                    continue 'round;
                }
                write_info_log("agent_loop", "无用户增量消息,break 'round (fallback 路径)");
                break 'round;
            }

            // ── 检查流式模式下是否有 tool_calls ──
            // 优先检查 active_tool_call_parts 是否非空,而非仅依赖 finish_reason。
            // 某些 API(非 OpenAI)流式返回的 finish_reason 不是 ToolCodes 枚举值,
            // 但 chunk 中确实包含 tool_calls 数据。此时如果只看 finish_reason 会直接
            // break 'round,导致工具调用被丢弃,agent 提前结束。
            let has_tool_calls = !active_tool_call_parts.is_empty();
            write_info_log(
                "agent_loop",
                &format!(
                    "流式路径决策: has_tool_calls={}, finish_reason={:?}",
                    has_tool_calls, finish_reason
                ),
            );

            if has_tool_calls {
                // 日志:检测 finish_reason 与实际 tool_calls 是否一致
                let finish_reason_is_tool_calls = matches!(
                    finish_reason,
                    Some(async_openai::types::chat::FinishReason::ToolCalls)
                );
                if !finish_reason_is_tool_calls {
                    write_info_log(
                        "agent_loop",
                        &format!(
                            "finish_reason={:?} 不是 ToolCalls 但 active_tool_call_parts 非空({}),仍处理工具调用",
                            finish_reason,
                            active_tool_call_parts.len()
                        ),
                    );
                }

                let tool_items: Vec<ToolCallItem> = active_tool_call_parts
                    .into_values()
                    .map(|part| {
                        // 某些 API 在流式 chunk 中不返回 tool_call id,
                        // 导致 id 为空字符串;发送给 API 时会报 tool_call_id not found。
                        // 此处为空 id 生成随机唯一 id。
                        let id = if part.call_id.is_empty() {
                            let rand_id =
                                format!("call_{:016x}", rand::thread_rng().r#gen::<u64>());
                            write_info_log(
                                "agent_loop",
                                &format!(
                                    "tool_call id 为空(API 未在流式 chunk 中返回),已生成随机 id: {}",
                                    rand_id
                                ),
                            );
                            rand_id
                        } else {
                            part.call_id
                        };
                        ToolCallItem { id, name: part.function_name, arguments: part.function_arguments }
                    })
                    .collect();

                if tool_items.is_empty() {
                    write_info_log("agent_loop", "流式 tool_items 转换后为空,break 'round");
                    break 'round;
                }

                write_info_log(
                    "agent_loop",
                    &format!(
                        "开始处理 {} 个工具调用: [{}]",
                        tool_items.len(),
                        tool_items
                            .iter()
                            .map(|t| t.name.as_str())
                            .collect::<Vec<_>>()
                            .join(", ")
                    ),
                );
                match process_tool_calls(tool_items, assistant_text, &mut messages, &tool_ctx) {
                    Ok(result) => {
                        // ── Layer 3: compact tool 触发 ──
                        if result.compact_requested && compact_config.enabled {
                            let _ = compact::auto_compact(
                                &mut messages,
                                &provider,
                                &invoked_skills,
                                &session_id,
                                None,
                            )
                            .await;
                        }
                        // ── Plan 被批准且清空上下文 ──
                        if let Some(ref plan_content) = result.plan_with_context_clear {
                            write_info_log("agent_loop", "Clearing context after plan approval");
                            messages.clear();
                            if let Ok(mut shared) = ui_messages.lock() {
                                shared.clear();
                            }
                            let plan_msg = ChatMessage {
                                role: ROLE_USER.to_string(),
                                content: format!(
                                    "以下计划已获批准,请按计划执行:\n\n{}",
                                    plan_content
                                ),
                                tool_calls: None,
                                tool_call_id: None,
                                images: None,
                            };
                            messages.push(plan_msg.clone());
                            push_ui(&ui_messages, plan_msg);
                        }
                        continue 'round;
                    }
                    Err(e) => {
                        write_error_log("agent_loop", &format!("process_tool_calls failed: {}", e));
                        return;
                    }
                }
            } else {
                // 正常结束,但如果有用户增量消息则继续循环
                let has_pending =
                    !safe_lock(&pending_user_messages, "agent::pending_check_stream").is_empty();
                write_info_log(
                    "agent_loop",
                    &format!(
                        "LLM 未调用工具 (finish_reason={:?}, text_len={}),pending_user_messages={}",
                        finish_reason,
                        assistant_text.len(),
                        has_pending
                    ),
                );
                if has_pending {
                    flush_streaming_as_message(&streaming_content, &mut messages, &ui_messages);
                    write_info_log("agent_loop", "有用户增量消息,continue 'round");
                    continue 'round;
                }

                // ★ Stop hook:LLM 即将结束回复(无工具调用且无待处理消息),纠查官可阻止并注入反馈
                if hook_manager.has_hooks_for(HookEvent::Stop) {
                    flush_streaming_as_message(&streaming_content, &mut messages, &ui_messages);
                    let stop_ctx = HookContext {
                        event: HookEvent::Stop,
                        messages: Some(messages.clone()),
                        system_prompt: system_prompt.clone(),
                        model: Some(provider.model.clone()),
                        user_input: Some(assistant_text.clone()),
                        session_id: Some(session_id.clone()),
                        ..Default::default()
                    };
                    if let Some(result) = hook_manager.execute(HookEvent::Stop, stop_ctx) {
                        // 注入额外上下文(追加到 system_prompt)
                        if let Some(ref ctx_text) = result.additional_context {
                            let current = system_prompt.unwrap_or_default();
                            system_prompt = Some(format!("{}\n\n{}", current, ctx_text));
                        }
                        // retry_feedback → 注入为 user message,LLM 带反馈继续
                        if let Some(ref feedback) = result.retry_feedback {
                            write_info_log("Stop hook", &format!("纠查官反馈: {}", feedback));
                            let feedback_msg = ChatMessage {
                                role: ROLE_USER.to_string(),
                                content: feedback.clone(),
                                tool_calls: None,
                                tool_call_id: None,
                                images: None,
                            };
                            messages.push(feedback_msg.clone());
                            push_ui(&ui_messages, feedback_msg);
                            continue 'round;
                        }
                        // stop → 直接中止
                        if result.is_stop() {
                            let _ = tx.send(StreamMsg::Error(ChatError::HookAborted));
                            return;
                        }
                    }
                }

                write_info_log(
                    "agent_loop",
                    &format!(
                        "break 'round: LLM 返回 Stop 且无工具调用,无待处理消息 (round={}, text_len={})",
                        round_idx,
                        assistant_text.len()
                    ),
                );
                break 'round;
            }

            // 流式请求成功完成,退出重试循环
            #[allow(unreachable_code)]
            {
                break 'api_retry;
            }
        } // end 'api_retry
    } // end 'round

    write_info_log(
        "agent_loop",
        &format!(
            "agent loop 结束,发送 Done (共执行 {} 轮后退出 'round)",
            final_round_idx + 1
        ),
    );
    let _ = tx.send(StreamMsg::Done);
}

/// 从待处理队列中 drain 用户在 agent loop 期间发送的新消息,追加到 messages
fn drain_pending_user_messages(
    messages: &mut Vec<ChatMessage>,
    pending_user_messages: &Arc<Mutex<Vec<ChatMessage>>>,
) {
    let mut pending = safe_lock(pending_user_messages, "agent::drain_pending");
    if !pending.is_empty() {
        // 给每条追加的用户消息添加 [User appended] 标记
        for msg in pending.iter_mut() {
            if msg.role == "user" {
                msg.content = format!("[User appended] {}", msg.content);
            }
        }
        messages.append(&mut *pending);
    }
}

/// 向共享消息列表中追加一条消息(agent 线程写入,UI 线程读取)
fn push_ui(shared: &Arc<Mutex<Vec<ChatMessage>>>, msg: ChatMessage) {
    if let Ok(mut msgs) = shared.lock() {
        msgs.push(msg);
    }
}

/// 将 streaming_content 中的文本保存为 assistant 消息(多轮 agent loop 中间轮的文本回复)
/// 调用后 streaming_content 被清空,避免 UI 侧 finish_loading 再次保存导致重复
fn flush_streaming_as_message(
    streaming_content: &Arc<Mutex<String>>,
    messages: &mut Vec<ChatMessage>,
    ui_messages: &Arc<Mutex<Vec<ChatMessage>>>,
) {
    let mut stream_buf = safe_lock(streaming_content, "agent::flush_streaming");
    if !stream_buf.is_empty() {
        let text_msg = ChatMessage {
            role: ROLE_ASSISTANT.to_string(),
            content: std::mem::take(&mut *stream_buf),
            tool_calls: None,
            tool_call_id: None,
            images: None,
        };
        messages.push(text_msg.clone());
        push_ui(ui_messages, text_msg);
    }
}

/// 记录工具调用请求日志
fn log_tool_request(tool_items: &[ToolCallItem]) {
    let mut log_content = String::new();
    for item in tool_items {
        log_content.push_str(&format!("- {}: {}\n", item.name, item.arguments));
    }
    write_info_log("工具调用请求", &log_content);
}

/// 记录工具调用结果日志
fn log_tool_results(tool_items: &[ToolCallItem], tool_results: &[ToolResultMsg]) {
    let mut log_content = String::new();
    for (i, result) in tool_results.iter().enumerate() {
        let (tool_name, tool_args) = tool_items
            .get(i)
            .map(|t| (t.name.as_str(), t.arguments.as_str()))
            .unwrap_or(("unknown", ""));
        log_content.push_str(&format!(
            "- [{}] {}({}): {}\n",
            result.tool_call_id, tool_name, tool_args, result.result
        ));
    }
    write_info_log("工具调用结果", &log_content);
}

/// process_tool_calls 的返回结果
struct ToolCallResult {
    compact_requested: bool,
    /// Plan 被批准且用户选择清空上下文,值为 plan 文件内容
    plan_with_context_clear: Option<String>,
}

/// 处理工具调用的公共逻辑:发送请求、等待结果、更新 messages
/// 返回 Ok(ToolCallResult) 表示成功(应 continue 循环)
/// Err(ChatError) 表示 channel 断开或执行失败
fn process_tool_calls(
    tool_items: Vec<ToolCallItem>,
    assistant_text: String,
    messages: &mut Vec<ChatMessage>,
    ctx: &ToolCallContext<'_>,
) -> Result<ToolCallResult, ChatError> {
    log_tool_request(&tool_items);

    if !assistant_text.is_empty() {
        write_info_log("Sprite 回复", &assistant_text);
    }

    // 检查是否有 compact tool 被调用
    let compact_requested = tool_items.iter().any(|t| t.name == CompactTool {}.name());

    // ★ 如果 LLM 同时返回了文本和 tool_calls,拆成两条消息:
    //   1. 纯文本 assistant 消息(让 UI 先渲染文字)
    //   2. tool_call assistant 消息(content 为空,只带 tool_calls)
    //   这样渲染时文字在上面,tool_call 在下面
    if !assistant_text.is_empty() {
        let text_msg = ChatMessage {
            role: ROLE_ASSISTANT.to_string(),
            content: assistant_text,
            tool_calls: None,
            tool_call_id: None,
            images: None,
        };
        messages.push(text_msg.clone());
        push_ui(ctx.ui_messages, text_msg);
        // 清空 streaming_content,文本已保存,避免 UI 继续显示流式内容
        if let Ok(mut stream_buf) = ctx.streaming_content.lock() {
            stream_buf.clear();
        }
    }

    let tool_call_msg = ChatMessage {
        role: ROLE_ASSISTANT.to_string(),
        content: String::new(),
        tool_calls: Some(tool_items.clone()),
        tool_call_id: None,
        images: None,
    };
    messages.push(tool_call_msg.clone());
    push_ui(ctx.ui_messages, tool_call_msg);

    if ctx
        .stream_msg_sender
        .send(StreamMsg::ToolCallRequest(tool_items.clone()))
        .is_err()
    {
        return Err(ChatError::Other("工具调用通道已断开".to_string()));
    }

    let mut tool_results: Vec<ToolResultMsg> = Vec::new();
    let mut plan_clear_context: Option<String> = None;
    for _ in &tool_items {
        match ctx.tool_result_receiver.recv() {
            Ok(result) => {
                // 检测 ExitPlanMode 返回清空上下文信号
                if result.plan_decision == PlanDecision::ApproveAndClearContext {
                    plan_clear_context = Some(result.result.clone());
                }
                tool_results.push(result);
            }
            Err(_) => return Err(ChatError::Other("工具执行结果通道已断开".to_string())),
        }
    }

    log_tool_results(&tool_items, &tool_results);

    // 收集需要延迟注入的图片消息(在所有 tool results 之后统一注入,
    // 避免在 tool results 中间插入 user 消息导致 API 报错)
    let mut deferred_image_msgs: Vec<ChatMessage> = Vec::new();

    for result in tool_results {
        let mut result_content = result.result;
        let result_images = result.images;

        // 查找工具名
        let tool_name = tool_items
            .iter()
            .find(|t| t.id == result.tool_call_id)
            .map(|t| t.name.clone());

        // ★ PostToolExecution hook
        if ctx.hook_manager.has_hooks_for(HookEvent::PostToolExecution) {
            let hook_ctx = HookContext {
                event: HookEvent::PostToolExecution,
                tool_name: tool_name.clone(),
                tool_result: Some(result_content.clone()),
                session_id: Some(ctx.session_id.to_string()),
                cwd: std::env::current_dir()
                    .map(|p| p.display().to_string())
                    .unwrap_or_else(|_| ".".to_string()),
                ..Default::default()
            };
            if let Some(hook_result) = ctx
                .hook_manager
                .execute(HookEvent::PostToolExecution, hook_ctx)
                && let Some(new_result) = hook_result.tool_result
            {
                result_content = new_result;
            }
        }

        let tool_msg = ChatMessage {
            role: ROLE_TOOL.to_string(),
            content: result_content,
            tool_calls: None,
            tool_call_id: Some(result.tool_call_id.clone()),
            images: None,
        };
        messages.push(tool_msg.clone());
        push_ui(ctx.ui_messages, tool_msg);

        // 如果模型支持视觉且工具返回了图片,先收集,稍后统一注入
        if !result_images.is_empty() {
            let tool_label = tool_name.as_deref().unwrap_or("unknown");
            let img_count = result_images.len();
            write_info_log(
                "ImageInjection",
                &format!(
                    "工具 {} 返回了 {} 张图片, supports_vision={}",
                    tool_label, img_count, ctx.supports_vision
                ),
            );
            if ctx.supports_vision {
                let img_msg = ChatMessage {
                    role: ROLE_USER.to_string(),
                    content: format!(
                        "[{tool_label} 返回了 {img_count} 张图片,请查看图片内容并继续帮助完成任务]"
                    ),
                    tool_calls: None,
                    tool_call_id: None,
                    images: Some(
                        result_images
                            .into_iter()
                            .map(|img| super::super::storage::ImageData {
                                base64: img.base64,
                                media_type: img.media_type,
                            })
                            .collect(),
                    ),
                };
                deferred_image_msgs.push(img_msg);
            } else {
                write_info_log(
                    "ImageInjection",
                    &format!(
                        "supports_vision=false,丢弃 {} 返回的 {} 张图片",
                        tool_label, img_count
                    ),
                );
            }
        }
    }

    // ★ 所有 tool results 处理完毕后,统一注入图片 user messages
    if !deferred_image_msgs.is_empty() {
        write_info_log(
            "ImageInjection",
            &format!(
                "在所有 tool results 之后注入 {} 条图片消息",
                deferred_image_msgs.len()
            ),
        );
        for img_msg in deferred_image_msgs {
            // 只加入 LLM 上下文,不推送到 ui_messages(避免 UI 渲染这条内部消息)
            messages.push(img_msg);
        }
    }

    drain_pending_user_messages(messages, ctx.pending_user_messages);

    Ok(ToolCallResult {
        compact_requested,
        plan_with_context_clear: plan_clear_context,
    })
}

// ==================== 指数退避重试 ====================

/// 每种可重试错误的重试策略
struct RetryPolicy {
    /// 最大重试次数(不含首次请求)
    max_attempts: u32,
    /// 首次退避基础延迟(毫秒)
    base_ms: u64,
    /// 延迟上限(毫秒)
    cap_ms: u64,
}

/// 根据错误类型确定重试策略
///
/// 策略设计原则:
/// - 网络瞬断(超时/断连):快速重试,基础 1s,最多 5 次
/// - 5xx 服务端过载(503/504/529):稍慢重试,基础 2s,最多 4 次
/// - 5xx 服务端错误(500/502):再慢一些,基础 3s,最多 3 次
/// - 429 有 retry_after:精确等待(上限 120s),只重试 1 次
/// - 429 无 retry_after:保守重试,基础 5s,最多 3 次
/// - 非标准 finish_reason(如 network_error):中等重试
fn retry_policy_for(error: &ChatError) -> Option<RetryPolicy> {
    match error {
        ChatError::NetworkTimeout(_) | ChatError::StreamInterrupted(_) => Some(RetryPolicy {
            max_attempts: 5,
            base_ms: 1_000,
            cap_ms: 30_000,
        }),
        ChatError::NetworkError(_) => Some(RetryPolicy {
            max_attempts: 5,
            base_ms: 2_000,
            cap_ms: 30_000,
        }),
        ChatError::ApiServerError { status, .. } => match status {
            503 | 504 | 529 => Some(RetryPolicy {
                max_attempts: 4,
                base_ms: 2_000,
                cap_ms: 30_000,
            }),
            500 | 502 => Some(RetryPolicy {
                max_attempts: 3,
                base_ms: 3_000,
                cap_ms: 30_000,
            }),
            _ => None,
        },
        ChatError::ApiRateLimit {
            retry_after_secs: Some(secs),
            ..
        } => {
            // 有明确的等待时间:等待指定时长(上限 120s),只重试一次
            let wait = (*secs).min(120);
            Some(RetryPolicy {
                max_attempts: 1,
                base_ms: wait * 1_000,
                cap_ms: 120_000,
            })
        }
        ChatError::ApiRateLimit {
            retry_after_secs: None,
            ..
        } => Some(RetryPolicy {
            max_attempts: 3,
            base_ms: 5_000,
            cap_ms: 60_000,
        }),
        ChatError::AbnormalFinish(reason)
            if matches!(reason.as_str(), "network_error" | "timeout" | "overloaded") =>
        {
            Some(RetryPolicy {
                max_attempts: 3,
                base_ms: 2_000,
                cap_ms: 20_000,
            })
        }
        // 兜底:Other 中包含过载/访问量过大关键词(部分 API 错误未被正确分类时)
        ChatError::Other(msg)
            if msg.contains("访问量过大")
                || msg.contains("过载")
                || msg.contains("overloaded")
                || msg.contains("too busy")
                || msg.contains("1305") =>
        {
            Some(RetryPolicy {
                max_attempts: 3,
                base_ms: 3_000,
                cap_ms: 30_000,
            })
        }
        _ => None,
    }
}

/// 计算第 `attempt`(从 1 开始)次重试的退避延迟(毫秒)
///
/// 公式:`clamp(base * 2^(attempt-1), 0, cap) + jitter(0..20%)`
fn backoff_delay_ms(attempt: u32, base_ms: u64, cap_ms: u64) -> u64 {
    // 最多移位 10 次,避免溢出
    let shift = (attempt - 1).min(10) as u64;
    let exp = base_ms.saturating_mul(1u64 << shift).min(cap_ms);
    // 加 0–20% 随机抖动,分散并发重试
    let jitter = rand::thread_rng().gen_range(0..=(exp / 5));
    exp + jitter
}