@team-agent/installer 0.5.50 → 0.5.51
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 +5 -3
- package/crates/team-agent/src/cli/emit.rs +2 -1
- package/crates/team-agent/src/cli/mod.rs +32 -14
- package/crates/team-agent/src/cli/named_address.rs +11 -48
- package/crates/team-agent/src/cli/send/coordinator.rs +163 -0
- package/crates/team-agent/src/cli/send/mailbox.rs +99 -0
- package/crates/team-agent/src/cli/send/persist.rs +154 -0
- package/crates/team-agent/src/cli/send/presentation.rs +333 -0
- package/crates/team-agent/src/cli/send/resolve.rs +361 -0
- package/crates/team-agent/src/cli/send.rs +44 -1169
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/status_port/agents.rs +358 -0
- package/crates/team-agent/src/cli/status_port/approvals.rs +79 -0
- package/crates/team-agent/src/cli/status_port/compact.rs +207 -0
- package/crates/team-agent/src/cli/status_port/format.rs +145 -0
- package/crates/team-agent/src/cli/status_port/inbox.rs +36 -0
- package/crates/team-agent/src/cli/status_port/runtime.rs +195 -0
- package/crates/team-agent/src/cli/status_port/snapshot.rs +181 -0
- package/crates/team-agent/src/cli/status_port/store.rs +412 -0
- package/crates/team-agent/src/cli/status_port/tests.rs +54 -0
- package/crates/team-agent/src/cli/status_port.rs +47 -1548
- package/crates/team-agent/src/cli/tests/run_delegation.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/conpty_shim.rs +34 -30
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +135 -13
- package/crates/team-agent/src/coordinator/tick.rs +37 -0
- package/crates/team-agent/src/db/agent_health_capture.rs +18 -13
- package/crates/team-agent/src/db/message_store.rs +154 -44
- package/crates/team-agent/src/event_log.rs +73 -0
- package/crates/team-agent/src/leader/start.rs +28 -4
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +424 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +160 -0
- package/crates/team-agent/src/lifecycle/launch/approval.rs +134 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +492 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +297 -0
- package/crates/team-agent/src/lifecycle/launch/identity.rs +372 -0
- package/crates/team-agent/src/lifecycle/launch/layout.rs +313 -0
- package/crates/team-agent/src/lifecycle/launch/leader_context.rs +478 -0
- package/crates/team-agent/src/lifecycle/launch/mcp_config.rs +201 -0
- package/crates/team-agent/src/lifecycle/launch/ownership.rs +66 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +477 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start_transport.rs +278 -0
- package/crates/team-agent/src/lifecycle/launch/readiness.rs +123 -0
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +377 -0
- package/crates/team-agent/src/lifecycle/launch/spec_state.rs +434 -0
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +499 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +438 -0
- package/crates/team-agent/src/lifecycle/launch.rs +119 -5351
- package/crates/team-agent/src/lifecycle/restart/agent.rs +44 -26
- package/crates/team-agent/src/lifecycle/restart/common.rs +53 -27
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +67 -26
- package/crates/team-agent/src/lifecycle/restart/remove.rs +435 -72
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +575 -17
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +55 -2
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +24 -1
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +30 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +8 -4
- package/crates/team-agent/src/mcp_server/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/send.rs +6 -10
- package/crates/team-agent/src/mcp_server/tools.rs +14 -5
- package/crates/team-agent/src/messaging/activity.rs +4 -2
- package/crates/team-agent/src/messaging/address.rs +86 -0
- package/crates/team-agent/src/messaging/delivery.rs +165 -39
- package/crates/team-agent/src/messaging/helpers.rs +17 -13
- package/crates/team-agent/src/messaging/leader_receiver.rs +60 -35
- package/crates/team-agent/src/messaging/mod.rs +10 -2
- package/crates/team-agent/src/messaging/persist.rs +309 -0
- package/crates/team-agent/src/messaging/results.rs +16 -24
- package/crates/team-agent/src/messaging/scheduler.rs +4 -2
- package/crates/team-agent/src/messaging/selftest.rs +19 -12
- package/crates/team-agent/src/messaging/send.rs +101 -49
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +305 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +1 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +38 -17
- package/crates/team-agent/src/messaging/watchers.rs +13 -3
- package/crates/team-agent/src/redaction.rs +72 -2
- package/crates/team-agent/src/state/persist.rs +2 -1
- package/crates/team-agent/src/state/repository/tests.rs +47 -0
- package/crates/team-agent/src/state/repository.rs +59 -16
- package/package.json +4 -4
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::process::Command;
|
|
4
|
+
|
|
5
|
+
use crate::lifecycle::*;
|
|
6
|
+
use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
|
|
7
|
+
use crate::model::ids::AgentId;
|
|
8
|
+
use crate::model::permissions::{self, AgentPermissionInput};
|
|
9
|
+
use crate::model::yaml::{self, Value};
|
|
10
|
+
use crate::state::persist::load_runtime_state;
|
|
11
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
12
|
+
|
|
13
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
14
|
+
|
|
15
|
+
use super::*;
|
|
16
|
+
|
|
17
|
+
pub(super) fn maybe_fail_fork_after_spawn(step: &str) -> Result<(), LifecycleError> {
|
|
18
|
+
let Ok(reason) = std::env::var("TEAM_AGENT_TEST_FAIL_FORK_AFTER_SPAWN") else {
|
|
19
|
+
return Ok(());
|
|
20
|
+
};
|
|
21
|
+
if reason.is_empty() {
|
|
22
|
+
return Ok(());
|
|
23
|
+
}
|
|
24
|
+
let should_fail = reason == step || (step == "start_coordinator" && reason == "coordinator");
|
|
25
|
+
if !should_fail {
|
|
26
|
+
return Ok(());
|
|
27
|
+
}
|
|
28
|
+
Err(LifecycleError::StatePersist(format!(
|
|
29
|
+
"injected fork failure after spawn: {reason}"
|
|
30
|
+
)))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub(super) fn cleanup_fork_mcp_artifacts(
|
|
34
|
+
workspace: &Path,
|
|
35
|
+
agent_id: &AgentId,
|
|
36
|
+
mcp_config_path: &Path,
|
|
37
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
38
|
+
) {
|
|
39
|
+
let _ = std::fs::remove_file(mcp_config_path);
|
|
40
|
+
let _ = std::fs::remove_file(
|
|
41
|
+
workspace
|
|
42
|
+
.join(".team/runtime/provider-env")
|
|
43
|
+
.join(format!("{}.env", agent_id.as_str())),
|
|
44
|
+
);
|
|
45
|
+
if let Some(config_dir) = profile_launch.claude_config_dir.as_ref() {
|
|
46
|
+
let _ = std::fs::remove_dir_all(config_dir.parent().unwrap_or(config_dir));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pub(super) fn leader_id_matches(spec: &Value, agent_id: &AgentId) -> bool {
|
|
51
|
+
spec.get("leader")
|
|
52
|
+
.and_then(|v| v.get("id"))
|
|
53
|
+
.and_then(Value::as_str)
|
|
54
|
+
.map(|id| id == agent_id.as_str())
|
|
55
|
+
.unwrap_or(false)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pub(super) fn find_spec_agent<'a>(spec: &'a Value, agent_id: &AgentId) -> Option<&'a Value> {
|
|
59
|
+
let leader_is_agent = spec
|
|
60
|
+
.get("leader")
|
|
61
|
+
.and_then(|v| v.get("id"))
|
|
62
|
+
.and_then(Value::as_str)
|
|
63
|
+
.map(|id| id == agent_id.as_str())
|
|
64
|
+
.unwrap_or(false);
|
|
65
|
+
if leader_is_agent {
|
|
66
|
+
return None;
|
|
67
|
+
}
|
|
68
|
+
spec.get("agents")?.as_list()?.iter().find(|agent| {
|
|
69
|
+
agent
|
|
70
|
+
.get("id")
|
|
71
|
+
.and_then(Value::as_str)
|
|
72
|
+
.map(|id| id == agent_id.as_str())
|
|
73
|
+
.unwrap_or(false)
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
pub(super) fn append_forked_agent(
|
|
78
|
+
spec: &Value,
|
|
79
|
+
source_agent: &Value,
|
|
80
|
+
source_agent_id: &AgentId,
|
|
81
|
+
as_agent_id: &AgentId,
|
|
82
|
+
label: Option<&str>,
|
|
83
|
+
) -> Result<Value, LifecycleError> {
|
|
84
|
+
let mut new_agent = source_agent.clone();
|
|
85
|
+
set_yaml_map_value(
|
|
86
|
+
&mut new_agent,
|
|
87
|
+
"id",
|
|
88
|
+
Value::Str(as_agent_id.as_str().to_string()),
|
|
89
|
+
)?;
|
|
90
|
+
// golden operations.py:315 `str(label or new_agent.get("role") or as_agent_id)` — Python `or`
|
|
91
|
+
// falsiness: an EMPTY-string label/role is falsy and falls through to the next tier.
|
|
92
|
+
// The label IS the forked agent's new role (it feeds the identity prompt — B2 family).
|
|
93
|
+
let role = label
|
|
94
|
+
.filter(|s| !s.is_empty())
|
|
95
|
+
.or_else(|| {
|
|
96
|
+
new_agent
|
|
97
|
+
.get("role")
|
|
98
|
+
.and_then(Value::as_str)
|
|
99
|
+
.filter(|s| !s.is_empty())
|
|
100
|
+
})
|
|
101
|
+
.unwrap_or_else(|| as_agent_id.as_str())
|
|
102
|
+
.to_string();
|
|
103
|
+
set_yaml_map_value(&mut new_agent, "role", Value::Str(role.clone()))?;
|
|
104
|
+
set_yaml_map_value(
|
|
105
|
+
&mut new_agent,
|
|
106
|
+
"forked_from",
|
|
107
|
+
Value::Str(source_agent_id.as_str().to_string()),
|
|
108
|
+
)?;
|
|
109
|
+
set_yaml_map_value(
|
|
110
|
+
&mut new_agent,
|
|
111
|
+
"preferred_for",
|
|
112
|
+
Value::List(vec![
|
|
113
|
+
Value::Str(as_agent_id.as_str().to_string()),
|
|
114
|
+
Value::Str(role),
|
|
115
|
+
]),
|
|
116
|
+
)?;
|
|
117
|
+
|
|
118
|
+
let Value::Map(pairs) = spec else {
|
|
119
|
+
return Err(LifecycleError::Compile(
|
|
120
|
+
"spec root is not a map".to_string(),
|
|
121
|
+
));
|
|
122
|
+
};
|
|
123
|
+
let mut out = Vec::new();
|
|
124
|
+
for (key, value) in pairs {
|
|
125
|
+
if key == "agents" {
|
|
126
|
+
let mut agents = value
|
|
127
|
+
.as_list()
|
|
128
|
+
.map(|items| items.to_vec())
|
|
129
|
+
.unwrap_or_default();
|
|
130
|
+
agents.push(new_agent.clone());
|
|
131
|
+
out.push((key.clone(), Value::List(agents)));
|
|
132
|
+
} else if key == "runtime" {
|
|
133
|
+
out.push((key.clone(), runtime_with_startup_agent(value, as_agent_id)));
|
|
134
|
+
} else {
|
|
135
|
+
out.push((key.clone(), value.clone()));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
Ok(Value::Map(out))
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
pub(super) fn set_yaml_map_value(
|
|
142
|
+
value: &mut Value,
|
|
143
|
+
key: &str,
|
|
144
|
+
next: Value,
|
|
145
|
+
) -> Result<(), LifecycleError> {
|
|
146
|
+
let Value::Map(pairs) = value else {
|
|
147
|
+
return Err(LifecycleError::Compile(
|
|
148
|
+
"agent entry is not a map".to_string(),
|
|
149
|
+
));
|
|
150
|
+
};
|
|
151
|
+
if let Some((_, existing)) = pairs.iter_mut().find(|(k, _)| k == key) {
|
|
152
|
+
*existing = next;
|
|
153
|
+
} else {
|
|
154
|
+
pairs.push((key.to_string(), next));
|
|
155
|
+
}
|
|
156
|
+
Ok(())
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
pub(super) fn runtime_with_startup_agent(runtime: &Value, agent_id: &AgentId) -> Value {
|
|
160
|
+
let Value::Map(pairs) = runtime else {
|
|
161
|
+
return runtime.clone();
|
|
162
|
+
};
|
|
163
|
+
let mut out = Vec::new();
|
|
164
|
+
let mut saw_startup = false;
|
|
165
|
+
for (key, value) in pairs {
|
|
166
|
+
if key == "startup_order" {
|
|
167
|
+
saw_startup = true;
|
|
168
|
+
let mut order = value
|
|
169
|
+
.as_list()
|
|
170
|
+
.map(|items| items.to_vec())
|
|
171
|
+
.unwrap_or_default();
|
|
172
|
+
let already_present = order.iter().any(|item| {
|
|
173
|
+
item.as_str()
|
|
174
|
+
.map(|id| id == agent_id.as_str())
|
|
175
|
+
.unwrap_or(false)
|
|
176
|
+
});
|
|
177
|
+
if !already_present {
|
|
178
|
+
order.push(Value::Str(agent_id.as_str().to_string()));
|
|
179
|
+
}
|
|
180
|
+
out.push((key.clone(), Value::List(order)));
|
|
181
|
+
} else {
|
|
182
|
+
out.push((key.clone(), value.clone()));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if !saw_startup {
|
|
186
|
+
out.push((
|
|
187
|
+
"startup_order".to_string(),
|
|
188
|
+
Value::List(vec![Value::Str(agent_id.as_str().to_string())]),
|
|
189
|
+
));
|
|
190
|
+
}
|
|
191
|
+
Value::Map(out)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
pub(super) fn upsert_forked_agent_state(
|
|
195
|
+
state: &mut serde_json::Value,
|
|
196
|
+
source_agent_id: &AgentId,
|
|
197
|
+
as_agent_id: &AgentId,
|
|
198
|
+
spec_agent: &Value,
|
|
199
|
+
safety: &DangerousApproval,
|
|
200
|
+
plan: &crate::provider::CommandPlan,
|
|
201
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
202
|
+
spawn: &crate::transport::SpawnResult,
|
|
203
|
+
spawn_cwd: &Path,
|
|
204
|
+
profile_dir: Option<&Path>,
|
|
205
|
+
) -> Result<(), LifecycleError> {
|
|
206
|
+
if !state.is_object() {
|
|
207
|
+
*state = serde_json::json!({});
|
|
208
|
+
}
|
|
209
|
+
let Some(root) = state.as_object_mut() else {
|
|
210
|
+
return Err(LifecycleError::StatePersist(
|
|
211
|
+
"runtime state root is not an object".to_string(),
|
|
212
|
+
));
|
|
213
|
+
};
|
|
214
|
+
let agents = root
|
|
215
|
+
.entry("agents".to_string())
|
|
216
|
+
.or_insert_with(|| serde_json::json!({}));
|
|
217
|
+
if !agents.is_object() {
|
|
218
|
+
*agents = serde_json::json!({});
|
|
219
|
+
}
|
|
220
|
+
let Some(agent_map) = agents.as_object_mut() else {
|
|
221
|
+
return Err(LifecycleError::StatePersist(
|
|
222
|
+
"runtime state agents is not an object".to_string(),
|
|
223
|
+
));
|
|
224
|
+
};
|
|
225
|
+
let provider = spec_agent
|
|
226
|
+
.get("provider")
|
|
227
|
+
.and_then(Value::as_str)
|
|
228
|
+
.unwrap_or("codex");
|
|
229
|
+
let mut entry = serde_json::Map::new();
|
|
230
|
+
entry.insert("status".to_string(), serde_json::json!("running"));
|
|
231
|
+
entry.insert("provider".to_string(), serde_json::json!(provider));
|
|
232
|
+
entry.insert(
|
|
233
|
+
"agent_id".to_string(),
|
|
234
|
+
serde_json::json!(as_agent_id.as_str()),
|
|
235
|
+
);
|
|
236
|
+
entry.insert(
|
|
237
|
+
"window".to_string(),
|
|
238
|
+
serde_json::json!(as_agent_id.as_str()),
|
|
239
|
+
);
|
|
240
|
+
entry.insert(
|
|
241
|
+
"forked_from".to_string(),
|
|
242
|
+
serde_json::json!(source_agent_id.as_str()),
|
|
243
|
+
);
|
|
244
|
+
entry.insert(
|
|
245
|
+
"spawn_cwd".to_string(),
|
|
246
|
+
serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
|
|
247
|
+
);
|
|
248
|
+
entry.insert(
|
|
249
|
+
"pane_id".to_string(),
|
|
250
|
+
serde_json::json!(spawn.pane_id.as_str()),
|
|
251
|
+
);
|
|
252
|
+
if let Some(pid) = spawn.child_pid {
|
|
253
|
+
entry.insert("pane_pid".to_string(), serde_json::json!(pid));
|
|
254
|
+
}
|
|
255
|
+
for key in [
|
|
256
|
+
"auth_mode",
|
|
257
|
+
"model",
|
|
258
|
+
"model_source",
|
|
259
|
+
"profile",
|
|
260
|
+
"_profile_dir",
|
|
261
|
+
"role",
|
|
262
|
+
// 0.4.x provider effort MVP step 8: fork inherits compiled effort.
|
|
263
|
+
"effort",
|
|
264
|
+
] {
|
|
265
|
+
if let Some(value) = spec_agent.get(key) {
|
|
266
|
+
entry.insert(key.to_string(), yaml_value_to_json(value));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if spec_agent.get("profile").is_some() && !entry.contains_key("_profile_dir") {
|
|
270
|
+
if let Some(profile_dir) = profile_dir {
|
|
271
|
+
entry.insert(
|
|
272
|
+
"_profile_dir".to_string(),
|
|
273
|
+
serde_json::json!(profile_dir.to_string_lossy().to_string()),
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
entry.insert("session_id".to_string(), serde_json::Value::Null);
|
|
278
|
+
entry.insert("rollout_path".to_string(), serde_json::Value::Null);
|
|
279
|
+
entry.insert("captured_at".to_string(), serde_json::Value::Null);
|
|
280
|
+
entry.insert("captured_via".to_string(), serde_json::Value::Null);
|
|
281
|
+
entry.insert(
|
|
282
|
+
"attribution_confidence".to_string(),
|
|
283
|
+
serde_json::Value::Null,
|
|
284
|
+
);
|
|
285
|
+
persist_command_plan_state(&mut entry, plan, profile_launch);
|
|
286
|
+
agent_map.insert(
|
|
287
|
+
as_agent_id.as_str().to_string(),
|
|
288
|
+
serde_json::Value::Object(entry),
|
|
289
|
+
);
|
|
290
|
+
if let Some(entry) = agent_map
|
|
291
|
+
.get_mut(as_agent_id.as_str())
|
|
292
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
293
|
+
{
|
|
294
|
+
persist_effective_approval_policy(entry, safety);
|
|
295
|
+
}
|
|
296
|
+
Ok(())
|
|
297
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
use std::collections::{BTreeMap, BTreeSet};
|
|
2
|
+
use std::path::{Path, PathBuf};
|
|
3
|
+
use std::process::Command;
|
|
4
|
+
|
|
5
|
+
use crate::lifecycle::*;
|
|
6
|
+
use crate::model::enums::{AuthMode, DisplayBackend, PaneLiveness, Provider, ProviderEffort};
|
|
7
|
+
use crate::model::ids::AgentId;
|
|
8
|
+
use crate::model::permissions::{self, AgentPermissionInput};
|
|
9
|
+
use crate::model::yaml::{self, Value};
|
|
10
|
+
use crate::state::persist::load_runtime_state;
|
|
11
|
+
use crate::transport::{PaneId, SessionName, Target, Transport, WindowName};
|
|
12
|
+
|
|
13
|
+
use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest};
|
|
14
|
+
|
|
15
|
+
use super::*;
|
|
16
|
+
|
|
17
|
+
pub(super) fn spec_team_id(spec: &Value) -> Option<String> {
|
|
18
|
+
spec.get("team")
|
|
19
|
+
.and_then(|v| v.get("id").or_else(|| v.get("name")))
|
|
20
|
+
.and_then(Value::as_str)
|
|
21
|
+
.map(str::to_string)
|
|
22
|
+
.or_else(|| spec.get("name").and_then(Value::as_str).map(str::to_string))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub(super) fn explicit_active_team_key(state: &serde_json::Value) -> Option<String> {
|
|
26
|
+
state
|
|
27
|
+
.get("active_team_key")
|
|
28
|
+
.and_then(serde_json::Value::as_str)
|
|
29
|
+
.filter(|team| !team.is_empty())
|
|
30
|
+
.map(str::to_string)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
pub(super) fn runtime_team_key_for_spec(
|
|
34
|
+
spec_path: &Path,
|
|
35
|
+
spec: &Value,
|
|
36
|
+
session_name: &SessionName,
|
|
37
|
+
) -> String {
|
|
38
|
+
let team_dir = spec_path.parent().unwrap_or_else(|| Path::new("."));
|
|
39
|
+
let state = serde_json::json!({
|
|
40
|
+
"team_dir": team_dir.to_string_lossy(),
|
|
41
|
+
"spec_path": spec_path.to_string_lossy(),
|
|
42
|
+
"session_name": session_name.as_str(),
|
|
43
|
+
"team": spec.get("team").map(yaml_value_to_json).unwrap_or(serde_json::Value::Null),
|
|
44
|
+
});
|
|
45
|
+
crate::state::projection::team_state_key(&state)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
pub(super) fn transport_has_session(transport: &dyn Transport, session_name: &SessionName) -> bool {
|
|
49
|
+
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
50
|
+
transport.has_session(session_name)
|
|
51
|
+
})) {
|
|
52
|
+
Ok(Ok(live)) => live,
|
|
53
|
+
Ok(Err(_)) | Err(_) => false,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
pub(super) fn spec_display_backend(spec: &Value) -> DisplayBackend {
|
|
58
|
+
let requested = spec
|
|
59
|
+
.get("runtime")
|
|
60
|
+
.and_then(|runtime| runtime.get("display_backend"))
|
|
61
|
+
.and_then(Value::as_str)
|
|
62
|
+
.and_then(|backend| {
|
|
63
|
+
serde_json::from_value::<DisplayBackend>(serde_json::json!(backend)).ok()
|
|
64
|
+
});
|
|
65
|
+
crate::lifecycle::display::resolve_display_backend(requested, None).backend
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
use crate::provider::wire::parse_provider;
|
|
69
|
+
|
|
70
|
+
pub(super) fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
|
|
71
|
+
match raw {
|
|
72
|
+
"subscription" => Some(AuthMode::Subscription),
|
|
73
|
+
"official_api" => Some(AuthMode::OfficialApi),
|
|
74
|
+
"compatible_api" => Some(AuthMode::CompatibleApi),
|
|
75
|
+
_ => None,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// 0.4.x provider effort MVP step 4: low-level from a raw string. Returns
|
|
80
|
+
/// `Some(effort)` when the level parses AND the provider supports it.
|
|
81
|
+
pub(crate) fn provider_effort_from_raw(
|
|
82
|
+
raw: Option<&str>,
|
|
83
|
+
provider: Provider,
|
|
84
|
+
) -> Option<ProviderEffort> {
|
|
85
|
+
let raw = raw?.trim();
|
|
86
|
+
if raw.is_empty() {
|
|
87
|
+
return None;
|
|
88
|
+
}
|
|
89
|
+
let effort = ProviderEffort::parse(raw)?;
|
|
90
|
+
if effort.is_supported_by(provider) {
|
|
91
|
+
Some(effort)
|
|
92
|
+
} else {
|
|
93
|
+
None
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// 0.4.x provider effort MVP step 7: warning event payload when the spec
|
|
98
|
+
/// requested an effort level the provider does not support.
|
|
99
|
+
pub(crate) fn provider_effort_event_payload(
|
|
100
|
+
raw: Option<&str>,
|
|
101
|
+
provider: Provider,
|
|
102
|
+
agent_id: &str,
|
|
103
|
+
) -> Option<serde_json::Value> {
|
|
104
|
+
let raw = raw?.trim();
|
|
105
|
+
if raw.is_empty() {
|
|
106
|
+
return None;
|
|
107
|
+
}
|
|
108
|
+
let effort = ProviderEffort::parse(raw)?;
|
|
109
|
+
if effort.is_supported_by(provider) {
|
|
110
|
+
return None;
|
|
111
|
+
}
|
|
112
|
+
Some(serde_json::json!({
|
|
113
|
+
"agent_id": agent_id,
|
|
114
|
+
"provider": format!("{provider:?}").to_lowercase(),
|
|
115
|
+
"effort": effort.as_str(),
|
|
116
|
+
"action": "ignored",
|
|
117
|
+
"reason": "provider does not support effort",
|
|
118
|
+
}))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// 0.4.x provider effort MVP step 9: defensive guarantee that `CLAUDE_EFFORT`
|
|
122
|
+
/// is unset in the Claude/ClaudeCode worker spawn env. As of the
|
|
123
|
+
/// `profile_launch::provider_env_unsets` update, the base list already
|
|
124
|
+
/// includes `CLAUDE_EFFORT` for Claude — so this function is idempotent
|
|
125
|
+
/// (returns input unchanged). Kept as a belt-and-braces guard so a future
|
|
126
|
+
/// refactor that bypasses provider_env_unsets cannot silently drop the
|
|
127
|
+
/// scrub. The structural win is in `tmux_backend::shell_command` which now
|
|
128
|
+
/// filters env exports by env_unset (preventing inherited values from
|
|
129
|
+
/// re-introducing keys we just unset).
|
|
130
|
+
pub(crate) fn extend_worker_env_unset_for_effort(
|
|
131
|
+
base: Vec<String>,
|
|
132
|
+
provider: Provider,
|
|
133
|
+
) -> Vec<String> {
|
|
134
|
+
if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
|
|
135
|
+
return base;
|
|
136
|
+
}
|
|
137
|
+
let mut out = base;
|
|
138
|
+
if !out.iter().any(|k| k == "CLAUDE_EFFORT") {
|
|
139
|
+
out.push("CLAUDE_EFFORT".to_string());
|
|
140
|
+
}
|
|
141
|
+
out
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/// Convenience: resolve effort for a yaml::Value agent (spec / compiled).
|
|
145
|
+
pub(crate) fn provider_effort_for_spawn(
|
|
146
|
+
agent: &crate::model::yaml::Value,
|
|
147
|
+
provider: Provider,
|
|
148
|
+
) -> Option<ProviderEffort> {
|
|
149
|
+
provider_effort_from_raw(agent.get("effort").and_then(|v| v.as_str()), provider)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
pub(crate) fn provider_effort_event_if_dropped(
|
|
153
|
+
agent: &crate::model::yaml::Value,
|
|
154
|
+
provider: Provider,
|
|
155
|
+
agent_id: &str,
|
|
156
|
+
) -> Option<serde_json::Value> {
|
|
157
|
+
provider_effort_event_payload(
|
|
158
|
+
agent.get("effort").and_then(|v| v.as_str()),
|
|
159
|
+
provider,
|
|
160
|
+
agent_id,
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/// Same as [`provider_effort_for_spawn`] but for serde_json state values
|
|
165
|
+
/// (used by restart paths reading from `state.agents[id]`).
|
|
166
|
+
pub(crate) fn provider_effort_for_spawn_json(
|
|
167
|
+
agent: &serde_json::Value,
|
|
168
|
+
provider: Provider,
|
|
169
|
+
) -> Option<ProviderEffort> {
|
|
170
|
+
provider_effort_from_raw(
|
|
171
|
+
agent.get("effort").and_then(serde_json::Value::as_str),
|
|
172
|
+
provider,
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
pub(crate) fn provider_effort_event_if_dropped_json(
|
|
177
|
+
agent: &serde_json::Value,
|
|
178
|
+
provider: Provider,
|
|
179
|
+
agent_id: &str,
|
|
180
|
+
) -> Option<serde_json::Value> {
|
|
181
|
+
provider_effort_event_payload(
|
|
182
|
+
agent.get("effort").and_then(serde_json::Value::as_str),
|
|
183
|
+
provider,
|
|
184
|
+
agent_id,
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
pub(super) fn quick_start_requested_team_key<'a>(
|
|
189
|
+
team_id: Option<&'a str>,
|
|
190
|
+
name: Option<&'a str>,
|
|
191
|
+
) -> Option<&'a str> {
|
|
192
|
+
team_id.or(name).filter(|team| !team.is_empty())
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
pub(super) struct QuickStartDepth {
|
|
196
|
+
pub(super) parent_team_key: Option<String>,
|
|
197
|
+
pub(super) team_depth: u64,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
pub(super) fn quick_start_depth_guard(
|
|
201
|
+
workspace: &Path,
|
|
202
|
+
_agents_dir: &Path,
|
|
203
|
+
requested_team: Option<&str>,
|
|
204
|
+
_strict_real_runtime: bool,
|
|
205
|
+
) -> Result<QuickStartDepth, LifecycleError> {
|
|
206
|
+
let env_parent = std::env::var("TEAM_AGENT_OWNER_TEAM_ID")
|
|
207
|
+
.ok()
|
|
208
|
+
.map(|value| value.trim().to_string())
|
|
209
|
+
.filter(|value| !value.is_empty());
|
|
210
|
+
let parent = env_parent;
|
|
211
|
+
let Some(parent) = parent else {
|
|
212
|
+
let state = crate::state::persist::load_runtime_state(workspace)
|
|
213
|
+
.unwrap_or_else(|_| serde_json::json!({}));
|
|
214
|
+
let ambiguous_nested_intent = requested_team.is_some_and(|team| {
|
|
215
|
+
looks_ambiguous_child_team_key(team) || looks_grandchild_team_key(team)
|
|
216
|
+
});
|
|
217
|
+
if has_live_runtime_teams(&state) && ambiguous_nested_intent {
|
|
218
|
+
if requested_team.is_some_and(looks_grandchild_team_key) {
|
|
219
|
+
if let Some(parent_key) = infer_parent_team_from_active_state(&state) {
|
|
220
|
+
let parent_state =
|
|
221
|
+
crate::state::projection::project_top_level_view(&state, &parent_key);
|
|
222
|
+
let parent_depth = parent_state
|
|
223
|
+
.get("team_depth")
|
|
224
|
+
.and_then(serde_json::Value::as_u64)
|
|
225
|
+
.unwrap_or(1);
|
|
226
|
+
return Ok(QuickStartDepth {
|
|
227
|
+
parent_team_key: Some(parent_key),
|
|
228
|
+
team_depth: parent_depth.saturating_add(1),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return Err(LifecycleError::RequirementUnmet(
|
|
233
|
+
"cannot infer parent team for nested quick-start; pass an explicit worker/subleader owner context"
|
|
234
|
+
.to_string(),
|
|
235
|
+
));
|
|
236
|
+
}
|
|
237
|
+
return Ok(QuickStartDepth {
|
|
238
|
+
parent_team_key: None,
|
|
239
|
+
team_depth: 1,
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
let state = crate::state::persist::load_runtime_state(workspace)
|
|
243
|
+
.unwrap_or_else(|_| serde_json::json!({}));
|
|
244
|
+
let parent_key = crate::state::projection::resolve_owner_team_id(&state, &parent)
|
|
245
|
+
.canonical_key()
|
|
246
|
+
.map(str::to_string)
|
|
247
|
+
.unwrap_or(parent);
|
|
248
|
+
let parent_state = crate::state::projection::project_top_level_view(&state, &parent_key);
|
|
249
|
+
let parent_depth = parent_state
|
|
250
|
+
.get("team_depth")
|
|
251
|
+
.and_then(serde_json::Value::as_u64)
|
|
252
|
+
.unwrap_or(1);
|
|
253
|
+
let team_depth = parent_depth.saturating_add(1);
|
|
254
|
+
Ok(QuickStartDepth {
|
|
255
|
+
parent_team_key: Some(parent_key),
|
|
256
|
+
team_depth,
|
|
257
|
+
})
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
pub(super) fn infer_parent_team_from_active_state(state: &serde_json::Value) -> Option<String> {
|
|
261
|
+
let active = explicit_active_team_key(state)?;
|
|
262
|
+
let team = state
|
|
263
|
+
.get("teams")
|
|
264
|
+
.and_then(serde_json::Value::as_object)
|
|
265
|
+
.and_then(|teams| teams.get(&active))?;
|
|
266
|
+
team_has_running_agent(team).then_some(active)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
pub(super) fn has_live_runtime_teams(state: &serde_json::Value) -> bool {
|
|
270
|
+
state
|
|
271
|
+
.get("teams")
|
|
272
|
+
.and_then(serde_json::Value::as_object)
|
|
273
|
+
.is_some_and(|teams| teams.values().any(team_has_running_agent))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
pub(super) fn team_has_running_agent(team: &serde_json::Value) -> bool {
|
|
277
|
+
team.get("agents")
|
|
278
|
+
.and_then(serde_json::Value::as_object)
|
|
279
|
+
.is_some_and(|agents| {
|
|
280
|
+
agents.values().any(|agent| {
|
|
281
|
+
agent.get("status").and_then(serde_json::Value::as_str) == Some("running")
|
|
282
|
+
})
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
pub(super) fn looks_ambiguous_child_team_key(team: &str) -> bool {
|
|
287
|
+
let team = team.trim().to_ascii_lowercase();
|
|
288
|
+
team != "child"
|
|
289
|
+
&& (team.starts_with("child-")
|
|
290
|
+
|| team.starts_with("child_")
|
|
291
|
+
|| team.starts_with("child.")
|
|
292
|
+
|| team.starts_with("child"))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
pub(super) fn looks_grandchild_team_key(team: &str) -> bool {
|
|
296
|
+
let team = team.trim().to_ascii_lowercase();
|
|
297
|
+
team == "grandchild"
|
|
298
|
+
|| team.starts_with("grandchild-")
|
|
299
|
+
|| team.starts_with("grandchild_")
|
|
300
|
+
|| team.starts_with("grandchild.")
|
|
301
|
+
|| team.starts_with("grandchild")
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
pub(super) fn annotate_team_depth(
|
|
305
|
+
state: &mut serde_json::Value,
|
|
306
|
+
parent_team_key: Option<&str>,
|
|
307
|
+
team_depth: u64,
|
|
308
|
+
) {
|
|
309
|
+
let Some(obj) = state.as_object_mut() else {
|
|
310
|
+
return;
|
|
311
|
+
};
|
|
312
|
+
obj.insert("team_depth".to_string(), serde_json::json!(team_depth));
|
|
313
|
+
if let Some(parent) = parent_team_key.filter(|value| !value.is_empty()) {
|
|
314
|
+
obj.insert("parent_team_key".to_string(), serde_json::json!(parent));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
pub(super) fn annotate_persisted_team_depth(
|
|
319
|
+
workspace: &Path,
|
|
320
|
+
team_key: &str,
|
|
321
|
+
parent_team_key: Option<&str>,
|
|
322
|
+
team_depth: u64,
|
|
323
|
+
) -> Result<(), LifecycleError> {
|
|
324
|
+
let mut state = crate::state::persist::load_runtime_state(workspace)
|
|
325
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
326
|
+
let Some(team) = state
|
|
327
|
+
.get_mut("teams")
|
|
328
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
329
|
+
.and_then(|teams| teams.get_mut(team_key))
|
|
330
|
+
else {
|
|
331
|
+
return Ok(());
|
|
332
|
+
};
|
|
333
|
+
annotate_team_depth(team, parent_team_key, team_depth);
|
|
334
|
+
crate::state::repository::StateRepository::new(workspace)
|
|
335
|
+
.save(
|
|
336
|
+
crate::state::repository::StateWriteIntent::AnnotateTeamDepth { team_key },
|
|
337
|
+
&state,
|
|
338
|
+
)
|
|
339
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
340
|
+
Ok(())
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
pub(super) fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) -> bool {
|
|
344
|
+
explicit_active_team_key(state).as_deref() == Some(team)
|
|
345
|
+
|| state
|
|
346
|
+
.get("teams")
|
|
347
|
+
.and_then(serde_json::Value::as_object)
|
|
348
|
+
.is_some_and(|teams| {
|
|
349
|
+
teams.contains_key(team)
|
|
350
|
+
|| teams
|
|
351
|
+
.values()
|
|
352
|
+
.any(|entry| json_team_identity_matches(entry, team))
|
|
353
|
+
})
|
|
354
|
+
|| crate::state::projection::team_state_key(state) == team
|
|
355
|
+
|| json_team_identity_matches(state, team)
|
|
356
|
+
|| state
|
|
357
|
+
.get("session_name")
|
|
358
|
+
.and_then(serde_json::Value::as_str)
|
|
359
|
+
.is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
pub(super) fn json_team_identity_matches(state: &serde_json::Value, team: &str) -> bool {
|
|
363
|
+
state
|
|
364
|
+
.get("team")
|
|
365
|
+
.and_then(|value| value.get("id").or_else(|| value.get("name")))
|
|
366
|
+
.and_then(serde_json::Value::as_str)
|
|
367
|
+
.is_some_and(|value| value == team)
|
|
368
|
+
|| state
|
|
369
|
+
.get("name")
|
|
370
|
+
.and_then(serde_json::Value::as_str)
|
|
371
|
+
.is_some_and(|value| value == team)
|
|
372
|
+
}
|