@team-agent/installer 0.3.3 → 0.3.4
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/diagnose.rs +1 -1
- package/crates/team-agent/src/cli/emit.rs +1 -1
- package/crates/team-agent/src/cli/mod.rs +8 -0
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +1 -1
- package/crates/team-agent/src/coordinator/tests/watch.rs +4 -2
- package/crates/team-agent/src/lifecycle/display.rs +3 -3
- package/crates/team-agent/src/lifecycle/launch.rs +342 -260
- package/crates/team-agent/src/lifecycle/mod.rs +1 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +16 -5
- package/crates/team-agent/src/lifecycle/restart/common.rs +16 -23
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +31 -25
- package/crates/team-agent/src/lifecycle/tests/core.rs +4 -4
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +2 -2
- package/crates/team-agent/src/lifecycle/types.rs +4 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +326 -0
- package/crates/team-agent/src/tmux_backend.rs +54 -0
- package/package.json +4 -4
|
@@ -72,9 +72,8 @@ pub fn launch_with_transport_in_workspace(
|
|
|
72
72
|
spec_path.display()
|
|
73
73
|
)));
|
|
74
74
|
}
|
|
75
|
-
let text = std::fs::read_to_string(spec_path)
|
|
76
|
-
LifecycleError::Compile(format!("{}: {e}", spec_path.display()))
|
|
77
|
-
})?;
|
|
75
|
+
let text = std::fs::read_to_string(spec_path)
|
|
76
|
+
.map_err(|e| LifecycleError::Compile(format!("{}: {e}", spec_path.display())))?;
|
|
78
77
|
let spec = yaml::loads(&text).map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
79
78
|
let session_name = spec_session_name(&spec);
|
|
80
79
|
let safety = effective_runtime_config(&spec)?;
|
|
@@ -101,8 +100,23 @@ pub fn launch_with_transport_in_workspace(
|
|
|
101
100
|
let started = if dry_run {
|
|
102
101
|
Vec::new()
|
|
103
102
|
} else {
|
|
104
|
-
let started = spawn_agents(
|
|
105
|
-
|
|
103
|
+
let started = spawn_agents(
|
|
104
|
+
workspace,
|
|
105
|
+
spec_path,
|
|
106
|
+
&spec,
|
|
107
|
+
&session_name,
|
|
108
|
+
&safety,
|
|
109
|
+
transport,
|
|
110
|
+
)?;
|
|
111
|
+
persist_spawn_agent_state(
|
|
112
|
+
workspace,
|
|
113
|
+
spec_path,
|
|
114
|
+
&spec,
|
|
115
|
+
&session_name,
|
|
116
|
+
transport,
|
|
117
|
+
&started,
|
|
118
|
+
&safety,
|
|
119
|
+
)?;
|
|
106
120
|
started
|
|
107
121
|
};
|
|
108
122
|
Ok(LaunchReport {
|
|
@@ -152,9 +166,19 @@ fn spawn_agents(
|
|
|
152
166
|
// has both the role instruction AND the callable Team Agent MCP capability.
|
|
153
167
|
// probe5 RED proved that `build_command(.., None, None, ..)` left the worker
|
|
154
168
|
// without `report_result`; placeholders are substituted at spawn time.
|
|
155
|
-
let
|
|
156
|
-
|
|
157
|
-
|
|
169
|
+
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
|
|
170
|
+
agent,
|
|
171
|
+
Some(agent_id_raw),
|
|
172
|
+
provider,
|
|
173
|
+
);
|
|
174
|
+
let system_prompt =
|
|
175
|
+
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
176
|
+
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
177
|
+
&command_agent,
|
|
178
|
+
provider,
|
|
179
|
+
safety,
|
|
180
|
+
)?;
|
|
181
|
+
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
158
182
|
let mcp_team_id =
|
|
159
183
|
runtime_active_team_key_for_spawn(workspace, spec_path, spec, session_name);
|
|
160
184
|
let mcp_config = adapter
|
|
@@ -163,43 +187,32 @@ fn spawn_agents(
|
|
|
163
187
|
let mcp_config = resolve_mcp_config(mcp_config, workspace, agent_id_raw, &mcp_team_id);
|
|
164
188
|
let mcp_config_path = write_worker_mcp_config(workspace, agent_id_raw, &mcp_config)?;
|
|
165
189
|
let profile_dir = team_dir.join("profiles");
|
|
166
|
-
let profile_launch =
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
.model
|
|
176
|
-
.as_deref()
|
|
177
|
-
.or(model);
|
|
190
|
+
let profile_launch =
|
|
191
|
+
crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
192
|
+
workspace,
|
|
193
|
+
agent_id_raw,
|
|
194
|
+
agent,
|
|
195
|
+
Some(&profile_dir),
|
|
196
|
+
Some(&mcp_config),
|
|
197
|
+
)?;
|
|
198
|
+
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
178
199
|
let mut plan = adapter
|
|
179
200
|
.build_command_plan(crate::provider::ProviderCommandContext {
|
|
180
201
|
auth_mode,
|
|
181
202
|
mcp_config: Some(&mcp_config),
|
|
182
|
-
system_prompt:
|
|
203
|
+
system_prompt: Some(system_prompt.as_str()),
|
|
183
204
|
model: command_model,
|
|
184
|
-
tools: &
|
|
205
|
+
tools: &resolved_tool_refs,
|
|
185
206
|
profile_launch: Some(&profile_launch),
|
|
186
207
|
})
|
|
187
208
|
.map_err(|e| LifecycleError::Provider(e.to_string()))?;
|
|
188
209
|
if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
|
|
189
210
|
point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
|
|
190
211
|
}
|
|
191
|
-
fill_spawn_placeholders_full(
|
|
192
|
-
&mut plan.argv,
|
|
193
|
-
workspace,
|
|
194
|
-
agent_id_raw,
|
|
195
|
-
Some(&mcp_team_id),
|
|
196
|
-
);
|
|
212
|
+
fill_spawn_placeholders_full(&mut plan.argv, workspace, agent_id_raw, Some(&mcp_team_id));
|
|
197
213
|
let window = WindowName::new(agent_id_raw);
|
|
198
|
-
let mut env =
|
|
199
|
-
workspace,
|
|
200
|
-
agent_id_raw,
|
|
201
|
-
Some(&mcp_team_id),
|
|
202
|
-
);
|
|
214
|
+
let mut env =
|
|
215
|
+
inherited_env_with_team_overrides(workspace, agent_id_raw, Some(&mcp_team_id));
|
|
203
216
|
apply_profile_launch_env(&mut env, &profile_launch);
|
|
204
217
|
let spawn = if started.is_empty() {
|
|
205
218
|
transport.spawn_first(session_name, &window, &plan.argv, team_dir, &env)
|
|
@@ -258,12 +271,7 @@ fn persist_spawn_agent_state(
|
|
|
258
271
|
let team_id = explicit_active_team_key(&state)
|
|
259
272
|
.unwrap_or_else(|| runtime_team_key_for_spec(spec_path, spec, session_name));
|
|
260
273
|
let worker_tmux_socket = launched_worker_tmux_socket(transport, workspace);
|
|
261
|
-
drop_worker_pane_seeded_owner(
|
|
262
|
-
&mut state,
|
|
263
|
-
&team_id,
|
|
264
|
-
started,
|
|
265
|
-
worker_tmux_socket.as_deref(),
|
|
266
|
-
);
|
|
274
|
+
drop_worker_pane_seeded_owner(&mut state, &team_id, started, worker_tmux_socket.as_deref());
|
|
267
275
|
// Only persist running state for agents whose spawn still has a live target.
|
|
268
276
|
let live_windows: BTreeSet<String> = transport
|
|
269
277
|
.list_windows(session_name)
|
|
@@ -276,10 +284,7 @@ fn persist_spawn_agent_state(
|
|
|
276
284
|
.map(|agent| agent.agent_id.as_str().to_string())
|
|
277
285
|
.collect();
|
|
278
286
|
let pane_pids_by_agent = pane_pids_by_started_agent(transport, started);
|
|
279
|
-
let profile_dir = spec_path
|
|
280
|
-
.parent()
|
|
281
|
-
.unwrap_or(workspace)
|
|
282
|
-
.join("profiles");
|
|
287
|
+
let profile_dir = spec_path.parent().unwrap_or(workspace).join("profiles");
|
|
283
288
|
let mut agents = serde_json::Map::new();
|
|
284
289
|
let mut spawn_index = 0_u32;
|
|
285
290
|
for agent in spec_agent_values(spec) {
|
|
@@ -317,9 +322,7 @@ fn persist_spawn_agent_state(
|
|
|
317
322
|
let pane_pid = pane_pids_by_agent.get(id).copied();
|
|
318
323
|
let spawned_at = spawn_timestamp_for_agent(spawn_index);
|
|
319
324
|
spawn_index = spawn_index.saturating_add(1);
|
|
320
|
-
let started_agent = started
|
|
321
|
-
.iter()
|
|
322
|
-
.find(|agent| agent.agent_id.as_str() == id);
|
|
325
|
+
let started_agent = started.iter().find(|agent| agent.agent_id.as_str() == id);
|
|
323
326
|
agents.insert(
|
|
324
327
|
id.to_string(),
|
|
325
328
|
running_agent_state(
|
|
@@ -373,7 +376,10 @@ fn agent_id_to_pane_id<'a>(started: &'a [StartedAgent], agent_id: &str) -> &'a s
|
|
|
373
376
|
.unwrap_or("")
|
|
374
377
|
}
|
|
375
378
|
|
|
376
|
-
fn save_launched_team_state(
|
|
379
|
+
fn save_launched_team_state(
|
|
380
|
+
workspace: &Path,
|
|
381
|
+
launched: &serde_json::Value,
|
|
382
|
+
) -> Result<(), LifecycleError> {
|
|
377
383
|
save_launched_team_state_for_key(workspace, launched, None)
|
|
378
384
|
}
|
|
379
385
|
|
|
@@ -404,7 +410,8 @@ fn save_launched_team_state_for_key(
|
|
|
404
410
|
};
|
|
405
411
|
let mut projected = crate::state::projection::project_top_level_view(&merged, &launched_key);
|
|
406
412
|
drop_unbound_top_level_owner(&mut projected);
|
|
407
|
-
save_runtime_state(workspace, &projected)
|
|
413
|
+
save_runtime_state(workspace, &projected)
|
|
414
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))
|
|
408
415
|
}
|
|
409
416
|
|
|
410
417
|
fn drop_bare_worker_seeded_owner(launched: &mut serde_json::Value, launched_key: &str) {
|
|
@@ -506,7 +513,11 @@ fn drop_unbound_top_level_owner(state: &mut serde_json::Value) {
|
|
|
506
513
|
}
|
|
507
514
|
}
|
|
508
515
|
|
|
509
|
-
fn drop_foreign_seeded_owner(
|
|
516
|
+
fn drop_foreign_seeded_owner(
|
|
517
|
+
existing: &serde_json::Value,
|
|
518
|
+
launched_key: &str,
|
|
519
|
+
launched: &mut serde_json::Value,
|
|
520
|
+
) {
|
|
510
521
|
let Some(pane) = launched
|
|
511
522
|
.get("team_owner")
|
|
512
523
|
.and_then(|owner| owner.get("pane_id"))
|
|
@@ -549,8 +560,7 @@ fn drop_worker_pane_seeded_owner(
|
|
|
549
560
|
.ok()
|
|
550
561
|
.filter(|value| !value.is_empty());
|
|
551
562
|
let has_leader_identity_env = has_positive_caller_leader_env();
|
|
552
|
-
let seeded_from_bare_tmux =
|
|
553
|
-
!has_leader_identity_env && tmux_pane.as_deref() == Some(pane);
|
|
563
|
+
let seeded_from_bare_tmux = !has_leader_identity_env && tmux_pane.as_deref() == Some(pane);
|
|
554
564
|
let caller_tmux_socket = crate::tmux_backend::socket_name_from_tmux_env();
|
|
555
565
|
if seeded_from_bare_tmux
|
|
556
566
|
&& (tmux_sockets_match_or_unknown(caller_tmux_socket.as_deref(), worker_tmux_socket)
|
|
@@ -563,19 +573,14 @@ fn drop_worker_pane_seeded_owner(
|
|
|
563
573
|
|
|
564
574
|
fn seeded_pane_looks_like_worker(pane: &str, started: &[StartedAgent]) -> bool {
|
|
565
575
|
pane.ends_with("-first")
|
|
566
|
-
|| started
|
|
567
|
-
.
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|| agent.target.starts_with(pane)
|
|
572
|
-
})
|
|
576
|
+
|| started.iter().any(|agent| {
|
|
577
|
+
pane == agent.target
|
|
578
|
+
|| pane.starts_with(agent.target.as_str())
|
|
579
|
+
|| agent.target.starts_with(pane)
|
|
580
|
+
})
|
|
573
581
|
}
|
|
574
582
|
|
|
575
|
-
fn launched_worker_tmux_socket(
|
|
576
|
-
transport: &dyn Transport,
|
|
577
|
-
workspace: &Path,
|
|
578
|
-
) -> Option<String> {
|
|
583
|
+
fn launched_worker_tmux_socket(transport: &dyn Transport, workspace: &Path) -> Option<String> {
|
|
579
584
|
if matches!(transport.kind(), crate::transport::BackendKind::Tmux) {
|
|
580
585
|
Some(crate::tmux_backend::socket_name_for_workspace(workspace))
|
|
581
586
|
} else {
|
|
@@ -583,10 +588,7 @@ fn launched_worker_tmux_socket(
|
|
|
583
588
|
}
|
|
584
589
|
}
|
|
585
590
|
|
|
586
|
-
fn tmux_sockets_match_or_unknown(
|
|
587
|
-
caller_socket: Option<&str>,
|
|
588
|
-
worker_socket: Option<&str>,
|
|
589
|
-
) -> bool {
|
|
591
|
+
fn tmux_sockets_match_or_unknown(caller_socket: Option<&str>, worker_socket: Option<&str>) -> bool {
|
|
590
592
|
match (caller_socket, worker_socket) {
|
|
591
593
|
(Some(caller), Some(worker)) => caller == worker,
|
|
592
594
|
(Some(_), None) => false,
|
|
@@ -595,7 +597,9 @@ fn tmux_sockets_match_or_unknown(
|
|
|
595
597
|
}
|
|
596
598
|
|
|
597
599
|
fn env_nonempty(key: &str) -> bool {
|
|
598
|
-
std::env::var(key)
|
|
600
|
+
std::env::var(key)
|
|
601
|
+
.ok()
|
|
602
|
+
.is_some_and(|value| !value.is_empty())
|
|
599
603
|
}
|
|
600
604
|
|
|
601
605
|
fn seed_unbound_launched_owner(launched: &mut serde_json::Value, launched_key: &str) {
|
|
@@ -664,7 +668,11 @@ fn unbound_launched_owner(
|
|
|
664
668
|
}))
|
|
665
669
|
}
|
|
666
670
|
|
|
667
|
-
fn owner_pane_belongs_to_other_team(
|
|
671
|
+
fn owner_pane_belongs_to_other_team(
|
|
672
|
+
existing: &serde_json::Value,
|
|
673
|
+
launched_key: &str,
|
|
674
|
+
pane: &str,
|
|
675
|
+
) -> bool {
|
|
668
676
|
existing
|
|
669
677
|
.get("teams")
|
|
670
678
|
.and_then(serde_json::Value::as_object)
|
|
@@ -700,7 +708,10 @@ fn running_agent_state(
|
|
|
700
708
|
.and_then(Value::as_str)
|
|
701
709
|
.and_then(parse_auth_mode)
|
|
702
710
|
.unwrap_or(AuthMode::Subscription);
|
|
703
|
-
let profile = agent
|
|
711
|
+
let profile = agent
|
|
712
|
+
.get("profile")
|
|
713
|
+
.map(yaml_value_to_json)
|
|
714
|
+
.unwrap_or(serde_json::Value::Null);
|
|
704
715
|
let window = agent.get("window").and_then(Value::as_str).unwrap_or(id);
|
|
705
716
|
let mcp_config = crate::provider::get_adapter(provider)
|
|
706
717
|
.mcp_config(auth_mode)
|
|
@@ -711,7 +722,10 @@ fn running_agent_state(
|
|
|
711
722
|
state.insert("status".to_string(), serde_json::json!("running"));
|
|
712
723
|
state.insert("provider".to_string(), serde_json::json!(provider));
|
|
713
724
|
state.insert("agent_id".to_string(), serde_json::json!(id));
|
|
714
|
-
state.insert(
|
|
725
|
+
state.insert(
|
|
726
|
+
"model".to_string(),
|
|
727
|
+
model.map_or(serde_json::Value::Null, |m| serde_json::json!(m)),
|
|
728
|
+
);
|
|
715
729
|
state.insert("auth_mode".to_string(), serde_json::json!(auth_mode));
|
|
716
730
|
state.insert("profile".to_string(), profile);
|
|
717
731
|
if agent.get("profile").is_some() {
|
|
@@ -737,7 +751,10 @@ fn running_agent_state(
|
|
|
737
751
|
state.insert("rollout_path".to_string(), serde_json::Value::Null);
|
|
738
752
|
state.insert("captured_at".to_string(), serde_json::Value::Null);
|
|
739
753
|
state.insert("captured_via".to_string(), serde_json::Value::Null);
|
|
740
|
-
state.insert(
|
|
754
|
+
state.insert(
|
|
755
|
+
"attribution_confidence".to_string(),
|
|
756
|
+
serde_json::Value::Null,
|
|
757
|
+
);
|
|
741
758
|
if let Some(started_agent) = started_agent {
|
|
742
759
|
persist_started_agent_plan_state(&mut state, started_agent);
|
|
743
760
|
}
|
|
@@ -847,7 +864,11 @@ pub(crate) fn write_worker_mcp_config(
|
|
|
847
864
|
Ok(path)
|
|
848
865
|
}
|
|
849
866
|
|
|
850
|
-
pub(crate) fn point_native_mcp_config_at_file(
|
|
867
|
+
pub(crate) fn point_native_mcp_config_at_file(
|
|
868
|
+
argv: &mut [String],
|
|
869
|
+
provider: Provider,
|
|
870
|
+
path: &Path,
|
|
871
|
+
) {
|
|
851
872
|
if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
|
|
852
873
|
return;
|
|
853
874
|
}
|
|
@@ -874,13 +895,19 @@ fn permissions_json(
|
|
|
874
895
|
let resolved = permissions::resolve_permissions(&AgentPermissionInput {
|
|
875
896
|
id: Some(AgentId::new(id)),
|
|
876
897
|
provider,
|
|
877
|
-
role: agent
|
|
898
|
+
role: agent
|
|
899
|
+
.get("role")
|
|
900
|
+
.and_then(Value::as_str)
|
|
901
|
+
.map(str::to_string),
|
|
878
902
|
tools,
|
|
879
903
|
})?;
|
|
880
904
|
let mut out = serde_json::Map::new();
|
|
881
905
|
out.insert("agent_id".to_string(), serde_json::json!(id));
|
|
882
906
|
out.insert("provider".to_string(), serde_json::json!(provider));
|
|
883
|
-
out.insert(
|
|
907
|
+
out.insert(
|
|
908
|
+
"tools".to_string(),
|
|
909
|
+
serde_json::json!(resolved.sorted_tool_strings()),
|
|
910
|
+
);
|
|
884
911
|
out.insert(
|
|
885
912
|
"resolved_tools".to_string(),
|
|
886
913
|
serde_json::Value::Array(
|
|
@@ -896,7 +923,10 @@ fn permissions_json(
|
|
|
896
923
|
.collect(),
|
|
897
924
|
),
|
|
898
925
|
);
|
|
899
|
-
out.insert(
|
|
926
|
+
out.insert(
|
|
927
|
+
"has_prompt_only".to_string(),
|
|
928
|
+
serde_json::json!(resolved.has_prompt_only),
|
|
929
|
+
);
|
|
900
930
|
Ok(serde_json::Value::Object(out))
|
|
901
931
|
}
|
|
902
932
|
|
|
@@ -920,9 +950,10 @@ fn spawn_timestamp_for_agent(offset_micros: u32) -> String {
|
|
|
920
950
|
match std::env::var("TEAM_AGENT_TEST_FIXED_SPAWNED_AT") {
|
|
921
951
|
Ok(value) => chrono::DateTime::parse_from_rfc3339(&value)
|
|
922
952
|
.map(|dt| {
|
|
923
|
-
(dt.with_timezone(&chrono::Utc)
|
|
924
|
-
|
|
925
|
-
|
|
953
|
+
(dt.with_timezone(&chrono::Utc)
|
|
954
|
+
+ chrono::Duration::microseconds(i64::from(offset_micros)))
|
|
955
|
+
.format("%Y-%m-%dT%H:%M:%S%.6f+00:00")
|
|
956
|
+
.to_string()
|
|
926
957
|
})
|
|
927
958
|
.unwrap_or(value),
|
|
928
959
|
Err(_) => spawn_timestamp(),
|
|
@@ -1078,7 +1109,10 @@ pub(crate) fn fill_spawn_placeholders_full(
|
|
|
1078
1109
|
*arg = workspace_text.clone();
|
|
1079
1110
|
} else if arg == "{agent_id}" {
|
|
1080
1111
|
*arg = agent_id.to_string();
|
|
1081
|
-
} else if arg.contains("{workspace}")
|
|
1112
|
+
} else if arg.contains("{workspace}")
|
|
1113
|
+
|| arg.contains("{agent_id}")
|
|
1114
|
+
|| arg.contains("{team_id}")
|
|
1115
|
+
{
|
|
1082
1116
|
*arg = arg
|
|
1083
1117
|
.replace("{workspace}", &workspace_text)
|
|
1084
1118
|
.replace("{agent_id}", agent_id)
|
|
@@ -1087,30 +1121,12 @@ pub(crate) fn fill_spawn_placeholders_full(
|
|
|
1087
1121
|
}
|
|
1088
1122
|
}
|
|
1089
1123
|
|
|
1090
|
-
fn agent_tool_strings(agent: &Value) -> Vec<String> {
|
|
1091
|
-
agent
|
|
1092
|
-
.get("tools")
|
|
1093
|
-
.and_then(Value::as_list)
|
|
1094
|
-
.map(|items| {
|
|
1095
|
-
items
|
|
1096
|
-
.iter()
|
|
1097
|
-
.filter_map(Value::as_str)
|
|
1098
|
-
.map(str::to_string)
|
|
1099
|
-
.collect()
|
|
1100
|
-
})
|
|
1101
|
-
.unwrap_or_default()
|
|
1102
|
-
}
|
|
1103
|
-
|
|
1104
1124
|
fn spec_team_id(spec: &Value) -> Option<String> {
|
|
1105
1125
|
spec.get("team")
|
|
1106
1126
|
.and_then(|v| v.get("id").or_else(|| v.get("name")))
|
|
1107
1127
|
.and_then(Value::as_str)
|
|
1108
1128
|
.map(str::to_string)
|
|
1109
|
-
.or_else(||
|
|
1110
|
-
spec.get("name")
|
|
1111
|
-
.and_then(Value::as_str)
|
|
1112
|
-
.map(str::to_string)
|
|
1113
|
-
})
|
|
1129
|
+
.or_else(|| spec.get("name").and_then(Value::as_str).map(str::to_string))
|
|
1114
1130
|
}
|
|
1115
1131
|
|
|
1116
1132
|
fn runtime_active_team_key_for_spawn(
|
|
@@ -1173,7 +1189,10 @@ fn parse_auth_mode(raw: &str) -> Option<AuthMode> {
|
|
|
1173
1189
|
}
|
|
1174
1190
|
}
|
|
1175
1191
|
|
|
1176
|
-
fn quick_start_requested_team_key<'a>(
|
|
1192
|
+
fn quick_start_requested_team_key<'a>(
|
|
1193
|
+
team_id: Option<&'a str>,
|
|
1194
|
+
name: Option<&'a str>,
|
|
1195
|
+
) -> Option<&'a str> {
|
|
1177
1196
|
team_id.or(name).filter(|team| !team.is_empty())
|
|
1178
1197
|
}
|
|
1179
1198
|
|
|
@@ -1239,7 +1258,7 @@ fn quick_start_depth_guard(
|
|
|
1239
1258
|
Ok(QuickStartDepth {
|
|
1240
1259
|
parent_team_key: Some(parent_key),
|
|
1241
1260
|
team_depth,
|
|
1242
|
-
|
|
1261
|
+
})
|
|
1243
1262
|
}
|
|
1244
1263
|
|
|
1245
1264
|
fn infer_parent_team_from_active_state(state: &serde_json::Value) -> Option<String> {
|
|
@@ -1255,9 +1274,7 @@ fn has_live_runtime_teams(state: &serde_json::Value) -> bool {
|
|
|
1255
1274
|
state
|
|
1256
1275
|
.get("teams")
|
|
1257
1276
|
.and_then(serde_json::Value::as_object)
|
|
1258
|
-
.is_some_and(|teams|
|
|
1259
|
-
teams.values().any(team_has_running_agent)
|
|
1260
|
-
})
|
|
1277
|
+
.is_some_and(|teams| teams.values().any(team_has_running_agent))
|
|
1261
1278
|
}
|
|
1262
1279
|
|
|
1263
1280
|
fn team_has_running_agent(team: &serde_json::Value) -> bool {
|
|
@@ -1265,20 +1282,18 @@ fn team_has_running_agent(team: &serde_json::Value) -> bool {
|
|
|
1265
1282
|
.and_then(serde_json::Value::as_object)
|
|
1266
1283
|
.is_some_and(|agents| {
|
|
1267
1284
|
agents.values().any(|agent| {
|
|
1268
|
-
agent
|
|
1269
|
-
.get("status")
|
|
1270
|
-
.and_then(serde_json::Value::as_str)
|
|
1271
|
-
== Some("running")
|
|
1285
|
+
agent.get("status").and_then(serde_json::Value::as_str) == Some("running")
|
|
1272
1286
|
})
|
|
1273
1287
|
})
|
|
1274
1288
|
}
|
|
1275
1289
|
|
|
1276
1290
|
fn looks_ambiguous_child_team_key(team: &str) -> bool {
|
|
1277
1291
|
let team = team.trim().to_ascii_lowercase();
|
|
1278
|
-
team != "child"
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1292
|
+
team != "child"
|
|
1293
|
+
&& (team.starts_with("child-")
|
|
1294
|
+
|| team.starts_with("child_")
|
|
1295
|
+
|| team.starts_with("child.")
|
|
1296
|
+
|| team.starts_with("child"))
|
|
1282
1297
|
}
|
|
1283
1298
|
|
|
1284
1299
|
fn looks_grandchild_team_key(team: &str) -> bool {
|
|
@@ -1290,7 +1305,11 @@ fn looks_grandchild_team_key(team: &str) -> bool {
|
|
|
1290
1305
|
|| team.starts_with("grandchild")
|
|
1291
1306
|
}
|
|
1292
1307
|
|
|
1293
|
-
fn annotate_team_depth(
|
|
1308
|
+
fn annotate_team_depth(
|
|
1309
|
+
state: &mut serde_json::Value,
|
|
1310
|
+
parent_team_key: Option<&str>,
|
|
1311
|
+
team_depth: u64,
|
|
1312
|
+
) {
|
|
1294
1313
|
let Some(obj) = state.as_object_mut() else {
|
|
1295
1314
|
return;
|
|
1296
1315
|
};
|
|
@@ -1337,9 +1356,7 @@ fn runtime_state_has_quick_start_team(state: &serde_json::Value, team: &str) ->
|
|
|
1337
1356
|
|| state
|
|
1338
1357
|
.get("session_name")
|
|
1339
1358
|
.and_then(serde_json::Value::as_str)
|
|
1340
|
-
.is_some_and(|session|
|
|
1341
|
-
session == team || session.strip_prefix("team-") == Some(team)
|
|
1342
|
-
})
|
|
1359
|
+
.is_some_and(|session| session == team || session.strip_prefix("team-") == Some(team))
|
|
1343
1360
|
}
|
|
1344
1361
|
|
|
1345
1362
|
fn json_team_identity_matches(state: &serde_json::Value, team: &str) -> bool {
|
|
@@ -1412,7 +1429,9 @@ pub fn quick_start_with_transport(
|
|
|
1412
1429
|
transport: &dyn Transport,
|
|
1413
1430
|
) -> Result<QuickStartReport, LifecycleError> {
|
|
1414
1431
|
let workspace = team_workspace(agents_dir);
|
|
1415
|
-
quick_start_with_transport_in_workspace(
|
|
1432
|
+
quick_start_with_transport_in_workspace(
|
|
1433
|
+
&workspace, agents_dir, name, yes, fresh, team_id, transport,
|
|
1434
|
+
)
|
|
1416
1435
|
}
|
|
1417
1436
|
|
|
1418
1437
|
pub fn quick_start_with_transport_in_workspace(
|
|
@@ -1468,11 +1487,12 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1468
1487
|
.map(SessionName::new),
|
|
1469
1488
|
state_path: Some(state_path),
|
|
1470
1489
|
next_actions: vec![
|
|
1471
|
-
"run restart to resume the existing team or pass --fresh to replace it"
|
|
1490
|
+
"run restart to resume the existing team or pass --fresh to replace it"
|
|
1491
|
+
.to_string(),
|
|
1472
1492
|
],
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
}
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1476
1496
|
}
|
|
1477
1497
|
// CR-040/042: repeated quick-start from one template with distinct --team-id/--name
|
|
1478
1498
|
// must NOT collide on the template-derived tmux session. Override the compiled
|
|
@@ -1488,12 +1508,12 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1488
1508
|
runtime_team_key_for_spec(&spec_path, &spec, &session_name)
|
|
1489
1509
|
});
|
|
1490
1510
|
let spec_path = agents_dir.join("team.spec.yaml");
|
|
1491
|
-
std::fs::write(&spec_path, yaml::dumps(&spec))
|
|
1492
|
-
LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
|
|
1493
|
-
})?;
|
|
1511
|
+
std::fs::write(&spec_path, yaml::dumps(&spec))
|
|
1512
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", spec_path.display())))?;
|
|
1494
1513
|
let _store = crate::message_store::MessageStore::open(&workspace)
|
|
1495
1514
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
1496
|
-
let resolved_spec_path =
|
|
1515
|
+
let resolved_spec_path =
|
|
1516
|
+
std::fs::canonicalize(&spec_path).unwrap_or_else(|_| spec_path.clone());
|
|
1497
1517
|
let state = initial_runtime_state(&spec, &resolved_spec_path, &workspace, agents_dir);
|
|
1498
1518
|
save_launched_team_state_for_key(&workspace, &state, Some(&state_team_key))?;
|
|
1499
1519
|
annotate_persisted_team_depth(
|
|
@@ -1505,14 +1525,16 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1505
1525
|
// FIX (rt-host-a real-machine finding): dry_run=false so launch_with_transport calls spawn_agents
|
|
1506
1526
|
// and really creates the tmux session + worker windows (was hardcoded true → never spawned, which
|
|
1507
1527
|
// also starved the coordinator: no session → first tick TmuxSessionMissing → run_daemon loop exits).
|
|
1508
|
-
let mut launch =
|
|
1528
|
+
let mut launch =
|
|
1529
|
+
launch_with_transport_in_workspace(&workspace, &spec_path, false, yes, true, transport)?;
|
|
1509
1530
|
annotate_persisted_team_depth(
|
|
1510
1531
|
&workspace,
|
|
1511
1532
|
&state_team_key,
|
|
1512
1533
|
team_depth.parent_team_key.as_deref(),
|
|
1513
1534
|
team_depth.team_depth,
|
|
1514
1535
|
)?;
|
|
1515
|
-
launch.leader_receiver_attached =
|
|
1536
|
+
launch.leader_receiver_attached =
|
|
1537
|
+
launched_team_receiver_is_attached(&workspace, &state_team_key);
|
|
1516
1538
|
launch.session_capture_incomplete_agents =
|
|
1517
1539
|
quick_start_session_capture_incomplete_agents(&workspace, &state_team_key);
|
|
1518
1540
|
let coordinator_workspace = crate::coordinator::WorkspacePath::new(workspace.clone());
|
|
@@ -1532,12 +1554,29 @@ pub fn quick_start_with_transport_in_workspace(
|
|
|
1532
1554
|
// asynchronously after spawn), so the verdict is PendingToolLoad — never
|
|
1533
1555
|
// bare Ready.
|
|
1534
1556
|
let worker_readiness = quick_start_worker_readiness(&workspace, &state_team_key);
|
|
1557
|
+
let attach_commands = crate::tmux_backend::attach_commands_for_windows(
|
|
1558
|
+
&workspace,
|
|
1559
|
+
&session_name,
|
|
1560
|
+
launch
|
|
1561
|
+
.started
|
|
1562
|
+
.iter()
|
|
1563
|
+
.map(|started| started.agent_id.as_str()),
|
|
1564
|
+
);
|
|
1565
|
+
let mut next_actions = vec![format!(
|
|
1566
|
+
"team compiled; real spawn is behind the transport/provider boundary; {coordinator_action}"
|
|
1567
|
+
)];
|
|
1568
|
+
next_actions.extend(attach_commands.iter().cloned());
|
|
1569
|
+
let display_backend = state
|
|
1570
|
+
.get("display_backend")
|
|
1571
|
+
.and_then(serde_json::Value::as_str)
|
|
1572
|
+
.unwrap_or("none")
|
|
1573
|
+
.to_string();
|
|
1535
1574
|
Ok(QuickStartReport::Ready {
|
|
1536
1575
|
session_name,
|
|
1537
1576
|
launch: Box::new(launch),
|
|
1538
|
-
next_actions
|
|
1539
|
-
|
|
1540
|
-
|
|
1577
|
+
next_actions,
|
|
1578
|
+
attach_commands,
|
|
1579
|
+
display_backend,
|
|
1541
1580
|
worker_readiness,
|
|
1542
1581
|
})
|
|
1543
1582
|
}
|
|
@@ -1556,7 +1595,10 @@ fn quick_start_worker_readiness(workspace: &Path, team_key: &str) -> QuickStartR
|
|
|
1556
1595
|
.and_then(serde_json::Value::as_object)
|
|
1557
1596
|
.and_then(|teams| teams.get(team_key))
|
|
1558
1597
|
.unwrap_or(&state);
|
|
1559
|
-
let Some(agents) = team_state
|
|
1598
|
+
let Some(agents) = team_state
|
|
1599
|
+
.get("agents")
|
|
1600
|
+
.and_then(serde_json::Value::as_object)
|
|
1601
|
+
else {
|
|
1560
1602
|
return QuickStartReadiness::PendingToolLoad;
|
|
1561
1603
|
};
|
|
1562
1604
|
let all_spawned = !agents.is_empty();
|
|
@@ -1575,9 +1617,12 @@ fn quick_start_worker_readiness(workspace: &Path, team_key: &str) -> QuickStartR
|
|
|
1575
1617
|
if !unhealthy.is_empty() {
|
|
1576
1618
|
unhealthy.sort();
|
|
1577
1619
|
unhealthy.dedup();
|
|
1578
|
-
QuickStartReadiness::Degraded {
|
|
1620
|
+
QuickStartReadiness::Degraded {
|
|
1621
|
+
unhealthy_agents: unhealthy,
|
|
1622
|
+
}
|
|
1579
1623
|
} else {
|
|
1580
|
-
let incomplete_agents =
|
|
1624
|
+
let incomplete_agents =
|
|
1625
|
+
crate::session_capture::incomplete_interacted_resumable_agent_ids(team_state);
|
|
1581
1626
|
let all_resumable_have_session = incomplete_agents.is_empty();
|
|
1582
1627
|
let _readiness_ready = all_spawned && all_attached_receiver && all_resumable_have_session;
|
|
1583
1628
|
QuickStartReadiness::PendingToolLoad
|
|
@@ -1621,10 +1666,7 @@ fn team_uses_fake_model_harness(team_state: &serde_json::Value) -> bool {
|
|
|
1621
1666
|
.is_some_and(|agents| {
|
|
1622
1667
|
!agents.is_empty()
|
|
1623
1668
|
&& agents.values().all(|agent| {
|
|
1624
|
-
agent
|
|
1625
|
-
.get("model")
|
|
1626
|
-
.and_then(serde_json::Value::as_str)
|
|
1627
|
-
== Some("fake")
|
|
1669
|
+
agent.get("model").and_then(serde_json::Value::as_str) == Some("fake")
|
|
1628
1670
|
})
|
|
1629
1671
|
})
|
|
1630
1672
|
}
|
|
@@ -1649,9 +1691,11 @@ fn leader_receiver_is_attached(team_state: &serde_json::Value) -> bool {
|
|
|
1649
1691
|
/// `--dangerously-*` flag,产出危险审批继承态。launch 在 inherited=false 且无 --yes 时拒。
|
|
1650
1692
|
pub fn detect_dangerous_approval() -> Result<DangerousApproval, LifecycleError> {
|
|
1651
1693
|
if let Ok(raw) = std::env::var("TEAM_AGENT_TEST_PROCESS_ANCESTRY_ARGV_JSON") {
|
|
1652
|
-
let argv_tokens = serde_json::from_str::<Vec<String>>(&raw)
|
|
1653
|
-
|
|
1654
|
-
|
|
1694
|
+
let argv_tokens = serde_json::from_str::<Vec<String>>(&raw).map_err(|e| {
|
|
1695
|
+
LifecycleError::StatePersist(format!("invalid test ancestry argv: {e}"))
|
|
1696
|
+
})?;
|
|
1697
|
+
return Ok(detect_dangerous_approval_in_argv(&argv_tokens)
|
|
1698
|
+
.unwrap_or_else(disabled_dangerous_approval));
|
|
1655
1699
|
}
|
|
1656
1700
|
for argv_tokens in process_ancestry_argv(std::process::id()) {
|
|
1657
1701
|
if let Some(detected) = detect_dangerous_approval_in_argv(&argv_tokens) {
|
|
@@ -1667,7 +1711,8 @@ fn detect_dangerous_approval_in_argv(argv_tokens: &[String]) -> Option<Dangerous
|
|
|
1667
1711
|
for token in argv_tokens {
|
|
1668
1712
|
for (provider, flag) in dangerous_leader_flags() {
|
|
1669
1713
|
if token == flag {
|
|
1670
|
-
let unexpected_binary =
|
|
1714
|
+
let unexpected_binary =
|
|
1715
|
+
!binary_matches_provider(provider, ancestry_binary_name.as_deref());
|
|
1671
1716
|
return Some(DangerousApproval {
|
|
1672
1717
|
enabled: true,
|
|
1673
1718
|
source: DangerousApprovalSource::LeaderProcess,
|
|
@@ -1857,9 +1902,9 @@ pub fn add_agent(
|
|
|
1857
1902
|
}
|
|
1858
1903
|
Err(error) => return Err(LifecycleError::TeamSelect(error.to_string())),
|
|
1859
1904
|
};
|
|
1860
|
-
let team_dir = selected
|
|
1861
|
-
.
|
|
1862
|
-
|
|
1905
|
+
let team_dir = selected.spec_workspace.ok_or_else(|| {
|
|
1906
|
+
LifecycleError::TeamSelect("active team spec workspace not found".to_string())
|
|
1907
|
+
})?;
|
|
1863
1908
|
add_agent_with_transport_at_paths(
|
|
1864
1909
|
&selected.run_workspace,
|
|
1865
1910
|
&team_dir,
|
|
@@ -1910,8 +1955,9 @@ fn add_agent_with_transport_at_paths(
|
|
|
1910
1955
|
.map(str::to_string)
|
|
1911
1956
|
.or_else(|| explicit_active_team_key(&runtime_state))
|
|
1912
1957
|
.unwrap_or_else(|| crate::state::projection::team_state_key(&runtime_state));
|
|
1913
|
-
let owner_state =
|
|
1914
|
-
|
|
1958
|
+
let owner_state =
|
|
1959
|
+
crate::state::projection::select_runtime_state(run_workspace, Some(&canonical_team_key))
|
|
1960
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
1915
1961
|
ensure_owner_allowed_for_state(&owner_state, Some(agent_id))?;
|
|
1916
1962
|
if !role_file_path.exists() {
|
|
1917
1963
|
return Err(LifecycleError::Compile(format!(
|
|
@@ -1929,9 +1975,8 @@ fn add_agent_with_transport_at_paths(
|
|
|
1929
1975
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1930
1976
|
let safety = effective_runtime_config(&spec)?;
|
|
1931
1977
|
let spec_path = team_dir.join("team.spec.yaml");
|
|
1932
|
-
std::fs::write(&spec_path, yaml::dumps(&spec))
|
|
1933
|
-
LifecycleError::StatePersist(format!("{}: {e}", spec_path.display()))
|
|
1934
|
-
})?;
|
|
1978
|
+
std::fs::write(&spec_path, yaml::dumps(&spec))
|
|
1979
|
+
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", spec_path.display())))?;
|
|
1935
1980
|
let (meta, _) = crate::compiler::read_front_matter(&dynamic_role_file)
|
|
1936
1981
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
1937
1982
|
upsert_agent_state_from_role(
|
|
@@ -1978,8 +2023,9 @@ fn upsert_agent_state_from_role(
|
|
|
1978
2023
|
dynamic_role_file: &Path,
|
|
1979
2024
|
safety: &DangerousApproval,
|
|
1980
2025
|
) -> Result<(), LifecycleError> {
|
|
1981
|
-
let mut state =
|
|
1982
|
-
|
|
2026
|
+
let mut state =
|
|
2027
|
+
crate::state::projection::select_runtime_state(workspace, Some(canonical_team_key))
|
|
2028
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
1983
2029
|
if !state.is_object() {
|
|
1984
2030
|
state = serde_json::json!({});
|
|
1985
2031
|
}
|
|
@@ -2027,10 +2073,7 @@ fn upsert_agent_state_from_role(
|
|
|
2027
2073
|
if let Some(profile) = meta.get("profile").and_then(Value::as_str) {
|
|
2028
2074
|
if let Some(obj) = entry.as_object_mut() {
|
|
2029
2075
|
obj.insert("profile".to_string(), serde_json::json!(profile));
|
|
2030
|
-
if let Some(team_dir) = dynamic_role_file
|
|
2031
|
-
.parent()
|
|
2032
|
-
.and_then(Path::parent)
|
|
2033
|
-
{
|
|
2076
|
+
if let Some(team_dir) = dynamic_role_file.parent().and_then(Path::parent) {
|
|
2034
2077
|
obj.insert(
|
|
2035
2078
|
"_profile_dir".to_string(),
|
|
2036
2079
|
serde_json::json!(team_dir.join("profiles").to_string_lossy().to_string()),
|
|
@@ -2111,9 +2154,9 @@ pub fn fork_agent_with_transport(
|
|
|
2111
2154
|
crate::state::selector::SelectorMode::RequireSpec,
|
|
2112
2155
|
)
|
|
2113
2156
|
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
2114
|
-
let spec_workspace = selected
|
|
2115
|
-
.
|
|
2116
|
-
|
|
2157
|
+
let spec_workspace = selected.spec_workspace.ok_or_else(|| {
|
|
2158
|
+
LifecycleError::TeamSelect("active team spec workspace not found".to_string())
|
|
2159
|
+
})?;
|
|
2117
2160
|
let workspace = selected.run_workspace;
|
|
2118
2161
|
let state = selected.state;
|
|
2119
2162
|
ensure_owner_allowed_for_state(&state, Some(source_agent_id))?;
|
|
@@ -2126,8 +2169,9 @@ pub fn fork_agent_with_transport(
|
|
|
2126
2169
|
"agent id already exists: {as_agent_id}"
|
|
2127
2170
|
)));
|
|
2128
2171
|
}
|
|
2129
|
-
let source_agent = find_spec_agent(&spec, source_agent_id)
|
|
2130
|
-
|
|
2172
|
+
let source_agent = find_spec_agent(&spec, source_agent_id).ok_or_else(|| {
|
|
2173
|
+
LifecycleError::RequirementUnmet(format!("unknown worker agent id: {source_agent_id}"))
|
|
2174
|
+
})?;
|
|
2131
2175
|
let session_id = state
|
|
2132
2176
|
.get("agents")
|
|
2133
2177
|
.and_then(|v| v.get(source_agent_id.as_str()))
|
|
@@ -2162,8 +2206,9 @@ pub fn fork_agent_with_transport(
|
|
|
2162
2206
|
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
2163
2207
|
std::fs::write(&spec_path, yaml::dumps(&new_spec))
|
|
2164
2208
|
.map_err(|e| LifecycleError::StatePersist(format!("{}: {e}", spec_path.display())))?;
|
|
2165
|
-
let new_agent = find_spec_agent(&new_spec, as_agent_id)
|
|
2166
|
-
|
|
2209
|
+
let new_agent = find_spec_agent(&new_spec, as_agent_id).ok_or_else(|| {
|
|
2210
|
+
LifecycleError::RequirementUnmet(format!("unknown worker agent id: {as_agent_id}"))
|
|
2211
|
+
})?;
|
|
2167
2212
|
let provider = new_agent
|
|
2168
2213
|
.get("provider")
|
|
2169
2214
|
.and_then(Value::as_str)
|
|
@@ -2185,18 +2230,26 @@ pub fn fork_agent_with_transport(
|
|
|
2185
2230
|
"{provider_str} does not support native session fork"
|
|
2186
2231
|
)));
|
|
2187
2232
|
}
|
|
2188
|
-
let role = new_agent.get("role").and_then(Value::as_str);
|
|
2189
2233
|
let model = new_agent.get("model").and_then(Value::as_str);
|
|
2190
2234
|
let safety = effective_runtime_config(&new_spec)?;
|
|
2191
|
-
let
|
|
2192
|
-
|
|
2235
|
+
let command_agent = crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
|
|
2236
|
+
new_agent,
|
|
2237
|
+
Some(as_agent_id.as_str()),
|
|
2238
|
+
provider,
|
|
2239
|
+
);
|
|
2240
|
+
let system_prompt =
|
|
2241
|
+
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
2242
|
+
let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
|
|
2243
|
+
&command_agent,
|
|
2244
|
+
provider,
|
|
2245
|
+
&safety,
|
|
2246
|
+
)?;
|
|
2247
|
+
let resolved_tool_refs: Vec<&str> = tools.iter().map(String::as_str).collect();
|
|
2193
2248
|
let fork_team = crate::messaging::leader_receiver::active_team_key(&workspace, &state);
|
|
2194
|
-
let mcp_config = adapter
|
|
2195
|
-
.
|
|
2196
|
-
.
|
|
2197
|
-
|
|
2198
|
-
LifecycleError::Provider(e.to_string())
|
|
2199
|
-
})?;
|
|
2249
|
+
let mcp_config = adapter.mcp_config(auth_mode).map_err(|e| {
|
|
2250
|
+
let _ = std::fs::write(&spec_path, text.as_bytes());
|
|
2251
|
+
LifecycleError::Provider(e.to_string())
|
|
2252
|
+
})?;
|
|
2200
2253
|
let mcp_config = resolve_mcp_config(mcp_config, &workspace, as_agent_id.as_str(), &fork_team);
|
|
2201
2254
|
let mcp_config_path = write_worker_mcp_config(&workspace, as_agent_id.as_str(), &mcp_config)
|
|
2202
2255
|
.map_err(|e| {
|
|
@@ -2204,27 +2257,24 @@ pub fn fork_agent_with_transport(
|
|
|
2204
2257
|
e
|
|
2205
2258
|
})?;
|
|
2206
2259
|
let profile_dir = spec_workspace.join("profiles");
|
|
2207
|
-
let profile_launch =
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
.model
|
|
2217
|
-
.as_deref()
|
|
2218
|
-
.or(model);
|
|
2260
|
+
let profile_launch =
|
|
2261
|
+
crate::lifecycle::profile_launch::prepare_provider_profile_launch_with_profile_dir(
|
|
2262
|
+
&workspace,
|
|
2263
|
+
as_agent_id.as_str(),
|
|
2264
|
+
new_agent,
|
|
2265
|
+
Some(&profile_dir),
|
|
2266
|
+
Some(&mcp_config),
|
|
2267
|
+
)?;
|
|
2268
|
+
let command_model = profile_launch.command_overrides.model.as_deref().or(model);
|
|
2219
2269
|
let mut plan = adapter
|
|
2220
2270
|
.fork_plan(
|
|
2221
2271
|
Some(&session_id),
|
|
2222
2272
|
crate::provider::ProviderCommandContext {
|
|
2223
2273
|
auth_mode,
|
|
2224
2274
|
mcp_config: Some(&mcp_config),
|
|
2225
|
-
system_prompt:
|
|
2275
|
+
system_prompt: Some(system_prompt.as_str()),
|
|
2226
2276
|
model: command_model,
|
|
2227
|
-
tools: &
|
|
2277
|
+
tools: &resolved_tool_refs,
|
|
2228
2278
|
profile_launch: Some(&profile_launch),
|
|
2229
2279
|
},
|
|
2230
2280
|
)
|
|
@@ -2235,14 +2285,16 @@ pub fn fork_agent_with_transport(
|
|
|
2235
2285
|
if !plan.managed_mcp_config && !profile_launch.managed_mcp_config {
|
|
2236
2286
|
point_native_mcp_config_at_file(&mut plan.argv, provider, &mcp_config_path);
|
|
2237
2287
|
}
|
|
2238
|
-
fill_spawn_placeholders_full(
|
|
2239
|
-
|
|
2240
|
-
// fork inherits the parent agent's owner team via runtime state (`active_team_key`).
|
|
2241
|
-
let mut env = inherited_env_with_team_overrides(
|
|
2288
|
+
fill_spawn_placeholders_full(
|
|
2289
|
+
&mut plan.argv,
|
|
2242
2290
|
&workspace,
|
|
2243
2291
|
as_agent_id.as_str(),
|
|
2244
2292
|
Some(&fork_team),
|
|
2245
2293
|
);
|
|
2294
|
+
let window = WindowName::new(as_agent_id.as_str());
|
|
2295
|
+
// fork inherits the parent agent's owner team via runtime state (`active_team_key`).
|
|
2296
|
+
let mut env =
|
|
2297
|
+
inherited_env_with_team_overrides(&workspace, as_agent_id.as_str(), Some(&fork_team));
|
|
2246
2298
|
apply_profile_launch_env(&mut env, &profile_launch);
|
|
2247
2299
|
// golden operations.py:336 -> _tmux_start_command_for_agent_window (runtime.py:1017-1020): branch on
|
|
2248
2300
|
// _tmux_session_exists — an ABSENT session => new-session (spawn_first), present => new-window
|
|
@@ -2324,26 +2376,25 @@ pub fn fork_agent_with_transport(
|
|
|
2324
2376
|
);
|
|
2325
2377
|
return Err(e);
|
|
2326
2378
|
}
|
|
2327
|
-
let coordinator_started =
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
})?;
|
|
2379
|
+
let coordinator_started = crate::coordinator::start_coordinator(
|
|
2380
|
+
&crate::coordinator::WorkspacePath::new(workspace.to_path_buf()),
|
|
2381
|
+
)
|
|
2382
|
+
.map(|report| report.ok)
|
|
2383
|
+
.map_err(|e| {
|
|
2384
|
+
rollback_fork_after_spawn(
|
|
2385
|
+
&workspace,
|
|
2386
|
+
&spec_path,
|
|
2387
|
+
&text,
|
|
2388
|
+
&old_state,
|
|
2389
|
+
transport,
|
|
2390
|
+
&session_name,
|
|
2391
|
+
&window,
|
|
2392
|
+
&mcp_config_path,
|
|
2393
|
+
as_agent_id,
|
|
2394
|
+
&profile_launch,
|
|
2395
|
+
);
|
|
2396
|
+
LifecycleError::StatePersist(e.to_string())
|
|
2397
|
+
})?;
|
|
2347
2398
|
Ok(ForkAgentReport {
|
|
2348
2399
|
source_agent_id: source_agent_id.clone(),
|
|
2349
2400
|
new_agent_id: as_agent_id.clone(),
|
|
@@ -2384,8 +2435,7 @@ fn maybe_fail_fork_after_spawn(step: &str) -> Result<(), LifecycleError> {
|
|
|
2384
2435
|
if reason.is_empty() {
|
|
2385
2436
|
return Ok(());
|
|
2386
2437
|
}
|
|
2387
|
-
let should_fail = reason == step
|
|
2388
|
-
|| (step == "start_coordinator" && reason == "coordinator");
|
|
2438
|
+
let should_fail = reason == step || (step == "start_coordinator" && reason == "coordinator");
|
|
2389
2439
|
if !should_fail {
|
|
2390
2440
|
return Ok(());
|
|
2391
2441
|
}
|
|
@@ -2429,16 +2479,13 @@ fn find_spec_agent<'a>(spec: &'a Value, agent_id: &AgentId) -> Option<&'a Value>
|
|
|
2429
2479
|
if leader_is_agent {
|
|
2430
2480
|
return None;
|
|
2431
2481
|
}
|
|
2432
|
-
spec.get("agents")
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
.map(|id| id == agent_id.as_str())
|
|
2440
|
-
.unwrap_or(false)
|
|
2441
|
-
})
|
|
2482
|
+
spec.get("agents")?.as_list()?.iter().find(|agent| {
|
|
2483
|
+
agent
|
|
2484
|
+
.get("id")
|
|
2485
|
+
.and_then(Value::as_str)
|
|
2486
|
+
.map(|id| id == agent_id.as_str())
|
|
2487
|
+
.unwrap_or(false)
|
|
2488
|
+
})
|
|
2442
2489
|
}
|
|
2443
2490
|
|
|
2444
2491
|
fn append_forked_agent(
|
|
@@ -2477,12 +2524,17 @@ fn append_forked_agent(
|
|
|
2477
2524
|
)?;
|
|
2478
2525
|
|
|
2479
2526
|
let Value::Map(pairs) = spec else {
|
|
2480
|
-
return Err(LifecycleError::Compile(
|
|
2527
|
+
return Err(LifecycleError::Compile(
|
|
2528
|
+
"spec root is not a map".to_string(),
|
|
2529
|
+
));
|
|
2481
2530
|
};
|
|
2482
2531
|
let mut out = Vec::new();
|
|
2483
2532
|
for (key, value) in pairs {
|
|
2484
2533
|
if key == "agents" {
|
|
2485
|
-
let mut agents = value
|
|
2534
|
+
let mut agents = value
|
|
2535
|
+
.as_list()
|
|
2536
|
+
.map(|items| items.to_vec())
|
|
2537
|
+
.unwrap_or_default();
|
|
2486
2538
|
agents.push(new_agent.clone());
|
|
2487
2539
|
out.push((key.clone(), Value::List(agents)));
|
|
2488
2540
|
} else if key == "runtime" {
|
|
@@ -2496,7 +2548,9 @@ fn append_forked_agent(
|
|
|
2496
2548
|
|
|
2497
2549
|
fn set_yaml_map_value(value: &mut Value, key: &str, next: Value) -> Result<(), LifecycleError> {
|
|
2498
2550
|
let Value::Map(pairs) = value else {
|
|
2499
|
-
return Err(LifecycleError::Compile(
|
|
2551
|
+
return Err(LifecycleError::Compile(
|
|
2552
|
+
"agent entry is not a map".to_string(),
|
|
2553
|
+
));
|
|
2500
2554
|
};
|
|
2501
2555
|
if let Some((_, existing)) = pairs.iter_mut().find(|(k, _)| k == key) {
|
|
2502
2556
|
*existing = next;
|
|
@@ -2515,10 +2569,15 @@ fn runtime_with_startup_agent(runtime: &Value, agent_id: &AgentId) -> Value {
|
|
|
2515
2569
|
for (key, value) in pairs {
|
|
2516
2570
|
if key == "startup_order" {
|
|
2517
2571
|
saw_startup = true;
|
|
2518
|
-
let mut order = value
|
|
2519
|
-
|
|
2520
|
-
.
|
|
2521
|
-
.
|
|
2572
|
+
let mut order = value
|
|
2573
|
+
.as_list()
|
|
2574
|
+
.map(|items| items.to_vec())
|
|
2575
|
+
.unwrap_or_default();
|
|
2576
|
+
let already_present = order.iter().any(|item| {
|
|
2577
|
+
item.as_str()
|
|
2578
|
+
.map(|id| id == agent_id.as_str())
|
|
2579
|
+
.unwrap_or(false)
|
|
2580
|
+
});
|
|
2522
2581
|
if !already_present {
|
|
2523
2582
|
order.push(Value::Str(agent_id.as_str().to_string()));
|
|
2524
2583
|
}
|
|
@@ -2574,18 +2633,37 @@ fn upsert_forked_agent_state(
|
|
|
2574
2633
|
let mut entry = serde_json::Map::new();
|
|
2575
2634
|
entry.insert("status".to_string(), serde_json::json!("running"));
|
|
2576
2635
|
entry.insert("provider".to_string(), serde_json::json!(provider));
|
|
2577
|
-
entry.insert(
|
|
2578
|
-
|
|
2579
|
-
|
|
2636
|
+
entry.insert(
|
|
2637
|
+
"agent_id".to_string(),
|
|
2638
|
+
serde_json::json!(as_agent_id.as_str()),
|
|
2639
|
+
);
|
|
2640
|
+
entry.insert(
|
|
2641
|
+
"window".to_string(),
|
|
2642
|
+
serde_json::json!(as_agent_id.as_str()),
|
|
2643
|
+
);
|
|
2644
|
+
entry.insert(
|
|
2645
|
+
"forked_from".to_string(),
|
|
2646
|
+
serde_json::json!(source_agent_id.as_str()),
|
|
2647
|
+
);
|
|
2580
2648
|
entry.insert(
|
|
2581
2649
|
"spawn_cwd".to_string(),
|
|
2582
2650
|
serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
|
|
2583
2651
|
);
|
|
2584
|
-
entry.insert(
|
|
2652
|
+
entry.insert(
|
|
2653
|
+
"pane_id".to_string(),
|
|
2654
|
+
serde_json::json!(spawn.pane_id.as_str()),
|
|
2655
|
+
);
|
|
2585
2656
|
if let Some(pid) = spawn.child_pid {
|
|
2586
2657
|
entry.insert("pane_pid".to_string(), serde_json::json!(pid));
|
|
2587
2658
|
}
|
|
2588
|
-
for key in [
|
|
2659
|
+
for key in [
|
|
2660
|
+
"auth_mode",
|
|
2661
|
+
"model",
|
|
2662
|
+
"model_source",
|
|
2663
|
+
"profile",
|
|
2664
|
+
"_profile_dir",
|
|
2665
|
+
"role",
|
|
2666
|
+
] {
|
|
2589
2667
|
if let Some(value) = spec_agent.get(key) {
|
|
2590
2668
|
entry.insert(key.to_string(), yaml_value_to_json(value));
|
|
2591
2669
|
}
|
|
@@ -2602,9 +2680,15 @@ fn upsert_forked_agent_state(
|
|
|
2602
2680
|
entry.insert("rollout_path".to_string(), serde_json::Value::Null);
|
|
2603
2681
|
entry.insert("captured_at".to_string(), serde_json::Value::Null);
|
|
2604
2682
|
entry.insert("captured_via".to_string(), serde_json::Value::Null);
|
|
2605
|
-
entry.insert(
|
|
2683
|
+
entry.insert(
|
|
2684
|
+
"attribution_confidence".to_string(),
|
|
2685
|
+
serde_json::Value::Null,
|
|
2686
|
+
);
|
|
2606
2687
|
persist_command_plan_state(&mut entry, plan, profile_launch);
|
|
2607
|
-
agent_map.insert(
|
|
2688
|
+
agent_map.insert(
|
|
2689
|
+
as_agent_id.as_str().to_string(),
|
|
2690
|
+
serde_json::Value::Object(entry),
|
|
2691
|
+
);
|
|
2608
2692
|
if let Some(entry) = agent_map
|
|
2609
2693
|
.get_mut(as_agent_id.as_str())
|
|
2610
2694
|
.and_then(serde_json::Value::as_object_mut)
|
|
@@ -2642,12 +2726,9 @@ pub(crate) fn ensure_owner_allowed_for_state(
|
|
|
2642
2726
|
None,
|
|
2643
2727
|
)
|
|
2644
2728
|
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
2645
|
-
if let Some(refusal) =
|
|
2646
|
-
state,
|
|
2647
|
-
|
|
2648
|
-
false,
|
|
2649
|
-
&NoopLiveness,
|
|
2650
|
-
) {
|
|
2729
|
+
if let Some(refusal) =
|
|
2730
|
+
crate::state::owner_gate::check_team_owner(state, &caller, false, &NoopLiveness)
|
|
2731
|
+
{
|
|
2651
2732
|
return Err(LifecycleError::OwnerRefused(refusal.to_string()));
|
|
2652
2733
|
}
|
|
2653
2734
|
Ok(())
|
|
@@ -2676,7 +2757,10 @@ fn initial_runtime_state(
|
|
|
2676
2757
|
let Some(id) = agent.get("id").and_then(Value::as_str) else {
|
|
2677
2758
|
continue;
|
|
2678
2759
|
};
|
|
2679
|
-
let provider = agent
|
|
2760
|
+
let provider = agent
|
|
2761
|
+
.get("provider")
|
|
2762
|
+
.and_then(Value::as_str)
|
|
2763
|
+
.unwrap_or("codex");
|
|
2680
2764
|
let role = agent.get("role").and_then(Value::as_str).unwrap_or(id);
|
|
2681
2765
|
let model = agent.get("model").and_then(Value::as_str);
|
|
2682
2766
|
let auth_mode = agent.get("auth_mode").and_then(Value::as_str);
|
|
@@ -2698,7 +2782,9 @@ fn initial_runtime_state(
|
|
|
2698
2782
|
.get("runtime")
|
|
2699
2783
|
.and_then(|runtime| runtime.get("display_backend"))
|
|
2700
2784
|
.and_then(Value::as_str)
|
|
2701
|
-
.and_then(|backend|
|
|
2785
|
+
.and_then(|backend| {
|
|
2786
|
+
serde_json::from_value::<DisplayBackend>(serde_json::json!(backend)).ok()
|
|
2787
|
+
});
|
|
2702
2788
|
let display_backend =
|
|
2703
2789
|
crate::lifecycle::display::resolve_display_backend(requested_display, None).backend;
|
|
2704
2790
|
let mut state = serde_json::Map::new();
|
|
@@ -2720,11 +2806,16 @@ fn initial_runtime_state(
|
|
|
2720
2806
|
);
|
|
2721
2807
|
state.insert(
|
|
2722
2808
|
"leader".to_string(),
|
|
2723
|
-
spec.get("leader")
|
|
2809
|
+
spec.get("leader")
|
|
2810
|
+
.map(yaml_value_to_json)
|
|
2811
|
+
.unwrap_or(serde_json::Value::Null),
|
|
2724
2812
|
);
|
|
2725
2813
|
state.insert("agents".to_string(), serde_json::Value::Object(agents));
|
|
2726
2814
|
state.insert("tasks".to_string(), spec_tasks_json(spec));
|
|
2727
|
-
state.insert(
|
|
2815
|
+
state.insert(
|
|
2816
|
+
"display_backend".to_string(),
|
|
2817
|
+
serde_json::json!(display_backend),
|
|
2818
|
+
);
|
|
2728
2819
|
let mut state = serde_json::Value::Object(state);
|
|
2729
2820
|
if !seed_launched_owner_from_env(&mut state) {
|
|
2730
2821
|
let team_id = crate::state::projection::team_state_key(&state);
|
|
@@ -2800,9 +2891,7 @@ fn has_positive_caller_leader_env() -> bool {
|
|
|
2800
2891
|
fn spec_tasks_json(spec: &Value) -> serde_json::Value {
|
|
2801
2892
|
spec.get("tasks")
|
|
2802
2893
|
.and_then(Value::as_list)
|
|
2803
|
-
.map(|tasks|
|
|
2804
|
-
serde_json::Value::Array(tasks.iter().map(yaml_value_to_json).collect())
|
|
2805
|
-
})
|
|
2894
|
+
.map(|tasks| serde_json::Value::Array(tasks.iter().map(yaml_value_to_json).collect()))
|
|
2806
2895
|
.unwrap_or_else(|| serde_json::json!([]))
|
|
2807
2896
|
}
|
|
2808
2897
|
|
|
@@ -2841,7 +2930,10 @@ fn override_spec_session_name(spec: &mut Value, session_name: &str) {
|
|
|
2841
2930
|
if let Some((_, existing)) = runtime.iter_mut().find(|(k, _)| k == "session_name") {
|
|
2842
2931
|
*existing = Value::Str(session_name.to_string());
|
|
2843
2932
|
} else {
|
|
2844
|
-
runtime.push((
|
|
2933
|
+
runtime.push((
|
|
2934
|
+
"session_name".to_string(),
|
|
2935
|
+
Value::Str(session_name.to_string()),
|
|
2936
|
+
));
|
|
2845
2937
|
}
|
|
2846
2938
|
}
|
|
2847
2939
|
Some(other) => {
|
|
@@ -2948,20 +3040,11 @@ fn disabled_dangerous_approval() -> DangerousApproval {
|
|
|
2948
3040
|
}
|
|
2949
3041
|
}
|
|
2950
3042
|
|
|
2951
|
-
pub(crate) fn effective_runtime_config_for_worker_spawn(
|
|
3043
|
+
pub(crate) fn effective_runtime_config_for_worker_spawn(
|
|
3044
|
+
) -> Result<DangerousApproval, LifecycleError> {
|
|
2952
3045
|
detect_dangerous_approval()
|
|
2953
3046
|
}
|
|
2954
3047
|
|
|
2955
|
-
pub(crate) fn worker_tool_refs(
|
|
2956
|
-
mut tools: Vec<String>,
|
|
2957
|
-
safety: &DangerousApproval,
|
|
2958
|
-
) -> Vec<String> {
|
|
2959
|
-
if safety.enabled && !tools.iter().any(|tool| tool == "dangerous_auto_approve") {
|
|
2960
|
-
tools.push("dangerous_auto_approve".to_string());
|
|
2961
|
-
}
|
|
2962
|
-
tools
|
|
2963
|
-
}
|
|
2964
|
-
|
|
2965
3048
|
fn write_launch_permission_audit(
|
|
2966
3049
|
workspace: &Path,
|
|
2967
3050
|
safety: &DangerousApproval,
|
|
@@ -3019,6 +3102,5 @@ fn agent_id_exists_in_team_dir(team_dir: &Path, agent_id: &AgentId) -> bool {
|
|
|
3019
3102
|
.exists()
|
|
3020
3103
|
}
|
|
3021
3104
|
|
|
3022
|
-
|
|
3023
3105
|
mod plan;
|
|
3024
3106
|
pub use plan::{handle_report_result, start_plan};
|