@team-agent/installer 0.3.10 → 0.3.12
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/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/send.rs +9 -2
- package/crates/team-agent/src/coordinator/backoff.rs +83 -2
- package/crates/team-agent/src/coordinator/health.rs +63 -3
- package/crates/team-agent/src/coordinator/tick.rs +327 -167
- package/crates/team-agent/src/mcp_server/helpers.rs +24 -5
- package/crates/team-agent/src/mcp_server/normalize.rs +13 -6
- package/crates/team-agent/src/mcp_server/tests/send.rs +310 -212
- package/crates/team-agent/src/messaging/helpers.rs +30 -10
- package/crates/team-agent/src/messaging/send.rs +71 -14
- package/crates/team-agent/src/messaging/tests/basic.rs +25 -7
- package/crates/team-agent/src/messaging/tests/runtime.rs +489 -125
- package/crates/team-agent/src/messaging/types.rs +19 -4
- package/package.json +4 -4
|
@@ -89,6 +89,9 @@ pub enum TickError {
|
|
|
89
89
|
/// messaging subsystem failure(delivery/scheduler/result watchers).
|
|
90
90
|
#[error("messaging: {0}")]
|
|
91
91
|
Messaging(#[from] crate::messaging::MessagingError),
|
|
92
|
+
/// coordinator.tick panic caught by the daemon loop.
|
|
93
|
+
#[error("panic: {0}")]
|
|
94
|
+
Panic(String),
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
// ===========================================================================
|
|
@@ -98,7 +101,8 @@ pub enum TickError {
|
|
|
98
101
|
/// tick 末原子 save 失败注入钩(bug-084)。生产装配为 `None`(走真实 `save_runtime_state`);
|
|
99
102
|
/// 测试装配一个返回 `Err` 的闭包,在不触碰真实磁盘的前提下强制 save 失败,断言 degraded
|
|
100
103
|
/// `TickReport` 而非 panic/Err。porter 在 `tick` 的「ATOMIC save」包裹点先查它再落真实 save。
|
|
101
|
-
pub type SaveHook =
|
|
104
|
+
pub type SaveHook =
|
|
105
|
+
Box<dyn Fn(&WorkspacePath, &Value) -> Result<(), crate::state::StateError> + Send + Sync>;
|
|
102
106
|
|
|
103
107
|
/// tick 链式副作用 ORDER 记录器(测试探针)。porter 在 `tick` 的每个原子调用点 push 一个
|
|
104
108
|
/// 稳定步骤名;测试断言固定序列。生产装配为 `None`(零开销,porter 用 `if let Some(rec)` 守卫)。
|
|
@@ -222,7 +226,9 @@ impl Coordinator {
|
|
|
222
226
|
// become deliverable. Reset them to `accepted` so the existing
|
|
223
227
|
// `deliver_pending` step below picks them up on THIS tick. Reuses the
|
|
224
228
|
// delivery pipeline; no new injector. Best-effort logging on inner errors.
|
|
225
|
-
if let Err(error) =
|
|
229
|
+
if let Err(error) =
|
|
230
|
+
self.requeue_trust_retries_for_handled_agents(&state, &store, &event_log)
|
|
231
|
+
{
|
|
226
232
|
let _ = event_log.write(
|
|
227
233
|
"messaging.trust_retry_requeue_failed",
|
|
228
234
|
serde_json::json!({"error": error.to_string()}),
|
|
@@ -377,7 +383,9 @@ impl Coordinator {
|
|
|
377
383
|
self.record_step("atomic_save");
|
|
378
384
|
let saved = match &self.save_hook {
|
|
379
385
|
Some(hook) => hook(&self.workspace, &state),
|
|
380
|
-
None =>
|
|
386
|
+
None => {
|
|
387
|
+
crate::state::projection::save_team_scoped_state(self.workspace.as_path(), &state)
|
|
388
|
+
}
|
|
381
389
|
};
|
|
382
390
|
if saved.is_err() {
|
|
383
391
|
return Ok(base_tick_report(
|
|
@@ -390,17 +398,13 @@ impl Coordinator {
|
|
|
390
398
|
}
|
|
391
399
|
|
|
392
400
|
self.record_step("collect_results");
|
|
393
|
-
collections.results =
|
|
394
|
-
crate::messaging::collect_results_and_notify_watchers(
|
|
395
|
-
|
|
401
|
+
collections.results =
|
|
402
|
+
collect_results(crate::messaging::collect_results_and_notify_watchers(
|
|
403
|
+
self.workspace.as_path(),
|
|
404
|
+
&event_log,
|
|
405
|
+
)?);
|
|
396
406
|
self.record_step("prune_dedupe_log");
|
|
397
|
-
Ok(base_tick_report(
|
|
398
|
-
true,
|
|
399
|
-
false,
|
|
400
|
-
None,
|
|
401
|
-
Some(true),
|
|
402
|
-
collections,
|
|
403
|
-
))
|
|
407
|
+
Ok(base_tick_report(true, false, None, Some(true), collections))
|
|
404
408
|
}
|
|
405
409
|
|
|
406
410
|
// #236 nag_removal (N35): the framework-synthesized idle/stuck/deadlock nag
|
|
@@ -408,7 +412,11 @@ impl Coordinator {
|
|
|
408
412
|
// were removed by design. Delivery primitives still flow through the rest of
|
|
409
413
|
// the tick body unchanged.
|
|
410
414
|
|
|
411
|
-
fn capture_missing_sessions(
|
|
415
|
+
fn capture_missing_sessions(
|
|
416
|
+
&self,
|
|
417
|
+
state: &mut Value,
|
|
418
|
+
event_log: &EventLog,
|
|
419
|
+
) -> Result<(), TickError> {
|
|
412
420
|
let report = crate::session_capture::capture_missing_provider_sessions_once(
|
|
413
421
|
state,
|
|
414
422
|
&mut |provider| self.provider_registry.adapter_for(provider),
|
|
@@ -438,7 +446,10 @@ impl Coordinator {
|
|
|
438
446
|
let snapshot = state.clone();
|
|
439
447
|
let team = crate::state::projection::team_state_key(&snapshot);
|
|
440
448
|
let team_key = Some(crate::model::ids::TeamKey::new(team.clone()));
|
|
441
|
-
let session_name = state
|
|
449
|
+
let session_name = state
|
|
450
|
+
.get("session_name")
|
|
451
|
+
.and_then(Value::as_str)
|
|
452
|
+
.map(str::to_string);
|
|
442
453
|
// B-4 / 036b N36 三路可用 — sync_health 内 per-agent capture 失败本就降级
|
|
443
454
|
// (写 coordinator.agent_capture_failed 后 continue),不打断 deliver_pending
|
|
444
455
|
// 主干。但 contract 要求一条【tick 级】可观测的 step-failed 信号 —
|
|
@@ -447,13 +458,17 @@ impl Coordinator {
|
|
|
447
458
|
let mut had_capture_failure = false;
|
|
448
459
|
// P5 (C-P5-2): one list-windows per SESSION per tick — memoized across the
|
|
449
460
|
// agent loop instead of one fork per agent.
|
|
450
|
-
let mut windows_by_session: BTreeMap<
|
|
451
|
-
|
|
461
|
+
let mut windows_by_session: BTreeMap<
|
|
462
|
+
String,
|
|
463
|
+
Result<Vec<crate::transport::WindowName>, String>,
|
|
464
|
+
> = BTreeMap::new();
|
|
452
465
|
let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) else {
|
|
453
466
|
return Ok(captures);
|
|
454
467
|
};
|
|
455
468
|
for (agent_id, agent) in agents {
|
|
456
|
-
let Some((session, window, target)) =
|
|
469
|
+
let Some((session, window, target)) =
|
|
470
|
+
capture_window_target(agent, session_name.as_deref())
|
|
471
|
+
else {
|
|
457
472
|
continue;
|
|
458
473
|
};
|
|
459
474
|
let windows = match windows_by_session
|
|
@@ -535,7 +550,14 @@ impl Coordinator {
|
|
|
535
550
|
);
|
|
536
551
|
write_activity(agent, &activity, false);
|
|
537
552
|
let last_output_at = last_output_at_now;
|
|
538
|
-
write_agent_health(
|
|
553
|
+
write_agent_health(
|
|
554
|
+
store,
|
|
555
|
+
&team,
|
|
556
|
+
agent_id,
|
|
557
|
+
agent,
|
|
558
|
+
&activity,
|
|
559
|
+
last_output_at.as_deref(),
|
|
560
|
+
)?;
|
|
539
561
|
let pane_info = matching_capture_pane_info(agent, &session, &window, pane_infos);
|
|
540
562
|
let pane_id = pane_info
|
|
541
563
|
.as_ref()
|
|
@@ -547,7 +569,10 @@ impl Coordinator {
|
|
|
547
569
|
CapturedRuntimeFact {
|
|
548
570
|
team_key: team_key.clone(),
|
|
549
571
|
agent_id: AgentId::new(agent_id.clone()),
|
|
550
|
-
provider: agent
|
|
572
|
+
provider: agent
|
|
573
|
+
.get("provider")
|
|
574
|
+
.and_then(Value::as_str)
|
|
575
|
+
.and_then(parse_provider),
|
|
551
576
|
session_name: Some(session),
|
|
552
577
|
window: Some(window),
|
|
553
578
|
pane_id,
|
|
@@ -620,14 +645,22 @@ impl Coordinator {
|
|
|
620
645
|
let team = crate::state::projection::team_state_key(&snapshot);
|
|
621
646
|
let session_name = snapshot.get("session_name").and_then(Value::as_str);
|
|
622
647
|
for agent in abnormal_watch_agents(&snapshot) {
|
|
623
|
-
let rollout_path =
|
|
648
|
+
let rollout_path =
|
|
649
|
+
resolve_agent_rollout_path(self.workspace.as_path(), &agent.rollout_path);
|
|
624
650
|
let metadata = match std::fs::metadata(&rollout_path) {
|
|
625
651
|
Ok(metadata) => metadata,
|
|
626
652
|
Err(error) => {
|
|
627
653
|
upsert_abnormal_watch(
|
|
628
654
|
state,
|
|
629
655
|
&agent.agent_id,
|
|
630
|
-
abnormal_watch_payload(
|
|
656
|
+
abnormal_watch_payload(
|
|
657
|
+
&agent,
|
|
658
|
+
None,
|
|
659
|
+
None,
|
|
660
|
+
"unverifiable",
|
|
661
|
+
None,
|
|
662
|
+
Some(error.to_string()),
|
|
663
|
+
),
|
|
631
664
|
);
|
|
632
665
|
continue;
|
|
633
666
|
}
|
|
@@ -638,9 +671,10 @@ impl Coordinator {
|
|
|
638
671
|
// read at all (live sample: 332MB whole-file read per agent per 2s tick).
|
|
639
672
|
// ANY field change (including a size shrink / truncate) falls through to the
|
|
640
673
|
// re-read below.
|
|
641
|
-
if let (Some(mtime), Some(stored)) =
|
|
642
|
-
|
|
643
|
-
|
|
674
|
+
if let (Some(mtime), Some(stored)) = (
|
|
675
|
+
mtime_ns,
|
|
676
|
+
abnormal_watch_stored_metadata(&snapshot, &agent.agent_id),
|
|
677
|
+
) {
|
|
644
678
|
if stored == (size, mtime) {
|
|
645
679
|
continue;
|
|
646
680
|
}
|
|
@@ -654,17 +688,20 @@ impl Coordinator {
|
|
|
654
688
|
upsert_abnormal_watch(
|
|
655
689
|
state,
|
|
656
690
|
&agent.agent_id,
|
|
657
|
-
abnormal_watch_payload(
|
|
691
|
+
abnormal_watch_payload(
|
|
692
|
+
&agent,
|
|
693
|
+
Some(size),
|
|
694
|
+
mtime_ns,
|
|
695
|
+
"unverifiable",
|
|
696
|
+
None,
|
|
697
|
+
Some(error.to_string()),
|
|
698
|
+
),
|
|
658
699
|
);
|
|
659
700
|
continue;
|
|
660
701
|
}
|
|
661
702
|
};
|
|
662
|
-
let liveness =
|
|
663
|
-
&agent,
|
|
664
|
-
session_name,
|
|
665
|
-
targets,
|
|
666
|
-
self.transport.as_ref(),
|
|
667
|
-
);
|
|
703
|
+
let liveness =
|
|
704
|
+
agent_process_liveness(&agent, session_name, targets, self.transport.as_ref());
|
|
668
705
|
let fact = crate::provider::latest_explicit_error_fact(agent.provider, &text);
|
|
669
706
|
let decision = abnormal_exit_decision(liveness.state, fact.as_ref());
|
|
670
707
|
let check_key = abnormal_check_key(&agent, &liveness, fact.as_ref(), size);
|
|
@@ -680,8 +717,19 @@ impl Coordinator {
|
|
|
680
717
|
None,
|
|
681
718
|
),
|
|
682
719
|
);
|
|
683
|
-
if abnormal_last_check_key(state, &agent.agent_id).as_deref()
|
|
684
|
-
|
|
720
|
+
if abnormal_last_check_key(state, &agent.agent_id).as_deref()
|
|
721
|
+
!= Some(check_key.as_str())
|
|
722
|
+
{
|
|
723
|
+
write_abnormal_check(
|
|
724
|
+
event_log,
|
|
725
|
+
&team,
|
|
726
|
+
&agent,
|
|
727
|
+
&liveness,
|
|
728
|
+
fact.as_ref(),
|
|
729
|
+
decision,
|
|
730
|
+
size,
|
|
731
|
+
mtime_ns,
|
|
732
|
+
)?;
|
|
685
733
|
mark_abnormal_checked(state, &agent.agent_id, &check_key);
|
|
686
734
|
}
|
|
687
735
|
let fact = match (decision, fact) {
|
|
@@ -700,7 +748,9 @@ impl Coordinator {
|
|
|
700
748
|
(AbnormalExitDecision::Notify, None) => continue,
|
|
701
749
|
};
|
|
702
750
|
let dedupe_key = abnormal_dedupe_key(&agent, &fact, size);
|
|
703
|
-
if abnormal_last_notified_key(state, &agent.agent_id).as_deref()
|
|
751
|
+
if abnormal_last_notified_key(state, &agent.agent_id).as_deref()
|
|
752
|
+
== Some(dedupe_key.as_str())
|
|
753
|
+
{
|
|
704
754
|
continue;
|
|
705
755
|
}
|
|
706
756
|
let content = format_abnormal_exit_message(&team, &agent, &fact, &liveness, size);
|
|
@@ -755,7 +805,10 @@ impl Coordinator {
|
|
|
755
805
|
}
|
|
756
806
|
|
|
757
807
|
fn handle_startup_prompts(&self, state: &mut Value, event_log: &EventLog) {
|
|
758
|
-
let session_name = state
|
|
808
|
+
let session_name = state
|
|
809
|
+
.get("session_name")
|
|
810
|
+
.and_then(Value::as_str)
|
|
811
|
+
.map(str::to_string);
|
|
759
812
|
let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) else {
|
|
760
813
|
return;
|
|
761
814
|
};
|
|
@@ -827,7 +880,10 @@ impl Coordinator {
|
|
|
827
880
|
continue;
|
|
828
881
|
};
|
|
829
882
|
agent_obj.insert("startup_prompts".to_string(), serde_json::json!("handled"));
|
|
830
|
-
agent_obj.insert(
|
|
883
|
+
agent_obj.insert(
|
|
884
|
+
"startup_prompt_status".to_string(),
|
|
885
|
+
serde_json::json!("handled"),
|
|
886
|
+
);
|
|
831
887
|
agent_obj.insert("startup_prompt_handled".to_string(), handled_payload);
|
|
832
888
|
}
|
|
833
889
|
}
|
|
@@ -891,7 +947,10 @@ impl Coordinator {
|
|
|
891
947
|
) -> Result<(), TickError> {
|
|
892
948
|
let snapshot = state.clone();
|
|
893
949
|
let team = crate::state::projection::team_state_key(&snapshot);
|
|
894
|
-
let session_name = snapshot
|
|
950
|
+
let session_name = snapshot
|
|
951
|
+
.get("session_name")
|
|
952
|
+
.and_then(Value::as_str)
|
|
953
|
+
.map(str::to_string);
|
|
895
954
|
let mut dedup_updates = Vec::new();
|
|
896
955
|
{
|
|
897
956
|
let Some(agents) = state.get_mut("agents").and_then(Value::as_object_mut) else {
|
|
@@ -942,32 +1001,32 @@ impl Coordinator {
|
|
|
942
1001
|
});
|
|
943
1002
|
let choice = choose_internal_mcp_approval_choice(&prompt);
|
|
944
1003
|
let keys = approval_choice_keys(&prompt, &captured.text, &choice)
|
|
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
|
-
|
|
1004
|
+
.into_iter()
|
|
1005
|
+
.filter_map(runtime_approval_key)
|
|
1006
|
+
.collect::<Vec<_>>();
|
|
1007
|
+
// A-6 / Python approvals/runtime_prompts.py:21-43: prompts are handled
|
|
1008
|
+
// per-agent with run_cmd(check=False) — one agent's tmux failure must
|
|
1009
|
+
// not abort the whole tick for the rest.
|
|
1010
|
+
if let Err(error) = self.transport.send_keys(&target, &keys) {
|
|
1011
|
+
event_log.write(
|
|
1012
|
+
"runtime_approval.send_keys_failed",
|
|
1013
|
+
serde_json::json!({
|
|
1014
|
+
"agent_id": agent_id,
|
|
1015
|
+
"target": format!("{target:?}"),
|
|
1016
|
+
"tool": prompt.tool,
|
|
1017
|
+
"error": error.to_string(),
|
|
1018
|
+
}),
|
|
1019
|
+
)?;
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
let after = self
|
|
1023
|
+
.transport
|
|
1024
|
+
.capture(&target, crate::transport::CaptureRange::Tail(80))
|
|
1025
|
+
.ok()
|
|
1026
|
+
.and_then(|capture| extract_approval_prompt(agent_id, &capture.text));
|
|
1027
|
+
let cleared = after.as_ref().is_none_or(|after| {
|
|
1028
|
+
after.prompt != prompt.prompt || after.tool != prompt.tool
|
|
1029
|
+
});
|
|
971
1030
|
event_log.write(
|
|
972
1031
|
"runtime_approval.auto_approved",
|
|
973
1032
|
serde_json::json!({
|
|
@@ -1001,14 +1060,16 @@ impl Coordinator {
|
|
|
1001
1060
|
)?;
|
|
1002
1061
|
}
|
|
1003
1062
|
RuntimeApprovalDecision::AwaitingHumanConfirm => {
|
|
1004
|
-
let Some(reason) =
|
|
1063
|
+
let Some(reason) =
|
|
1064
|
+
awaiting_human_confirm_reason(&prompt, auto_answer_allowed)
|
|
1065
|
+
else {
|
|
1005
1066
|
continue;
|
|
1006
1067
|
};
|
|
1007
1068
|
let fact = awaiting_human_confirm_fact(&team, agent_id, &prompt, reason);
|
|
1008
1069
|
let previous = agent
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1070
|
+
.get("awaiting_human_confirm")
|
|
1071
|
+
.and_then(|v| v.get("fingerprint"))
|
|
1072
|
+
.and_then(Value::as_str);
|
|
1012
1073
|
if previous == Some(fact.fingerprint.as_str())
|
|
1013
1074
|
|| state_awaiting_human_confirm_fingerprint(&snapshot, &team, agent_id)
|
|
1014
1075
|
.as_deref()
|
|
@@ -1020,10 +1081,10 @@ impl Coordinator {
|
|
|
1020
1081
|
let notification = awaiting_human_confirm_payload(agent, &fact);
|
|
1021
1082
|
let content = notification.to_string();
|
|
1022
1083
|
let _ = crate::messaging::send_to_leader_receiver(
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1084
|
+
self.workspace.as_path(),
|
|
1085
|
+
&snapshot,
|
|
1086
|
+
"leader",
|
|
1087
|
+
&content,
|
|
1027
1088
|
None,
|
|
1028
1089
|
agent_id,
|
|
1029
1090
|
false,
|
|
@@ -1034,43 +1095,43 @@ impl Coordinator {
|
|
|
1034
1095
|
remember_awaiting_human_confirm(agent, &fact);
|
|
1035
1096
|
dedup_updates.push(AwaitingDedupUpdate::Remember(fact.clone()));
|
|
1036
1097
|
match reason {
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1098
|
+
"tool_not_allowlisted" => {
|
|
1099
|
+
event_log.write(
|
|
1100
|
+
"runtime_approval.tool_not_allowlisted",
|
|
1101
|
+
serde_json::json!({
|
|
1102
|
+
"agent_id": agent_id,
|
|
1103
|
+
"tool": prompt.tool,
|
|
1104
|
+
"kind": prompt.kind,
|
|
1105
|
+
"prompt": prompt.prompt,
|
|
1106
|
+
}),
|
|
1107
|
+
)?;
|
|
1108
|
+
}
|
|
1109
|
+
"leader_restricted" | "leader_safety_restricted" => {
|
|
1110
|
+
event_log.write(
|
|
1111
|
+
"runtime_approval.blocked_by_leader_safety",
|
|
1112
|
+
serde_json::json!({
|
|
1113
|
+
"agent_id": agent_id,
|
|
1114
|
+
"tool": prompt.tool,
|
|
1115
|
+
"command": prompt.command,
|
|
1116
|
+
"kind": prompt.kind,
|
|
1117
|
+
"prompt": prompt.prompt,
|
|
1118
|
+
}),
|
|
1119
|
+
)?;
|
|
1120
|
+
}
|
|
1121
|
+
"command_approval_requires_human" => {
|
|
1122
|
+
event_log.write(
|
|
1123
|
+
"runtime_approval.command_approval_requires_human",
|
|
1124
|
+
serde_json::json!({
|
|
1125
|
+
"agent_id": agent_id,
|
|
1126
|
+
"tool": prompt.tool,
|
|
1127
|
+
"command": prompt.command,
|
|
1128
|
+
"kind": prompt.kind,
|
|
1129
|
+
"prompt": prompt.prompt,
|
|
1130
|
+
}),
|
|
1131
|
+
)?;
|
|
1132
|
+
}
|
|
1133
|
+
_ => {}
|
|
1047
1134
|
}
|
|
1048
|
-
"leader_restricted" | "leader_safety_restricted" => {
|
|
1049
|
-
event_log.write(
|
|
1050
|
-
"runtime_approval.blocked_by_leader_safety",
|
|
1051
|
-
serde_json::json!({
|
|
1052
|
-
"agent_id": agent_id,
|
|
1053
|
-
"tool": prompt.tool,
|
|
1054
|
-
"command": prompt.command,
|
|
1055
|
-
"kind": prompt.kind,
|
|
1056
|
-
"prompt": prompt.prompt,
|
|
1057
|
-
}),
|
|
1058
|
-
)?;
|
|
1059
|
-
}
|
|
1060
|
-
"command_approval_requires_human" => {
|
|
1061
|
-
event_log.write(
|
|
1062
|
-
"runtime_approval.command_approval_requires_human",
|
|
1063
|
-
serde_json::json!({
|
|
1064
|
-
"agent_id": agent_id,
|
|
1065
|
-
"tool": prompt.tool,
|
|
1066
|
-
"command": prompt.command,
|
|
1067
|
-
"kind": prompt.kind,
|
|
1068
|
-
"prompt": prompt.prompt,
|
|
1069
|
-
}),
|
|
1070
|
-
)?;
|
|
1071
|
-
}
|
|
1072
|
-
_ => {}
|
|
1073
|
-
}
|
|
1074
1135
|
}
|
|
1075
1136
|
RuntimeApprovalDecision::Ignore => {
|
|
1076
1137
|
clear_awaiting_human_confirm(agent);
|
|
@@ -1084,7 +1145,9 @@ impl Coordinator {
|
|
|
1084
1145
|
}
|
|
1085
1146
|
for update in dedup_updates {
|
|
1086
1147
|
match update {
|
|
1087
|
-
AwaitingDedupUpdate::Remember(fact) =>
|
|
1148
|
+
AwaitingDedupUpdate::Remember(fact) => {
|
|
1149
|
+
remember_state_awaiting_human_confirm(state, &fact)
|
|
1150
|
+
}
|
|
1088
1151
|
AwaitingDedupUpdate::Clear { team, agent_id } => {
|
|
1089
1152
|
clear_state_awaiting_human_confirm(state, &team, &agent_id)
|
|
1090
1153
|
}
|
|
@@ -1126,7 +1189,9 @@ impl Coordinator {
|
|
|
1126
1189
|
/// Python 是 `python -m team_agent.coordinator`,`lifecycle.py:108`)。
|
|
1127
1190
|
/// **schema 兼容门**:三元任一不匹配 → restart_incompatible,**不可静默继续**(card §89)。
|
|
1128
1191
|
pub fn start(&self) -> Result<StartReport, StartError> {
|
|
1129
|
-
let health = self
|
|
1192
|
+
let health = self
|
|
1193
|
+
.health()
|
|
1194
|
+
.map_err(|e| std::io::Error::other(e.to_string()))?;
|
|
1130
1195
|
if health.ok {
|
|
1131
1196
|
return Ok(StartReport {
|
|
1132
1197
|
ok: true,
|
|
@@ -1164,14 +1229,26 @@ impl Coordinator {
|
|
|
1164
1229
|
pub fn stop(&self) -> Result<StopReport, StopError> {
|
|
1165
1230
|
let pid_path = coordinator_pid_path(&self.workspace);
|
|
1166
1231
|
if !pid_path.exists() {
|
|
1167
|
-
return Ok(StopReport {
|
|
1232
|
+
return Ok(StopReport {
|
|
1233
|
+
ok: true,
|
|
1234
|
+
status: StopOutcome::Missing,
|
|
1235
|
+
pid: None,
|
|
1236
|
+
});
|
|
1168
1237
|
}
|
|
1169
1238
|
let pid = read_pid_file(&pid_path);
|
|
1170
1239
|
remove_file_if_exists(&pid_path)?;
|
|
1171
1240
|
remove_file_if_exists(&coordinator_meta_path(&self.workspace))?;
|
|
1172
1241
|
match pid {
|
|
1173
|
-
Some(pid) => Ok(StopReport {
|
|
1174
|
-
|
|
1242
|
+
Some(pid) => Ok(StopReport {
|
|
1243
|
+
ok: true,
|
|
1244
|
+
status: StopOutcome::Stopped,
|
|
1245
|
+
pid: Some(pid),
|
|
1246
|
+
}),
|
|
1247
|
+
None => Ok(StopReport {
|
|
1248
|
+
ok: true,
|
|
1249
|
+
status: StopOutcome::InvalidPidRemoved,
|
|
1250
|
+
pid: None,
|
|
1251
|
+
}),
|
|
1175
1252
|
}
|
|
1176
1253
|
}
|
|
1177
1254
|
|
|
@@ -1236,20 +1313,16 @@ fn empty_tick_report(
|
|
|
1236
1313
|
reason: Option<TickStopReason>,
|
|
1237
1314
|
persisted: Option<bool>,
|
|
1238
1315
|
) -> TickReport {
|
|
1239
|
-
base_tick_report(
|
|
1240
|
-
ok,
|
|
1241
|
-
stop,
|
|
1242
|
-
reason,
|
|
1243
|
-
persisted,
|
|
1244
|
-
TickCollections::default(),
|
|
1245
|
-
)
|
|
1316
|
+
base_tick_report(ok, stop, reason, persisted, TickCollections::default())
|
|
1246
1317
|
}
|
|
1247
1318
|
|
|
1248
1319
|
fn collect_results(value: Value) -> Vec<CollectedResult> {
|
|
1249
1320
|
let Some(result_id) = value.get("result_id").and_then(Value::as_str) else {
|
|
1250
1321
|
return Vec::new();
|
|
1251
1322
|
};
|
|
1252
|
-
vec![CollectedResult {
|
|
1323
|
+
vec![CollectedResult {
|
|
1324
|
+
result_id: result_id.to_string(),
|
|
1325
|
+
}]
|
|
1253
1326
|
}
|
|
1254
1327
|
|
|
1255
1328
|
struct ProviderTurnClassifier;
|
|
@@ -1282,8 +1355,7 @@ impl TurnStateClassifier for ProviderTurnClassifier {
|
|
|
1282
1355
|
/// `coordinator.coordinator_tick_iteration_count` load fine (read-compat, C-P3-3) —
|
|
1283
1356
|
/// new versions simply stop writing it.
|
|
1284
1357
|
fn increment_coordinator_tick_iteration_count(workspace: &WorkspacePath) {
|
|
1285
|
-
let path =
|
|
1286
|
-
crate::model::paths::runtime_dir(workspace.as_path()).join("coordinator_tick.json");
|
|
1358
|
+
let path = crate::model::paths::runtime_dir(workspace.as_path()).join("coordinator_tick.json");
|
|
1287
1359
|
let next = std::fs::read_to_string(&path)
|
|
1288
1360
|
.ok()
|
|
1289
1361
|
.and_then(|text| serde_json::from_str::<Value>(&text).ok())
|
|
@@ -1423,13 +1495,13 @@ fn abnormal_watch_agents(state: &Value) -> Vec<AbnormalWatchAgent> {
|
|
|
1423
1495
|
agents
|
|
1424
1496
|
.iter()
|
|
1425
1497
|
.filter_map(|(agent_id, agent)| {
|
|
1426
|
-
if matches!(
|
|
1427
|
-
agent.get("status").and_then(Value::as_str),
|
|
1428
|
-
Some("paused")
|
|
1429
|
-
) {
|
|
1498
|
+
if matches!(agent.get("status").and_then(Value::as_str), Some("paused")) {
|
|
1430
1499
|
return None;
|
|
1431
1500
|
}
|
|
1432
|
-
let provider = agent
|
|
1501
|
+
let provider = agent
|
|
1502
|
+
.get("provider")
|
|
1503
|
+
.and_then(Value::as_str)
|
|
1504
|
+
.and_then(parse_provider)?;
|
|
1433
1505
|
let rollout_path_display = ["rollout_path", "transcript_path", "session_log_path"]
|
|
1434
1506
|
.into_iter()
|
|
1435
1507
|
.find_map(|key| agent.get(key).and_then(Value::as_str))
|
|
@@ -1440,10 +1512,19 @@ fn abnormal_watch_agents(state: &Value) -> Vec<AbnormalWatchAgent> {
|
|
|
1440
1512
|
provider,
|
|
1441
1513
|
rollout_path: PathBuf::from(&rollout_path_display),
|
|
1442
1514
|
rollout_path_display,
|
|
1443
|
-
status: agent
|
|
1515
|
+
status: agent
|
|
1516
|
+
.get("status")
|
|
1517
|
+
.and_then(Value::as_str)
|
|
1518
|
+
.map(str::to_string),
|
|
1444
1519
|
process_liveness: explicit_process_liveness(agent),
|
|
1445
|
-
window: agent
|
|
1446
|
-
|
|
1520
|
+
window: agent
|
|
1521
|
+
.get("window")
|
|
1522
|
+
.and_then(Value::as_str)
|
|
1523
|
+
.map(str::to_string),
|
|
1524
|
+
pane_id: agent
|
|
1525
|
+
.get("pane_id")
|
|
1526
|
+
.and_then(Value::as_str)
|
|
1527
|
+
.map(str::to_string),
|
|
1447
1528
|
pid: agent_pid(agent),
|
|
1448
1529
|
current_command: agent
|
|
1449
1530
|
.get("pane_current_command")
|
|
@@ -1462,12 +1543,19 @@ fn agent_pid(agent: &Value) -> Option<Pid> {
|
|
|
1462
1543
|
}
|
|
1463
1544
|
|
|
1464
1545
|
fn explicit_process_liveness(agent: &Value) -> Option<ProcessLiveness> {
|
|
1465
|
-
if let Some(process) = agent
|
|
1546
|
+
if let Some(process) = agent
|
|
1547
|
+
.get("provider_process")
|
|
1548
|
+
.or_else(|| agent.get("process"))
|
|
1549
|
+
{
|
|
1466
1550
|
if let Some(liveness) = explicit_process_liveness(process) {
|
|
1467
1551
|
return Some(liveness);
|
|
1468
1552
|
}
|
|
1469
1553
|
}
|
|
1470
|
-
for key in [
|
|
1554
|
+
for key in [
|
|
1555
|
+
"provider_process_liveness",
|
|
1556
|
+
"process_liveness",
|
|
1557
|
+
"pane_liveness",
|
|
1558
|
+
] {
|
|
1471
1559
|
match agent.get(key).and_then(Value::as_str) {
|
|
1472
1560
|
Some("dead") => return Some(ProcessLiveness::Dead),
|
|
1473
1561
|
Some("alive" | "live") => return Some(ProcessLiveness::Alive),
|
|
@@ -1475,14 +1563,32 @@ fn explicit_process_liveness(agent: &Value) -> Option<ProcessLiveness> {
|
|
|
1475
1563
|
_ => {}
|
|
1476
1564
|
}
|
|
1477
1565
|
}
|
|
1478
|
-
for key in [
|
|
1566
|
+
for key in [
|
|
1567
|
+
"provider_process_alive",
|
|
1568
|
+
"process_alive",
|
|
1569
|
+
"provider_alive",
|
|
1570
|
+
"alive",
|
|
1571
|
+
] {
|
|
1479
1572
|
if let Some(alive) = agent.get(key).and_then(Value::as_bool) {
|
|
1480
|
-
return Some(if alive {
|
|
1573
|
+
return Some(if alive {
|
|
1574
|
+
ProcessLiveness::Alive
|
|
1575
|
+
} else {
|
|
1576
|
+
ProcessLiveness::Dead
|
|
1577
|
+
});
|
|
1481
1578
|
}
|
|
1482
1579
|
}
|
|
1483
|
-
for key in [
|
|
1580
|
+
for key in [
|
|
1581
|
+
"provider_process_dead",
|
|
1582
|
+
"process_dead",
|
|
1583
|
+
"provider_dead",
|
|
1584
|
+
"dead",
|
|
1585
|
+
] {
|
|
1484
1586
|
if let Some(dead) = agent.get(key).and_then(Value::as_bool) {
|
|
1485
|
-
return Some(if dead {
|
|
1587
|
+
return Some(if dead {
|
|
1588
|
+
ProcessLiveness::Dead
|
|
1589
|
+
} else {
|
|
1590
|
+
ProcessLiveness::Alive
|
|
1591
|
+
});
|
|
1486
1592
|
}
|
|
1487
1593
|
}
|
|
1488
1594
|
for key in ["status", "state", "liveness"] {
|
|
@@ -1500,7 +1606,10 @@ fn explicit_process_liveness(agent: &Value) -> Option<ProcessLiveness> {
|
|
|
1500
1606
|
|
|
1501
1607
|
fn json_u32(value: Option<&Value>) -> Option<u32> {
|
|
1502
1608
|
value
|
|
1503
|
-
.and_then(|v|
|
|
1609
|
+
.and_then(|v| {
|
|
1610
|
+
v.as_u64()
|
|
1611
|
+
.or_else(|| v.as_i64().and_then(|n| u64::try_from(n).ok()))
|
|
1612
|
+
})
|
|
1504
1613
|
.and_then(|n| u32::try_from(n).ok())
|
|
1505
1614
|
}
|
|
1506
1615
|
|
|
@@ -1514,15 +1623,17 @@ fn agent_process_liveness(
|
|
|
1514
1623
|
return pid_process_check("pid", pid);
|
|
1515
1624
|
}
|
|
1516
1625
|
if let Some(liveness) = agent.process_liveness {
|
|
1517
|
-
return process_check(
|
|
1626
|
+
return process_check(
|
|
1627
|
+
liveness,
|
|
1628
|
+
format!("explicit:{}", process_liveness_wire(liveness)),
|
|
1629
|
+
);
|
|
1518
1630
|
}
|
|
1519
1631
|
if agent.status.as_deref().is_some_and(|status| {
|
|
1520
1632
|
matches!(
|
|
1521
1633
|
status,
|
|
1522
1634
|
"stopped" | "missing" | "error" | "dead" | "exited" | "terminated" | "crashed"
|
|
1523
1635
|
)
|
|
1524
|
-
})
|
|
1525
|
-
{
|
|
1636
|
+
}) {
|
|
1526
1637
|
return process_check(
|
|
1527
1638
|
ProcessLiveness::Dead,
|
|
1528
1639
|
format!("status:{}", agent.status.as_deref().unwrap_or("unknown")),
|
|
@@ -1538,7 +1649,10 @@ fn agent_process_liveness(
|
|
|
1538
1649
|
if let Some(pid) = target.pane_pid.map(Pid::new) {
|
|
1539
1650
|
return pid_process_check("pane_pid", pid);
|
|
1540
1651
|
}
|
|
1541
|
-
return process_check(
|
|
1652
|
+
return process_check(
|
|
1653
|
+
ProcessLiveness::Unverifiable,
|
|
1654
|
+
"pane_present_pid_unknown".to_string(),
|
|
1655
|
+
);
|
|
1542
1656
|
}
|
|
1543
1657
|
if let Some(pane_id) = agent.pane_id.as_deref() {
|
|
1544
1658
|
let pane = crate::transport::PaneId::new(pane_id);
|
|
@@ -1546,27 +1660,37 @@ fn agent_process_liveness(
|
|
|
1546
1660
|
Ok(crate::transport::PaneLiveness::Dead) => {
|
|
1547
1661
|
process_check(ProcessLiveness::Dead, format!("pane_dead:{pane_id}"))
|
|
1548
1662
|
}
|
|
1549
|
-
Ok(crate::transport::PaneLiveness::Live) =>
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1663
|
+
Ok(crate::transport::PaneLiveness::Live) => process_check(
|
|
1664
|
+
ProcessLiveness::Unverifiable,
|
|
1665
|
+
format!("pane_live_pid_unknown:{pane_id}"),
|
|
1666
|
+
),
|
|
1667
|
+
Ok(crate::transport::PaneLiveness::Unknown) => process_check(
|
|
1668
|
+
ProcessLiveness::Unverifiable,
|
|
1669
|
+
format!("pane_unknown:{pane_id}"),
|
|
1670
|
+
),
|
|
1671
|
+
Err(error) => process_check(
|
|
1672
|
+
ProcessLiveness::Unverifiable,
|
|
1673
|
+
format!("pane_unverifiable:{pane_id}:{error}"),
|
|
1674
|
+
),
|
|
1558
1675
|
};
|
|
1559
1676
|
}
|
|
1560
1677
|
let (Some(session), Some(window)) = (session_name, agent.window.as_deref()) else {
|
|
1561
|
-
return process_check(
|
|
1678
|
+
return process_check(
|
|
1679
|
+
ProcessLiveness::Unverifiable,
|
|
1680
|
+
"missing_session_or_window".to_string(),
|
|
1681
|
+
);
|
|
1562
1682
|
};
|
|
1563
1683
|
let session = crate::transport::SessionName::new(session);
|
|
1564
1684
|
match transport.list_windows(&session) {
|
|
1565
|
-
Ok(windows) if windows.iter().any(|known| known.as_str() == window) =>
|
|
1566
|
-
|
|
1567
|
-
|
|
1685
|
+
Ok(windows) if windows.iter().any(|known| known.as_str() == window) => process_check(
|
|
1686
|
+
ProcessLiveness::Unverifiable,
|
|
1687
|
+
"window_present_pid_unknown".to_string(),
|
|
1688
|
+
),
|
|
1568
1689
|
Ok(_) => process_check(ProcessLiveness::Dead, format!("window_missing:{window}")),
|
|
1569
|
-
Err(error) => process_check(
|
|
1690
|
+
Err(error) => process_check(
|
|
1691
|
+
ProcessLiveness::Unverifiable,
|
|
1692
|
+
format!("window_unverifiable:{window}:{error}"),
|
|
1693
|
+
),
|
|
1570
1694
|
}
|
|
1571
1695
|
}
|
|
1572
1696
|
|
|
@@ -1576,7 +1700,10 @@ fn matching_agent_target<'a>(
|
|
|
1576
1700
|
targets: &'a [crate::transport::PaneInfo],
|
|
1577
1701
|
) -> Option<&'a crate::transport::PaneInfo> {
|
|
1578
1702
|
if let Some(pane_id) = agent.pane_id.as_deref() {
|
|
1579
|
-
if let Some(target) = targets
|
|
1703
|
+
if let Some(target) = targets
|
|
1704
|
+
.iter()
|
|
1705
|
+
.find(|target| target.pane_id.as_str() == pane_id)
|
|
1706
|
+
{
|
|
1580
1707
|
return Some(target);
|
|
1581
1708
|
}
|
|
1582
1709
|
}
|
|
@@ -1596,7 +1723,10 @@ fn pid_process_check(label: &str, pid: Pid) -> ProcessCheck {
|
|
|
1596
1723
|
match pid_is_running(pid) {
|
|
1597
1724
|
Ok(true) => process_check(ProcessLiveness::Alive, format!("{label}_running:{pid}")),
|
|
1598
1725
|
Ok(false) => process_check(ProcessLiveness::Dead, format!("{label}_not_running:{pid}")),
|
|
1599
|
-
Err(error) => process_check(
|
|
1726
|
+
Err(error) => process_check(
|
|
1727
|
+
ProcessLiveness::Unverifiable,
|
|
1728
|
+
format!("{label}_unverifiable:{pid}:{error}"),
|
|
1729
|
+
),
|
|
1600
1730
|
}
|
|
1601
1731
|
}
|
|
1602
1732
|
|
|
@@ -1604,7 +1734,10 @@ fn command_process_check(provider: crate::model::enums::Provider, command: &str)
|
|
|
1604
1734
|
if provider_command_matches(provider, command) {
|
|
1605
1735
|
process_check(ProcessLiveness::Alive, format!("current_command:{command}"))
|
|
1606
1736
|
} else {
|
|
1607
|
-
process_check(
|
|
1737
|
+
process_check(
|
|
1738
|
+
ProcessLiveness::Dead,
|
|
1739
|
+
format!("provider_not_foreground:{command}"),
|
|
1740
|
+
)
|
|
1608
1741
|
}
|
|
1609
1742
|
}
|
|
1610
1743
|
|
|
@@ -1803,7 +1936,10 @@ fn mark_abnormal_notified(state: &mut Value, agent_id: &str, key: &str) {
|
|
|
1803
1936
|
}
|
|
1804
1937
|
if let Some(obj) = entry.as_object_mut() {
|
|
1805
1938
|
obj.insert("last_notified_key".to_string(), serde_json::json!(key));
|
|
1806
|
-
obj.insert(
|
|
1939
|
+
obj.insert(
|
|
1940
|
+
"last_notified_at".to_string(),
|
|
1941
|
+
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
1942
|
+
);
|
|
1807
1943
|
}
|
|
1808
1944
|
}
|
|
1809
1945
|
}
|
|
@@ -1818,7 +1954,10 @@ fn mark_abnormal_suppressed(state: &mut Value, agent_id: &str, key: &str) {
|
|
|
1818
1954
|
}
|
|
1819
1955
|
if let Some(obj) = entry.as_object_mut() {
|
|
1820
1956
|
obj.insert("last_suppressed_key".to_string(), serde_json::json!(key));
|
|
1821
|
-
obj.insert(
|
|
1957
|
+
obj.insert(
|
|
1958
|
+
"last_suppressed_at".to_string(),
|
|
1959
|
+
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
1960
|
+
);
|
|
1822
1961
|
}
|
|
1823
1962
|
}
|
|
1824
1963
|
}
|
|
@@ -1855,7 +1994,10 @@ fn mark_abnormal_checked(state: &mut Value, agent_id: &str, key: &str) {
|
|
|
1855
1994
|
}
|
|
1856
1995
|
if let Some(obj) = entry.as_object_mut() {
|
|
1857
1996
|
obj.insert("last_check_key".to_string(), serde_json::json!(key));
|
|
1858
|
-
obj.insert(
|
|
1997
|
+
obj.insert(
|
|
1998
|
+
"last_check_at".to_string(),
|
|
1999
|
+
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
2000
|
+
);
|
|
1859
2001
|
}
|
|
1860
2002
|
}
|
|
1861
2003
|
}
|
|
@@ -2043,7 +2185,10 @@ fn capture_window_target(
|
|
|
2043
2185
|
crate::transport::WindowName,
|
|
2044
2186
|
crate::transport::Target,
|
|
2045
2187
|
)> {
|
|
2046
|
-
let window = agent
|
|
2188
|
+
let window = agent
|
|
2189
|
+
.get("window")
|
|
2190
|
+
.and_then(Value::as_str)
|
|
2191
|
+
.filter(|s| !s.is_empty())?;
|
|
2047
2192
|
let session = session_name.filter(|s| !s.is_empty())?;
|
|
2048
2193
|
let session = crate::transport::SessionName::new(session);
|
|
2049
2194
|
let window = crate::transport::WindowName::new(window);
|
|
@@ -2093,13 +2238,18 @@ fn agent_rollout_path(agent: &Value) -> Option<PathBuf> {
|
|
|
2093
2238
|
.map(PathBuf::from)
|
|
2094
2239
|
}
|
|
2095
2240
|
|
|
2096
|
-
fn runtime_approval_target(
|
|
2241
|
+
fn runtime_approval_target(
|
|
2242
|
+
agent: &Value,
|
|
2243
|
+
session_name: Option<&str>,
|
|
2244
|
+
) -> Option<crate::transport::Target> {
|
|
2097
2245
|
if let Some(pane_id) = agent
|
|
2098
2246
|
.get("pane_id")
|
|
2099
2247
|
.and_then(Value::as_str)
|
|
2100
2248
|
.filter(|pane_id| !pane_id.is_empty())
|
|
2101
2249
|
{
|
|
2102
|
-
return Some(crate::transport::Target::Pane(
|
|
2250
|
+
return Some(crate::transport::Target::Pane(
|
|
2251
|
+
crate::transport::PaneId::new(pane_id),
|
|
2252
|
+
));
|
|
2103
2253
|
}
|
|
2104
2254
|
capture_window_target(agent, session_name).map(|(_, _, target)| target)
|
|
2105
2255
|
}
|
|
@@ -2200,7 +2350,14 @@ fn awaiting_human_confirm_payload(
|
|
|
2200
2350
|
fact: &crate::provider::AwaitingHumanConfirmFact,
|
|
2201
2351
|
) -> Value {
|
|
2202
2352
|
let mut payload = fact.to_event_payload();
|
|
2203
|
-
let excerpt = fact
|
|
2353
|
+
let excerpt = fact
|
|
2354
|
+
.prompt
|
|
2355
|
+
.lines()
|
|
2356
|
+
.next()
|
|
2357
|
+
.unwrap_or("")
|
|
2358
|
+
.chars()
|
|
2359
|
+
.take(240)
|
|
2360
|
+
.collect::<String>();
|
|
2204
2361
|
if let Some(obj) = payload.as_object_mut() {
|
|
2205
2362
|
obj.insert("team_id".to_string(), serde_json::json!(fact.team));
|
|
2206
2363
|
obj.insert("owner_team_id".to_string(), serde_json::json!(fact.team));
|
|
@@ -2363,7 +2520,10 @@ fn write_activity(
|
|
|
2363
2520
|
activity: &crate::messaging::AgentActivity,
|
|
2364
2521
|
output_advanced: bool,
|
|
2365
2522
|
) -> Option<String> {
|
|
2366
|
-
let previous_last_output = agent
|
|
2523
|
+
let previous_last_output = agent
|
|
2524
|
+
.get("last_output_at")
|
|
2525
|
+
.and_then(Value::as_str)
|
|
2526
|
+
.map(str::to_string);
|
|
2367
2527
|
let Some(agent_obj) = agent.as_object_mut() else {
|
|
2368
2528
|
return previous_last_output;
|
|
2369
2529
|
};
|