@team-agent/installer 0.3.12 → 0.3.14
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/adapters.rs +23 -0
- package/crates/team-agent/src/cli/leader.rs +2 -5
- package/crates/team-agent/src/cli/mod.rs +258 -79
- package/crates/team-agent/src/cli/tests/compile.rs +69 -0
- package/crates/team-agent/src/cli/tests/main_preserved.rs +68 -0
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +160 -2
- package/crates/team-agent/src/compiler.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +16 -83
- package/crates/team-agent/src/leader/lease.rs +4 -20
- package/crates/team-agent/src/leader/mod.rs +3 -1
- package/crates/team-agent/src/leader/owner_bind.rs +46 -48
- package/crates/team-agent/src/leader/provider_attribution.rs +214 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -3
- package/crates/team-agent/src/leader/rediscover.rs +34 -17
- package/crates/team-agent/src/leader/start.rs +279 -4
- package/crates/team-agent/src/lifecycle/launch.rs +279 -24
- package/crates/team-agent/src/lifecycle/restart/common.rs +50 -40
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +343 -49
- package/crates/team-agent/src/lifecycle/restart/selection.rs +9 -6
- package/crates/team-agent/src/lifecycle/tests/core.rs +181 -45
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +390 -61
- package/crates/team-agent/src/lifecycle/types.rs +28 -0
- package/crates/team-agent/src/messaging/delivery.rs +5 -1
- package/crates/team-agent/src/messaging/helpers.rs +1 -1
- package/crates/team-agent/src/messaging/tests/runtime.rs +14 -0
- package/crates/team-agent/src/provider/adapter.rs +6 -3
- package/crates/team-agent/src/provider/startup_prompt.rs +173 -0
- package/crates/team-agent/src/provider/testdata/copilot-ready-marker.txt +5 -0
- package/crates/team-agent/src/provider/testdata/copilot-trust-prompt.txt +19 -0
- package/crates/team-agent/src/tmux_backend.rs +5 -0
- package/crates/team-agent/src/transport/test_support.rs +22 -7
- package/crates/team-agent/src/transport.rs +4 -0
- package/package.json +4 -4
|
@@ -9,7 +9,7 @@ use crate::model::ids::AgentId;
|
|
|
9
9
|
use crate::model::permissions::{self, AgentPermissionInput};
|
|
10
10
|
use crate::model::yaml::{self, Value};
|
|
11
11
|
use crate::state::persist::{load_runtime_state, save_runtime_state};
|
|
12
|
-
use crate::transport::{SessionName, Target, Transport, WindowName};
|
|
12
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
13
13
|
|
|
14
14
|
use super::*;
|
|
15
15
|
|
|
@@ -595,7 +595,7 @@ fn merge_workspace_team_state_with_key(
|
|
|
595
595
|
launched_key: &str,
|
|
596
596
|
) -> serde_json::Value {
|
|
597
597
|
let mut launched_obj = launched.as_object().cloned().unwrap_or_default();
|
|
598
|
-
let mut teams =
|
|
598
|
+
let mut teams = existing
|
|
599
599
|
.get("teams")
|
|
600
600
|
.and_then(serde_json::Value::as_object)
|
|
601
601
|
.cloned()
|
|
@@ -638,6 +638,41 @@ fn merge_workspace_team_state_with_key(
|
|
|
638
638
|
serde_json::Value::Object(merged)
|
|
639
639
|
}
|
|
640
640
|
|
|
641
|
+
#[cfg(test)]
|
|
642
|
+
mod merge_workspace_team_state_with_key_tests {
|
|
643
|
+
use super::*;
|
|
644
|
+
use serde_json::json;
|
|
645
|
+
|
|
646
|
+
#[test]
|
|
647
|
+
fn empty_top_level_existing_session_preserves_existing_teams() {
|
|
648
|
+
let existing = json!({
|
|
649
|
+
"session_name": "",
|
|
650
|
+
"active_team_key": "parent",
|
|
651
|
+
"teams": {
|
|
652
|
+
"parent": {
|
|
653
|
+
"session_name": "team-parent",
|
|
654
|
+
"agents": {"parent_worker": {"status": "running"}}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
let launched = json!({
|
|
659
|
+
"session_name": "team-child",
|
|
660
|
+
"agents": {"child_worker": {"status": "running"}}
|
|
661
|
+
});
|
|
662
|
+
let merged = merge_workspace_team_state_with_key(&existing, &launched, "child");
|
|
663
|
+
assert_eq!(
|
|
664
|
+
merged.pointer("/teams/parent/session_name"),
|
|
665
|
+
Some(&json!("team-parent")),
|
|
666
|
+
"existing.teams must survive even when existing.session_name is empty: {merged}"
|
|
667
|
+
);
|
|
668
|
+
assert_eq!(
|
|
669
|
+
merged.pointer("/teams/child/session_name"),
|
|
670
|
+
Some(&json!("team-child")),
|
|
671
|
+
"launched team must still be inserted under its runtime key: {merged}"
|
|
672
|
+
);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
641
676
|
fn promote_launched_binding_from_team_entry(launched: &mut serde_json::Value, launched_key: &str) {
|
|
642
677
|
let entry = launched
|
|
643
678
|
.get("teams")
|
|
@@ -849,6 +884,29 @@ fn write_invalid_leader_pane_env_warning(workspaces: &[&Path], pane_id_raw: &str
|
|
|
849
884
|
}
|
|
850
885
|
}
|
|
851
886
|
|
|
887
|
+
fn warn_ignored_owner_team_id(workspace: &Path, team_dir: &Path, runtime_team_key: &str) {
|
|
888
|
+
let Ok(Some(ignored)) = crate::compiler::ignored_owner_team_id_from_team_md(team_dir) else {
|
|
889
|
+
return;
|
|
890
|
+
};
|
|
891
|
+
eprintln!("Warning: ignored TEAM.md {}={}", ignored.field, ignored.value);
|
|
892
|
+
eprintln!("Reason: owner identity is the canonical runtime team key ({runtime_team_key}), not TEAM.md front matter");
|
|
893
|
+
eprintln!("Action: remove {} from TEAM.md", ignored.field);
|
|
894
|
+
if let Err(err) = crate::event_log::EventLog::new(workspace).write(
|
|
895
|
+
"spec.field_ignored",
|
|
896
|
+
serde_json::json!({
|
|
897
|
+
"field": ignored.field,
|
|
898
|
+
"source": team_dir.join("TEAM.md").to_string_lossy().to_string(),
|
|
899
|
+
"value": ignored.value,
|
|
900
|
+
"warning": "ignored user-set owner_team_id",
|
|
901
|
+
"reason": "owner identity is derived from the canonical runtime team key",
|
|
902
|
+
"action": "remove owner_team_id from TEAM.md",
|
|
903
|
+
"runtime_team_key": runtime_team_key,
|
|
904
|
+
}),
|
|
905
|
+
) {
|
|
906
|
+
eprintln!("Warning: spec.field_ignored event write failed: {err}");
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
852
910
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
853
911
|
pub(crate) enum LeaderPaneEnvState {
|
|
854
912
|
Live,
|
|
@@ -932,12 +990,13 @@ fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &
|
|
|
932
990
|
let Some(owner) = unbound_launched_owner(launched, launched_key) else {
|
|
933
991
|
return;
|
|
934
992
|
};
|
|
935
|
-
let provider =
|
|
936
|
-
.get("
|
|
937
|
-
.and_then(|owner| owner.get("provider"))
|
|
993
|
+
let Some(provider) = owner
|
|
994
|
+
.get("provider")
|
|
938
995
|
.and_then(serde_json::Value::as_str)
|
|
939
996
|
.filter(|provider| !provider.is_empty())
|
|
940
|
-
|
|
997
|
+
else {
|
|
998
|
+
return;
|
|
999
|
+
};
|
|
941
1000
|
let owner_epoch = 1u64;
|
|
942
1001
|
let receiver = serde_json::json!({
|
|
943
1002
|
"mode": "direct_tmux",
|
|
@@ -958,12 +1017,7 @@ fn unbound_launched_owner(
|
|
|
958
1017
|
launched: &serde_json::Value,
|
|
959
1018
|
launched_key: &str,
|
|
960
1019
|
) -> Option<serde_json::Value> {
|
|
961
|
-
let provider = launched
|
|
962
|
-
.get("team_owner")
|
|
963
|
-
.and_then(|owner| owner.get("provider"))
|
|
964
|
-
.and_then(serde_json::Value::as_str)
|
|
965
|
-
.filter(|provider| !provider.is_empty())
|
|
966
|
-
.unwrap_or("codex");
|
|
1020
|
+
let provider = unbound_launched_provider(launched)?;
|
|
967
1021
|
let machine_fingerprint = launched
|
|
968
1022
|
.get("team_owner")
|
|
969
1023
|
.and_then(|owner| owner.get("machine_fingerprint"))
|
|
@@ -994,6 +1048,193 @@ fn unbound_launched_owner(
|
|
|
994
1048
|
}))
|
|
995
1049
|
}
|
|
996
1050
|
|
|
1051
|
+
fn unbound_launched_provider(launched: &serde_json::Value) -> Option<String> {
|
|
1052
|
+
if let Some(provider) = launched
|
|
1053
|
+
.get("team_owner")
|
|
1054
|
+
.and_then(|owner| owner.get("provider"))
|
|
1055
|
+
.and_then(serde_json::Value::as_str)
|
|
1056
|
+
.filter(|provider| !provider.is_empty())
|
|
1057
|
+
.and_then(parse_provider)
|
|
1058
|
+
.and_then(provider_wire_string)
|
|
1059
|
+
{
|
|
1060
|
+
return Some(provider);
|
|
1061
|
+
}
|
|
1062
|
+
let pane = launched
|
|
1063
|
+
.get("team_owner")
|
|
1064
|
+
.and_then(|owner| owner.get("pane_id"))
|
|
1065
|
+
.and_then(serde_json::Value::as_str)
|
|
1066
|
+
.filter(|pane| !pane.is_empty())?;
|
|
1067
|
+
let target = PaneId::new(pane);
|
|
1068
|
+
attributed_provider_for_pane_across_tmux_sockets(&target).and_then(provider_wire_string)
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
fn provider_wire_string(provider: Provider) -> Option<String> {
|
|
1072
|
+
serde_json::to_value(provider)
|
|
1073
|
+
.ok()
|
|
1074
|
+
.and_then(|value| value.as_str().map(str::to_string))
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
fn attributed_provider_for_pane_across_tmux_sockets(pane: &PaneId) -> Option<Provider> {
|
|
1078
|
+
crate::tmux_backend::tmux_socket_endpoints()
|
|
1079
|
+
.into_iter()
|
|
1080
|
+
.filter_map(|endpoint| {
|
|
1081
|
+
crate::tmux_backend::TmuxBackend::for_tmux_endpoint(&endpoint)
|
|
1082
|
+
.list_targets()
|
|
1083
|
+
.ok()
|
|
1084
|
+
})
|
|
1085
|
+
.flatten()
|
|
1086
|
+
.find(|info| info.pane_id == *pane)
|
|
1087
|
+
.and_then(|info| crate::leader::attribute_pane_provider(&info))
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
fn caller_provider_for_seed_with_lookup(
|
|
1091
|
+
caller: &crate::state::owner_gate::CallerIdentity,
|
|
1092
|
+
lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
|
|
1093
|
+
) -> Option<String> {
|
|
1094
|
+
if !caller.provider.is_empty() {
|
|
1095
|
+
if let Some(provider) = parse_provider(&caller.provider).and_then(provider_wire_string) {
|
|
1096
|
+
return Some(provider);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
(!caller.pane_id.is_empty())
|
|
1100
|
+
.then(|| PaneId::new(&caller.pane_id))
|
|
1101
|
+
.and_then(|pane| lookup_pane_provider(&pane))
|
|
1102
|
+
.and_then(provider_wire_string)
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
#[cfg(test)]
|
|
1106
|
+
mod e22_unbound_owner_provider_tests {
|
|
1107
|
+
use super::*;
|
|
1108
|
+
use crate::state::owner_gate::CallerIdentity;
|
|
1109
|
+
|
|
1110
|
+
#[test]
|
|
1111
|
+
fn unbound_owner_preserves_explicit_copilot_provider() {
|
|
1112
|
+
let mut launched = serde_json::json!({
|
|
1113
|
+
"workspace": "/tmp/team-agent-e22",
|
|
1114
|
+
"team_owner": {
|
|
1115
|
+
"provider": "copilot",
|
|
1116
|
+
"machine_fingerprint": "machine"
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
seed_unbound_launched_owner(&mut launched, "team-e22");
|
|
1121
|
+
|
|
1122
|
+
assert_eq!(
|
|
1123
|
+
launched
|
|
1124
|
+
.pointer("/team_owner/provider")
|
|
1125
|
+
.and_then(serde_json::Value::as_str),
|
|
1126
|
+
Some("copilot")
|
|
1127
|
+
);
|
|
1128
|
+
assert_eq!(
|
|
1129
|
+
launched
|
|
1130
|
+
.pointer("/leader_receiver/provider")
|
|
1131
|
+
.and_then(serde_json::Value::as_str),
|
|
1132
|
+
Some("copilot")
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
#[test]
|
|
1137
|
+
fn unbound_owner_without_attributed_provider_does_not_default_codex() {
|
|
1138
|
+
let mut launched = serde_json::json!({
|
|
1139
|
+
"workspace": "/tmp/team-agent-e22",
|
|
1140
|
+
"team_owner": {
|
|
1141
|
+
"machine_fingerprint": "machine"
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
|
|
1145
|
+
seed_unbound_launched_owner(&mut launched, "team-e22");
|
|
1146
|
+
|
|
1147
|
+
assert!(
|
|
1148
|
+
launched.get("leader_receiver").is_none(),
|
|
1149
|
+
"unattributed unbound owner must not seed a codex receiver: {launched}"
|
|
1150
|
+
);
|
|
1151
|
+
assert!(
|
|
1152
|
+
launched
|
|
1153
|
+
.pointer("/team_owner/provider")
|
|
1154
|
+
.and_then(serde_json::Value::as_str)
|
|
1155
|
+
!= Some("codex"),
|
|
1156
|
+
"unattributed unbound owner must not silently become codex: {launched}"
|
|
1157
|
+
);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
fn caller(provider: &str, pane_id: &str) -> CallerIdentity {
|
|
1161
|
+
CallerIdentity {
|
|
1162
|
+
pane_id: pane_id.to_string(),
|
|
1163
|
+
provider: provider.to_string(),
|
|
1164
|
+
machine_fingerprint: "machine".to_string(),
|
|
1165
|
+
leader_session_uuid: "leader-uuid".to_string(),
|
|
1166
|
+
leader_session_uuid_source: "derived".to_string(),
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
#[test]
|
|
1171
|
+
fn env_seed_attributes_in_tmux_node_form_copilot_from_caller_pane() {
|
|
1172
|
+
let mut state = serde_json::json!({
|
|
1173
|
+
"workspace": "/tmp/team-agent-e22",
|
|
1174
|
+
"leader": {"provider": "copilot"},
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
assert!(seed_launched_owner_from_caller_with_provider_lookup(
|
|
1178
|
+
&mut state,
|
|
1179
|
+
caller("", "%0"),
|
|
1180
|
+
|pane| (pane.as_str() == "%0").then_some(Provider::Copilot),
|
|
1181
|
+
));
|
|
1182
|
+
|
|
1183
|
+
assert_eq!(
|
|
1184
|
+
state
|
|
1185
|
+
.pointer("/team_owner/provider")
|
|
1186
|
+
.and_then(serde_json::Value::as_str),
|
|
1187
|
+
Some("copilot")
|
|
1188
|
+
);
|
|
1189
|
+
assert_eq!(
|
|
1190
|
+
state
|
|
1191
|
+
.pointer("/leader_receiver/provider")
|
|
1192
|
+
.and_then(serde_json::Value::as_str),
|
|
1193
|
+
Some("copilot")
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
#[test]
|
|
1198
|
+
fn env_seed_unknown_caller_pane_does_not_default_codex() {
|
|
1199
|
+
let mut state = serde_json::json!({
|
|
1200
|
+
"workspace": "/tmp/team-agent-e22",
|
|
1201
|
+
"leader": {"provider": "copilot"},
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
assert!(seed_launched_owner_from_caller_with_provider_lookup(
|
|
1205
|
+
&mut state,
|
|
1206
|
+
caller("", "%0"),
|
|
1207
|
+
|_| None,
|
|
1208
|
+
));
|
|
1209
|
+
assert_eq!(
|
|
1210
|
+
state
|
|
1211
|
+
.pointer("/team_owner/pane_id")
|
|
1212
|
+
.and_then(serde_json::Value::as_str),
|
|
1213
|
+
Some("%0")
|
|
1214
|
+
);
|
|
1215
|
+
assert_eq!(
|
|
1216
|
+
state
|
|
1217
|
+
.pointer("/leader_receiver/pane_id")
|
|
1218
|
+
.and_then(serde_json::Value::as_str),
|
|
1219
|
+
Some("%0")
|
|
1220
|
+
);
|
|
1221
|
+
assert!(
|
|
1222
|
+
state
|
|
1223
|
+
.pointer("/leader_receiver/provider")
|
|
1224
|
+
.and_then(serde_json::Value::as_str)
|
|
1225
|
+
!= Some("codex"),
|
|
1226
|
+
"unknown caller pane must not silently seed a codex receiver: {state}"
|
|
1227
|
+
);
|
|
1228
|
+
assert!(
|
|
1229
|
+
state
|
|
1230
|
+
.pointer("/team_owner/provider")
|
|
1231
|
+
.and_then(serde_json::Value::as_str)
|
|
1232
|
+
!= Some("codex"),
|
|
1233
|
+
"unknown caller pane must not silently become codex: {state}"
|
|
1234
|
+
);
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
997
1238
|
fn owner_pane_belongs_to_other_team(
|
|
998
1239
|
existing: &serde_json::Value,
|
|
999
1240
|
launched_key: &str,
|
|
@@ -2144,6 +2385,7 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
2144
2385
|
let state_team_key = explicit_team_key.clone().unwrap_or_else(|| {
|
|
2145
2386
|
runtime_team_key_for_spec(&agents_dir.join("team.spec.yaml"), &spec, &session_name)
|
|
2146
2387
|
});
|
|
2388
|
+
warn_ignored_owner_team_id(workspace.as_path(), agents_dir, &state_team_key);
|
|
2147
2389
|
// E5 spec 迁移:spec 写到 .team/runtime/<team_key>/(中间产物,绝不落用户目录 agents_dir)。
|
|
2148
2390
|
// Bug2:原子写(tmp+rename),避免半截 spec。
|
|
2149
2391
|
let spec_path = crate::model::paths::runtime_spec_path(&workspace, &state_team_key);
|
|
@@ -3565,19 +3807,26 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
|
|
|
3565
3807
|
) else {
|
|
3566
3808
|
return false;
|
|
3567
3809
|
};
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3810
|
+
seed_launched_owner_from_caller_with_provider_lookup(
|
|
3811
|
+
state,
|
|
3812
|
+
caller,
|
|
3813
|
+
attributed_provider_for_pane_across_tmux_sockets,
|
|
3814
|
+
)
|
|
3815
|
+
}
|
|
3816
|
+
|
|
3817
|
+
fn seed_launched_owner_from_caller_with_provider_lookup(
|
|
3818
|
+
state: &mut serde_json::Value,
|
|
3819
|
+
caller: crate::state::owner_gate::CallerIdentity,
|
|
3820
|
+
lookup_pane_provider: impl Fn(&PaneId) -> Option<Provider>,
|
|
3821
|
+
) -> bool {
|
|
3822
|
+
if caller.pane_id.is_empty() {
|
|
3575
3823
|
return false;
|
|
3576
3824
|
}
|
|
3825
|
+
let provider = caller_provider_for_seed_with_lookup(&caller, lookup_pane_provider);
|
|
3826
|
+
let pane_id = caller.pane_id;
|
|
3577
3827
|
let owner_epoch = 1u64;
|
|
3578
|
-
let owner = serde_json::json!({
|
|
3828
|
+
let mut owner = serde_json::json!({
|
|
3579
3829
|
"pane_id": pane_id,
|
|
3580
|
-
"provider": provider.clone(),
|
|
3581
3830
|
"machine_fingerprint": caller.machine_fingerprint,
|
|
3582
3831
|
"leader_session_uuid": caller.leader_session_uuid,
|
|
3583
3832
|
"owner_epoch": owner_epoch,
|
|
@@ -3587,17 +3836,23 @@ fn seed_launched_owner_from_env(state: &mut serde_json::Value) -> bool {
|
|
|
3587
3836
|
.or_else(|_| std::env::var("USERNAME"))
|
|
3588
3837
|
.unwrap_or_default(),
|
|
3589
3838
|
});
|
|
3590
|
-
let receiver = serde_json::json!({
|
|
3839
|
+
let mut receiver = serde_json::json!({
|
|
3591
3840
|
"mode": "direct_tmux",
|
|
3592
3841
|
"status": "attached",
|
|
3593
|
-
"provider": provider,
|
|
3594
3842
|
"pane_id": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
3595
3843
|
"pane": owner.get("pane_id").cloned().unwrap_or(serde_json::Value::Null),
|
|
3596
3844
|
"leader_session_uuid": owner.get("leader_session_uuid").cloned().unwrap_or(serde_json::Value::Null),
|
|
3597
3845
|
"owner_epoch": owner_epoch,
|
|
3598
3846
|
"discovery": "quick_start",
|
|
3599
3847
|
});
|
|
3600
|
-
let
|
|
3848
|
+
if let Some(provider) = provider.as_ref() {
|
|
3849
|
+
if let Some(owner) = owner.as_object_mut() {
|
|
3850
|
+
owner.insert("provider".to_string(), serde_json::json!(provider));
|
|
3851
|
+
}
|
|
3852
|
+
if let Some(receiver) = receiver.as_object_mut() {
|
|
3853
|
+
receiver.insert("provider".to_string(), serde_json::json!(provider));
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3601
3856
|
if let (Some(receiver), Some(socket)) = (
|
|
3602
3857
|
receiver.as_object_mut(),
|
|
3603
3858
|
crate::tmux_backend::socket_name_from_tmux_env(),
|
|
@@ -36,12 +36,11 @@ pub(super) fn spawn_agent_window(
|
|
|
36
36
|
detected_safety = crate::lifecycle::launch::effective_runtime_config_for_worker_spawn()?;
|
|
37
37
|
&detected_safety
|
|
38
38
|
};
|
|
39
|
-
let command_agent =
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
);
|
|
39
|
+
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_json(
|
|
40
|
+
agent,
|
|
41
|
+
Some(agent_id.as_str()),
|
|
42
|
+
provider,
|
|
43
|
+
);
|
|
45
44
|
let system_prompt =
|
|
46
45
|
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
47
46
|
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
@@ -60,7 +59,8 @@ pub(super) fn spawn_agent_window(
|
|
|
60
59
|
.and_then(|v| v.as_str())
|
|
61
60
|
.map(str::to_string)
|
|
62
61
|
.or_else(|| {
|
|
63
|
-
let key =
|
|
62
|
+
let key =
|
|
63
|
+
crate::messaging::leader_receiver::active_team_key(workspace, &state_for_team);
|
|
64
64
|
(!key.is_empty()).then_some(key)
|
|
65
65
|
});
|
|
66
66
|
let mcp_config = adapter
|
|
@@ -72,8 +72,11 @@ pub(super) fn spawn_agent_window(
|
|
|
72
72
|
agent_id.as_str(),
|
|
73
73
|
team_id.as_deref().unwrap_or(""),
|
|
74
74
|
);
|
|
75
|
-
let mcp_config_path =
|
|
76
|
-
|
|
75
|
+
let mcp_config_path = crate::lifecycle::launch::write_worker_mcp_config(
|
|
76
|
+
workspace,
|
|
77
|
+
agent_id.as_str(),
|
|
78
|
+
&mcp_config,
|
|
79
|
+
)?;
|
|
77
80
|
let profile_launch =
|
|
78
81
|
crate::lifecycle::profile_launch::prepare_provider_profile_launch_from_json(
|
|
79
82
|
workspace,
|
|
@@ -81,11 +84,7 @@ pub(super) fn spawn_agent_window(
|
|
|
81
84
|
agent,
|
|
82
85
|
Some(&mcp_config),
|
|
83
86
|
)?;
|
|
84
|
-
let command_model = profile_launch
|
|
85
|
-
.command_overrides
|
|
86
|
-
.model
|
|
87
|
-
.as_deref()
|
|
88
|
-
.or(model);
|
|
87
|
+
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
89
88
|
let context = crate::provider::ProviderCommandContext {
|
|
90
89
|
auth_mode,
|
|
91
90
|
mcp_config: Some(&mcp_config),
|
|
@@ -252,19 +251,30 @@ pub(super) fn resume_backing_exists_for_agent(
|
|
|
252
251
|
rollout_path: Option<&RolloutPath>,
|
|
253
252
|
) -> bool {
|
|
254
253
|
match provider {
|
|
254
|
+
provider if !provider_supports_resume(provider) => {
|
|
255
|
+
let _ = (workspace, agent_id, agent, session_id, rollout_path);
|
|
256
|
+
false
|
|
257
|
+
}
|
|
255
258
|
Provider::Codex => rollout_path_exists(rollout_path),
|
|
256
259
|
Provider::Claude | Provider::ClaudeCode => {
|
|
257
260
|
rollout_path_exists(rollout_path)
|
|
258
261
|
|| event_log_transcript_exists(workspace, agent_id.as_str(), session_id.as_str())
|
|
259
262
|
}
|
|
260
263
|
Provider::Copilot => copilot_session_store_has_session(session_id.as_str()),
|
|
261
|
-
Provider::GeminiCli | Provider::Fake =>
|
|
262
|
-
let _ = agent;
|
|
263
|
-
true
|
|
264
|
-
}
|
|
264
|
+
Provider::GeminiCli | Provider::Fake => false,
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
|
|
268
|
+
pub(super) fn provider_supports_resume(provider: Provider) -> bool {
|
|
269
|
+
crate::provider::get_adapter(provider).caps().resume
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
pub(super) fn provider_wire_supports_resume(provider: &str) -> bool {
|
|
273
|
+
parse_provider(provider)
|
|
274
|
+
.map(provider_supports_resume)
|
|
275
|
+
.unwrap_or(false)
|
|
276
|
+
}
|
|
277
|
+
|
|
268
278
|
fn rollout_path_exists(rollout_path: Option<&RolloutPath>) -> bool {
|
|
269
279
|
rollout_path
|
|
270
280
|
.as_ref()
|
|
@@ -308,13 +318,12 @@ fn copilot_session_store_has_session(session_id: &str) -> bool {
|
|
|
308
318
|
) else {
|
|
309
319
|
return false;
|
|
310
320
|
};
|
|
311
|
-
conn
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
.is_ok()
|
|
321
|
+
conn.query_row(
|
|
322
|
+
"select 1 from sessions where id = ?1 limit 1",
|
|
323
|
+
[session_id],
|
|
324
|
+
|_| Ok(()),
|
|
325
|
+
)
|
|
326
|
+
.is_ok()
|
|
318
327
|
}
|
|
319
328
|
|
|
320
329
|
pub(crate) fn refresh_missing_provider_sessions(
|
|
@@ -469,7 +478,10 @@ pub(super) fn load_team_spec(workspace: &Path) -> Result<YamlValue, LifecycleErr
|
|
|
469
478
|
yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))
|
|
470
479
|
}
|
|
471
480
|
|
|
472
|
-
pub(super) fn find_spec_agent<'a>(
|
|
481
|
+
pub(super) fn find_spec_agent<'a>(
|
|
482
|
+
spec: &'a YamlValue,
|
|
483
|
+
agent_id: &AgentId,
|
|
484
|
+
) -> Option<&'a YamlValue> {
|
|
473
485
|
let leader_is_agent = spec
|
|
474
486
|
.get("leader")
|
|
475
487
|
.and_then(|v| v.get("id"))
|
|
@@ -479,23 +491,23 @@ pub(super) fn find_spec_agent<'a>(spec: &'a YamlValue, agent_id: &AgentId) -> Op
|
|
|
479
491
|
if leader_is_agent {
|
|
480
492
|
return None;
|
|
481
493
|
}
|
|
482
|
-
spec.get("agents")
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
.map(|id| id == agent_id.as_str())
|
|
490
|
-
.unwrap_or(false)
|
|
491
|
-
})
|
|
494
|
+
spec.get("agents")?.as_list()?.iter().find(|agent| {
|
|
495
|
+
agent
|
|
496
|
+
.get("id")
|
|
497
|
+
.and_then(YamlValue::as_str)
|
|
498
|
+
.map(|id| id == agent_id.as_str())
|
|
499
|
+
.unwrap_or(false)
|
|
500
|
+
})
|
|
492
501
|
}
|
|
493
502
|
|
|
494
503
|
pub(super) fn unknown_worker(agent_id: &AgentId) -> LifecycleError {
|
|
495
504
|
LifecycleError::RequirementUnmet(format!("unknown worker agent id: {agent_id}"))
|
|
496
505
|
}
|
|
497
506
|
|
|
498
|
-
pub(super) fn state_session_name_from_spec(
|
|
507
|
+
pub(super) fn state_session_name_from_spec(
|
|
508
|
+
state: &serde_json::Value,
|
|
509
|
+
spec: &YamlValue,
|
|
510
|
+
) -> SessionName {
|
|
499
511
|
state
|
|
500
512
|
.get("session_name")
|
|
501
513
|
.and_then(|v| v.as_str())
|
|
@@ -709,9 +721,7 @@ pub(super) fn agent_is_running(
|
|
|
709
721
|
agent_id: &AgentId,
|
|
710
722
|
transport: &dyn crate::transport::Transport,
|
|
711
723
|
) -> bool {
|
|
712
|
-
let agent_state = state
|
|
713
|
-
.get("agents")
|
|
714
|
-
.and_then(|v| v.get(agent_id.as_str()));
|
|
724
|
+
let agent_state = state.get("agents").and_then(|v| v.get(agent_id.as_str()));
|
|
715
725
|
let status = agent_state
|
|
716
726
|
.and_then(|v| v.get("status"))
|
|
717
727
|
.and_then(|v| v.as_str())
|