@team-agent/installer 0.5.52 → 0.5.53
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 +18 -2
- package/crates/team-agent/src/cli/emit.rs +17 -0
- package/crates/team-agent/src/cli/mod.rs +155 -4
- package/crates/team-agent/src/cli/spec.rs +3 -0
- package/crates/team-agent/src/cli/types.rs +11 -0
- package/crates/team-agent/src/diagnose/orphans.rs +24 -3
- package/crates/team-agent/src/kill_audit.rs +67 -0
- package/crates/team-agent/src/leader/lease.rs +324 -57
- package/crates/team-agent/src/leader/rediscover/tests.rs +3 -0
- package/crates/team-agent/src/leader/rediscover.rs +6 -0
- package/crates/team-agent/src/leader/start.rs +144 -23
- package/crates/team-agent/src/leader/tests/idle.rs +3 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +5 -0
- package/crates/team-agent/src/lifecycle/launch/agent_state.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +106 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +216 -208
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +36 -0
- package/crates/team-agent/src/lifecycle/launch/fork_finalize.rs +238 -0
- package/crates/team-agent/src/lifecycle/launch/fork_state.rs +101 -5
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +170 -0
- package/crates/team-agent/src/lifecycle/launch/worker_env.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +12 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +13 -3
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +110 -12
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +8 -2
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +1 -0
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +3 -1
- package/crates/team-agent/src/lifecycle/types.rs +17 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +40 -5
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +1 -1
- package/crates/team-agent/src/mcp_server/tests/wire.rs +243 -167
- package/crates/team-agent/src/mcp_server/tools.rs +18 -4
- package/crates/team-agent/src/mcp_server/types.rs +3 -0
- package/crates/team-agent/src/mcp_server/wire.rs +30 -7
- package/crates/team-agent/src/messaging/leader_channel.rs +32 -5
- package/crates/team-agent/src/messaging/leader_receiver.rs +68 -21
- package/crates/team-agent/src/messaging/tests/runtime.rs +34 -23
- package/crates/team-agent/src/provider/adapter.rs +54 -13
- package/crates/team-agent/src/provider/adapters/claude_fork.rs +122 -0
- package/crates/team-agent/src/provider/adapters/copilot_fork.rs +306 -0
- package/crates/team-agent/src/provider/adapters/mod.rs +2 -0
- package/crates/team-agent/src/provider/session/capture.rs +321 -52
- package/crates/team-agent/src/provider/session/context_fork.rs +499 -0
- package/crates/team-agent/src/provider/session/mod.rs +6 -0
- package/crates/team-agent/src/provider/session_scan/claude.rs +1 -1
- package/crates/team-agent/src/provider/session_scan/codex.rs +144 -27
- package/crates/team-agent/src/provider/session_scan/common.rs +39 -6
- package/crates/team-agent/src/provider/session_scan/copilot.rs +47 -3
- package/crates/team-agent/src/provider/session_scan.rs +3 -3
- package/crates/team-agent/src/provider/tests/copilot_fork.rs +191 -0
- package/crates/team-agent/src/provider/tests.rs +1 -0
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -16
- package/crates/team-agent/src/tmux_backend.rs +29 -2
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +8 -3
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
use super::*;
|
|
2
|
+
|
|
3
|
+
pub(super) struct ForkPostSpawnInput<'a> {
|
|
4
|
+
pub workspace: &'a Path,
|
|
5
|
+
pub transport: &'a dyn Transport,
|
|
6
|
+
pub session_name: &'a SessionName,
|
|
7
|
+
pub window: &'a WindowName,
|
|
8
|
+
pub mcp_config_path: &'a Path,
|
|
9
|
+
pub agent_id: &'a AgentId,
|
|
10
|
+
pub profile_launch: &'a crate::provider::ProviderProfileLaunch,
|
|
11
|
+
pub team_key: &'a str,
|
|
12
|
+
pub spawn: &'a crate::transport::SpawnResult,
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
pub(super) fn ensure_fork_spawn_live(input: ForkPostSpawnInput<'_>) -> Result<(), LifecycleError> {
|
|
16
|
+
let rollback = || {
|
|
17
|
+
rollback_fork_after_spawn(
|
|
18
|
+
input.workspace,
|
|
19
|
+
input.transport,
|
|
20
|
+
input.session_name,
|
|
21
|
+
input.window,
|
|
22
|
+
input.mcp_config_path,
|
|
23
|
+
input.agent_id,
|
|
24
|
+
input.profile_launch,
|
|
25
|
+
input.team_key,
|
|
26
|
+
);
|
|
27
|
+
};
|
|
28
|
+
if !matches!(
|
|
29
|
+
input.transport.liveness(&input.spawn.pane_id),
|
|
30
|
+
Ok(PaneLiveness::Live)
|
|
31
|
+
) {
|
|
32
|
+
rollback();
|
|
33
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
34
|
+
"fork process is not live after spawn: agent={} pane={}",
|
|
35
|
+
input.agent_id,
|
|
36
|
+
input.spawn.pane_id.as_str()
|
|
37
|
+
)));
|
|
38
|
+
}
|
|
39
|
+
Ok(())
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub(super) fn prepare_claude_fork_backing(
|
|
43
|
+
provider: Provider,
|
|
44
|
+
plan: &crate::provider::CommandPlan,
|
|
45
|
+
source_backing: &Path,
|
|
46
|
+
source_session_id: &crate::provider::SessionId,
|
|
47
|
+
) -> Result<Option<crate::provider::adapters::claude_fork::ClaudeForkMaterialization>, LifecycleError>
|
|
48
|
+
{
|
|
49
|
+
if !matches!(provider, Provider::Claude | Provider::ClaudeCode) {
|
|
50
|
+
return Ok(None);
|
|
51
|
+
}
|
|
52
|
+
let target_session_id = plan.expected_session_id.as_ref().ok_or_else(|| {
|
|
53
|
+
LifecycleError::Provider("claude fork plan has no snapshot session id".to_string())
|
|
54
|
+
})?;
|
|
55
|
+
crate::provider::adapters::claude_fork::materialize_claude_fork(
|
|
56
|
+
source_backing,
|
|
57
|
+
source_session_id,
|
|
58
|
+
target_session_id,
|
|
59
|
+
)
|
|
60
|
+
.map(Some)
|
|
61
|
+
.map_err(|error| {
|
|
62
|
+
LifecycleError::Provider(format!(
|
|
63
|
+
"context_fork_unavailable: claude snapshot copy failed before spawn: {error}"
|
|
64
|
+
))
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pub(super) struct ForkCoordinatorInput<'a> {
|
|
69
|
+
pub workspace: &'a Path,
|
|
70
|
+
pub team_key: &'a str,
|
|
71
|
+
pub agent_id: &'a AgentId,
|
|
72
|
+
pub transport: &'a dyn Transport,
|
|
73
|
+
pub session_name: &'a SessionName,
|
|
74
|
+
pub window: &'a WindowName,
|
|
75
|
+
pub mcp_config_path: &'a Path,
|
|
76
|
+
pub profile_launch: &'a crate::provider::ProviderProfileLaunch,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
pub(super) fn start_fork_coordinator(
|
|
80
|
+
input: ForkCoordinatorInput<'_>,
|
|
81
|
+
) -> Result<bool, LifecycleError> {
|
|
82
|
+
let rollback = || {
|
|
83
|
+
rollback_fork_after_spawn(
|
|
84
|
+
input.workspace,
|
|
85
|
+
input.transport,
|
|
86
|
+
input.session_name,
|
|
87
|
+
input.window,
|
|
88
|
+
input.mcp_config_path,
|
|
89
|
+
input.agent_id,
|
|
90
|
+
input.profile_launch,
|
|
91
|
+
input.team_key,
|
|
92
|
+
);
|
|
93
|
+
};
|
|
94
|
+
if let Err(error) = maybe_fail_fork_after_spawn("start_coordinator") {
|
|
95
|
+
rollback();
|
|
96
|
+
return Err(error);
|
|
97
|
+
}
|
|
98
|
+
crate::coordinator::start_coordinator(&crate::coordinator::WorkspacePath::new(
|
|
99
|
+
input.workspace.to_path_buf(),
|
|
100
|
+
))
|
|
101
|
+
.map(|report| report.ok)
|
|
102
|
+
.map_err(|error| {
|
|
103
|
+
rollback();
|
|
104
|
+
LifecycleError::StatePersist(error.to_string())
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
pub(super) struct ForkFinalizeInput<'a> {
|
|
109
|
+
pub workspace: &'a Path,
|
|
110
|
+
pub team_key: &'a str,
|
|
111
|
+
pub source_agent_id: &'a AgentId,
|
|
112
|
+
pub agent_id: &'a AgentId,
|
|
113
|
+
pub spec_agent: &'a Value,
|
|
114
|
+
pub safety: &'a DangerousApproval,
|
|
115
|
+
pub plan: &'a crate::provider::CommandPlan,
|
|
116
|
+
pub profile_launch: &'a crate::provider::ProviderProfileLaunch,
|
|
117
|
+
pub spawn: &'a crate::transport::SpawnResult,
|
|
118
|
+
pub profile_dir: &'a Path,
|
|
119
|
+
pub dynamic_role_file: &'a Path,
|
|
120
|
+
pub context_proof: &'a crate::provider::session::ContextForkProof,
|
|
121
|
+
pub spawned_at: &'a str,
|
|
122
|
+
pub spawn_epoch: u64,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pub(super) fn finalize_fork_state(input: ForkFinalizeInput<'_>) -> Result<(), LifecycleError> {
|
|
126
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
127
|
+
workspace: input.workspace,
|
|
128
|
+
operation: "fork-agent-finalize",
|
|
129
|
+
team: Some(input.team_key),
|
|
130
|
+
agent_id: Some(input.agent_id),
|
|
131
|
+
})?;
|
|
132
|
+
let mut next_state = crate::state::selector::resolve_active_team(
|
|
133
|
+
input.workspace,
|
|
134
|
+
Some(input.team_key),
|
|
135
|
+
crate::state::selector::SelectorMode::RequireSpec,
|
|
136
|
+
)
|
|
137
|
+
.map_err(|error| LifecycleError::TeamSelect(error.to_string()))?
|
|
138
|
+
.state;
|
|
139
|
+
upsert_forked_agent_state(
|
|
140
|
+
&mut next_state,
|
|
141
|
+
input.source_agent_id,
|
|
142
|
+
input.agent_id,
|
|
143
|
+
input.spec_agent,
|
|
144
|
+
input.safety,
|
|
145
|
+
input.plan,
|
|
146
|
+
input.profile_launch,
|
|
147
|
+
input.spawn,
|
|
148
|
+
input.workspace,
|
|
149
|
+
Some(input.profile_dir),
|
|
150
|
+
input.dynamic_role_file,
|
|
151
|
+
input.context_proof,
|
|
152
|
+
input.spawned_at,
|
|
153
|
+
input.spawn_epoch,
|
|
154
|
+
)?;
|
|
155
|
+
if let Some(agent) = next_state
|
|
156
|
+
.get_mut("agents")
|
|
157
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
158
|
+
.and_then(|agents| agents.get_mut(input.agent_id.as_str()))
|
|
159
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
160
|
+
{
|
|
161
|
+
persist_effective_approval_policy(agent, input.safety);
|
|
162
|
+
}
|
|
163
|
+
maybe_fail_fork_after_spawn("save_runtime_state")?;
|
|
164
|
+
crate::state::repository::StateRepository::new(input.workspace)
|
|
165
|
+
.save(
|
|
166
|
+
crate::state::repository::StateWriteIntent::ForkAgent {
|
|
167
|
+
team_key: input.team_key,
|
|
168
|
+
agent_id: input.agent_id.as_str(),
|
|
169
|
+
},
|
|
170
|
+
&next_state,
|
|
171
|
+
)
|
|
172
|
+
.map_err(|error| LifecycleError::StatePersist(error.to_string()))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pub(super) fn verify_fork_registration(
|
|
176
|
+
workspace: &Path,
|
|
177
|
+
team_key: &str,
|
|
178
|
+
agent_id: &AgentId,
|
|
179
|
+
spawn: &crate::transport::SpawnResult,
|
|
180
|
+
window: &WindowName,
|
|
181
|
+
) -> Result<(), LifecycleError> {
|
|
182
|
+
let saved = crate::state::projection::select_runtime_state(workspace, Some(team_key))
|
|
183
|
+
.map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
|
|
184
|
+
let agent = saved
|
|
185
|
+
.get("agents")
|
|
186
|
+
.and_then(|agents| agents.get(agent_id.as_str()))
|
|
187
|
+
.ok_or_else(|| LifecycleError::StatePersist("canonical team row is missing".to_string()))?;
|
|
188
|
+
if agent.get("pane_id").and_then(serde_json::Value::as_str) != Some(spawn.pane_id.as_str()) {
|
|
189
|
+
return Err(LifecycleError::StatePersist(
|
|
190
|
+
"canonical team pane_id does not match spawned pane".to_string(),
|
|
191
|
+
));
|
|
192
|
+
}
|
|
193
|
+
if agent.get("window").and_then(serde_json::Value::as_str) != Some(window.as_str()) {
|
|
194
|
+
return Err(LifecycleError::StatePersist(
|
|
195
|
+
"canonical team window does not match spawned window".to_string(),
|
|
196
|
+
));
|
|
197
|
+
}
|
|
198
|
+
if let Some(pid) = spawn.child_pid {
|
|
199
|
+
if agent.get("pane_pid").and_then(serde_json::Value::as_u64) != Some(u64::from(pid)) {
|
|
200
|
+
return Err(LifecycleError::StatePersist(
|
|
201
|
+
"canonical team pane_pid does not match spawned process".to_string(),
|
|
202
|
+
));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
Ok(())
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
pub(super) fn rollback_fork_after_spawn(
|
|
209
|
+
workspace: &Path,
|
|
210
|
+
transport: &dyn Transport,
|
|
211
|
+
session_name: &SessionName,
|
|
212
|
+
window: &WindowName,
|
|
213
|
+
mcp_config_path: &Path,
|
|
214
|
+
agent_id: &AgentId,
|
|
215
|
+
profile_launch: &crate::provider::ProviderProfileLaunch,
|
|
216
|
+
team_key: &str,
|
|
217
|
+
) {
|
|
218
|
+
let _ = transport.kill_window(&Target::SessionWindow {
|
|
219
|
+
session: session_name.clone(),
|
|
220
|
+
window: window.clone(),
|
|
221
|
+
});
|
|
222
|
+
if let Ok(_lock) = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
223
|
+
workspace,
|
|
224
|
+
operation: "fork-agent-rollback",
|
|
225
|
+
team: Some(team_key),
|
|
226
|
+
agent_id: Some(agent_id),
|
|
227
|
+
}) {
|
|
228
|
+
let _ = crate::lifecycle::restart::remove::remove_agent_with_transport_locked(
|
|
229
|
+
workspace,
|
|
230
|
+
agent_id,
|
|
231
|
+
true,
|
|
232
|
+
true,
|
|
233
|
+
Some(team_key),
|
|
234
|
+
transport,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
cleanup_fork_mcp_artifacts(workspace, agent_id, mcp_config_path, profile_launch);
|
|
238
|
+
}
|
|
@@ -14,6 +14,61 @@ use crate::lifecycle::lock::{acquire_agent_lifecycle_lock, LifecycleLockRequest}
|
|
|
14
14
|
|
|
15
15
|
use super::*;
|
|
16
16
|
|
|
17
|
+
pub(super) fn reserve_forked_agent_state(
|
|
18
|
+
state: &mut serde_json::Value,
|
|
19
|
+
source_agent_id: &AgentId,
|
|
20
|
+
as_agent_id: &AgentId,
|
|
21
|
+
spec_agent: &Value,
|
|
22
|
+
dynamic_role_file: &Path,
|
|
23
|
+
) -> Result<(), LifecycleError> {
|
|
24
|
+
let agents = state
|
|
25
|
+
.as_object_mut()
|
|
26
|
+
.ok_or_else(|| {
|
|
27
|
+
LifecycleError::StatePersist("runtime state root is not an object".to_string())
|
|
28
|
+
})?
|
|
29
|
+
.entry("agents".to_string())
|
|
30
|
+
.or_insert_with(|| serde_json::json!({}));
|
|
31
|
+
let agent_map = agents.as_object_mut().ok_or_else(|| {
|
|
32
|
+
LifecycleError::StatePersist("runtime state agents is not an object".to_string())
|
|
33
|
+
})?;
|
|
34
|
+
if agent_map.contains_key(as_agent_id.as_str()) {
|
|
35
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
36
|
+
"agent id already exists: {as_agent_id}"
|
|
37
|
+
)));
|
|
38
|
+
}
|
|
39
|
+
crate::lifecycle::restart::remove::clear_agent_retirement_in_state(state, as_agent_id);
|
|
40
|
+
let agents = state
|
|
41
|
+
.get_mut("agents")
|
|
42
|
+
.and_then(serde_json::Value::as_object_mut)
|
|
43
|
+
.ok_or_else(|| {
|
|
44
|
+
LifecycleError::StatePersist("runtime state agents is not an object".to_string())
|
|
45
|
+
})?;
|
|
46
|
+
let mut entry = serde_json::json!({
|
|
47
|
+
"status": "forking",
|
|
48
|
+
"agent_id": as_agent_id.as_str(),
|
|
49
|
+
"window": as_agent_id.as_str(),
|
|
50
|
+
"forked_from": source_agent_id.as_str(),
|
|
51
|
+
"dynamic_role_file": dynamic_role_file.to_string_lossy().to_string(),
|
|
52
|
+
"role_source_ownership": "managed",
|
|
53
|
+
});
|
|
54
|
+
if let Some(object) = entry.as_object_mut() {
|
|
55
|
+
for key in [
|
|
56
|
+
"provider",
|
|
57
|
+
"auth_mode",
|
|
58
|
+
"model",
|
|
59
|
+
"profile",
|
|
60
|
+
"role",
|
|
61
|
+
"effort",
|
|
62
|
+
] {
|
|
63
|
+
if let Some(value) = spec_agent.get(key) {
|
|
64
|
+
object.insert(key.to_string(), yaml_value_to_json(value));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
agents.insert(as_agent_id.as_str().to_string(), entry);
|
|
69
|
+
Ok(())
|
|
70
|
+
}
|
|
71
|
+
|
|
17
72
|
pub(super) fn maybe_fail_fork_after_spawn(step: &str) -> Result<(), LifecycleError> {
|
|
18
73
|
let Ok(reason) = std::env::var("TEAM_AGENT_TEST_FAIL_FORK_AFTER_SPAWN") else {
|
|
19
74
|
return Ok(());
|
|
@@ -42,6 +97,11 @@ pub(super) fn cleanup_fork_mcp_artifacts(
|
|
|
42
97
|
.join(".team/runtime/provider-env")
|
|
43
98
|
.join(format!("{}.env", agent_id.as_str())),
|
|
44
99
|
);
|
|
100
|
+
let _ = std::fs::remove_dir_all(
|
|
101
|
+
workspace
|
|
102
|
+
.join(".team/runtime/copilot-instructions")
|
|
103
|
+
.join(agent_id.as_str()),
|
|
104
|
+
);
|
|
45
105
|
if let Some(config_dir) = profile_launch.claude_config_dir.as_ref() {
|
|
46
106
|
let _ = std::fs::remove_dir_all(config_dir.parent().unwrap_or(config_dir));
|
|
47
107
|
}
|
|
@@ -202,6 +262,10 @@ pub(super) fn upsert_forked_agent_state(
|
|
|
202
262
|
spawn: &crate::transport::SpawnResult,
|
|
203
263
|
spawn_cwd: &Path,
|
|
204
264
|
profile_dir: Option<&Path>,
|
|
265
|
+
dynamic_role_file: &Path,
|
|
266
|
+
context_proof: &crate::provider::session::ContextForkProof,
|
|
267
|
+
spawned_at: &str,
|
|
268
|
+
spawn_epoch: u64,
|
|
205
269
|
) -> Result<(), LifecycleError> {
|
|
206
270
|
if !state.is_object() {
|
|
207
271
|
*state = serde_json::json!({});
|
|
@@ -241,6 +305,14 @@ pub(super) fn upsert_forked_agent_state(
|
|
|
241
305
|
"forked_from".to_string(),
|
|
242
306
|
serde_json::json!(source_agent_id.as_str()),
|
|
243
307
|
);
|
|
308
|
+
entry.insert(
|
|
309
|
+
"dynamic_role_file".to_string(),
|
|
310
|
+
serde_json::json!(dynamic_role_file.to_string_lossy().to_string()),
|
|
311
|
+
);
|
|
312
|
+
entry.insert(
|
|
313
|
+
"role_source_ownership".to_string(),
|
|
314
|
+
serde_json::json!("managed"),
|
|
315
|
+
);
|
|
244
316
|
entry.insert(
|
|
245
317
|
"spawn_cwd".to_string(),
|
|
246
318
|
serde_json::json!(spawn_cwd.to_string_lossy().to_string()),
|
|
@@ -249,6 +321,8 @@ pub(super) fn upsert_forked_agent_state(
|
|
|
249
321
|
"pane_id".to_string(),
|
|
250
322
|
serde_json::json!(spawn.pane_id.as_str()),
|
|
251
323
|
);
|
|
324
|
+
entry.insert("spawned_at".to_string(), serde_json::json!(spawned_at));
|
|
325
|
+
entry.insert("spawn_epoch".to_string(), serde_json::json!(spawn_epoch));
|
|
252
326
|
if let Some(pid) = spawn.child_pid {
|
|
253
327
|
entry.insert("pane_pid".to_string(), serde_json::json!(pid));
|
|
254
328
|
}
|
|
@@ -274,14 +348,36 @@ pub(super) fn upsert_forked_agent_state(
|
|
|
274
348
|
);
|
|
275
349
|
}
|
|
276
350
|
}
|
|
277
|
-
entry.insert(
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
351
|
+
entry.insert(
|
|
352
|
+
"session_id".to_string(),
|
|
353
|
+
serde_json::json!(context_proof.new_session_id.as_str()),
|
|
354
|
+
);
|
|
355
|
+
entry.insert(
|
|
356
|
+
"rollout_path".to_string(),
|
|
357
|
+
serde_json::json!(context_proof.backing_path.to_string_lossy().to_string()),
|
|
358
|
+
);
|
|
359
|
+
entry.insert(
|
|
360
|
+
"captured_at".to_string(),
|
|
361
|
+
serde_json::json!(chrono::Utc::now().to_rfc3339()),
|
|
362
|
+
);
|
|
363
|
+
entry.insert(
|
|
364
|
+
"captured_via".to_string(),
|
|
365
|
+
serde_json::json!(context_proof.captured_via),
|
|
366
|
+
);
|
|
281
367
|
entry.insert(
|
|
282
368
|
"attribution_confidence".to_string(),
|
|
283
|
-
serde_json::
|
|
369
|
+
serde_json::json!(context_proof.attribution_confidence),
|
|
284
370
|
);
|
|
371
|
+
if let Some(root) = context_proof.managed_backing_root.as_ref() {
|
|
372
|
+
entry.insert(
|
|
373
|
+
"session_backing_root".to_string(),
|
|
374
|
+
serde_json::json!(root.to_string_lossy().to_string()),
|
|
375
|
+
);
|
|
376
|
+
entry.insert(
|
|
377
|
+
"session_backing_ownership".to_string(),
|
|
378
|
+
serde_json::json!("managed"),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
285
381
|
persist_command_plan_state(&mut entry, plan, profile_launch);
|
|
286
382
|
agent_map.insert(
|
|
287
383
|
as_agent_id.as_str().to_string(),
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
use std::path::{Path, PathBuf};
|
|
2
|
+
|
|
3
|
+
use crate::lifecycle::LifecycleError;
|
|
4
|
+
use crate::model::ids::AgentId;
|
|
5
|
+
use crate::model::yaml::{self, Value};
|
|
6
|
+
|
|
7
|
+
use super::set_yaml_map_value;
|
|
8
|
+
|
|
9
|
+
pub(super) struct MaterializedRole {
|
|
10
|
+
path: PathBuf,
|
|
11
|
+
keep: bool,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
impl MaterializedRole {
|
|
15
|
+
pub(super) fn path(&self) -> &Path {
|
|
16
|
+
&self.path
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
pub(super) fn keep(&mut self) {
|
|
20
|
+
self.keep = true;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
impl Drop for MaterializedRole {
|
|
25
|
+
fn drop(&mut self) {
|
|
26
|
+
if !self.keep {
|
|
27
|
+
let _ = std::fs::remove_file(&self.path);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
pub(super) fn materialize_latest_role(
|
|
33
|
+
run_workspace: &Path,
|
|
34
|
+
team_dir: &Path,
|
|
35
|
+
state: &serde_json::Value,
|
|
36
|
+
source_agent_id: &AgentId,
|
|
37
|
+
as_agent_id: &AgentId,
|
|
38
|
+
label: Option<&str>,
|
|
39
|
+
) -> Result<MaterializedRole, LifecycleError> {
|
|
40
|
+
let source_path = resolve_role_source(run_workspace, team_dir, state, source_agent_id)?;
|
|
41
|
+
let (mut meta, body) = crate::compiler::read_front_matter(&source_path)
|
|
42
|
+
.map_err(|error| LifecycleError::Compile(error.to_string()))?;
|
|
43
|
+
let declared = meta
|
|
44
|
+
.get("name")
|
|
45
|
+
.and_then(Value::as_str)
|
|
46
|
+
.filter(|name| !name.is_empty())
|
|
47
|
+
.ok_or_else(|| {
|
|
48
|
+
LifecycleError::Compile(format!(
|
|
49
|
+
"source role file does not declare name: {}",
|
|
50
|
+
source_path.display()
|
|
51
|
+
))
|
|
52
|
+
})?;
|
|
53
|
+
if declared != source_agent_id.as_str() {
|
|
54
|
+
return Err(LifecycleError::Compile(format!(
|
|
55
|
+
"source role file declares name '{}' but source agent is '{}'",
|
|
56
|
+
declared, source_agent_id
|
|
57
|
+
)));
|
|
58
|
+
}
|
|
59
|
+
set_yaml_map_value(
|
|
60
|
+
&mut meta,
|
|
61
|
+
"name",
|
|
62
|
+
Value::Str(as_agent_id.as_str().to_string()),
|
|
63
|
+
)?;
|
|
64
|
+
if let Some(label) = label.filter(|value| !value.is_empty()) {
|
|
65
|
+
set_yaml_map_value(&mut meta, "role", Value::Str(label.to_string()))?;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let managed_dir = run_workspace.join(".team").join("dynamic-role-files");
|
|
69
|
+
std::fs::create_dir_all(&managed_dir)
|
|
70
|
+
.map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
|
|
71
|
+
let path = managed_dir.join(format!("{}.md", as_agent_id.as_str()));
|
|
72
|
+
if path.exists() {
|
|
73
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
74
|
+
"managed role file already exists: {}",
|
|
75
|
+
path.display()
|
|
76
|
+
)));
|
|
77
|
+
}
|
|
78
|
+
let rendered = format!("---\n{}---\n\n{}", yaml::dumps(&meta), body);
|
|
79
|
+
let temp = path.with_extension(format!("md.tmp-{}", std::process::id()));
|
|
80
|
+
std::fs::write(&temp, rendered.as_bytes())
|
|
81
|
+
.map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
|
|
82
|
+
if let Err(error) = std::fs::rename(&temp, &path) {
|
|
83
|
+
let _ = std::fs::remove_file(&temp);
|
|
84
|
+
return Err(LifecycleError::StatePersist(error.to_string()));
|
|
85
|
+
}
|
|
86
|
+
Ok(MaterializedRole { path, keep: false })
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
pub(super) fn clamp_materialized_role_to_leader(
|
|
90
|
+
materialized: &Path,
|
|
91
|
+
spec: &Value,
|
|
92
|
+
) -> Result<(), LifecycleError> {
|
|
93
|
+
let leader_tools = spec
|
|
94
|
+
.get("leader")
|
|
95
|
+
.and_then(|leader| leader.get("tools"))
|
|
96
|
+
.and_then(Value::as_list)
|
|
97
|
+
.map(|items| {
|
|
98
|
+
items
|
|
99
|
+
.iter()
|
|
100
|
+
.filter_map(Value::as_str)
|
|
101
|
+
.map(str::to_string)
|
|
102
|
+
.collect::<Vec<_>>()
|
|
103
|
+
})
|
|
104
|
+
.unwrap_or_default();
|
|
105
|
+
let (mut meta, body) = crate::compiler::read_front_matter(materialized)
|
|
106
|
+
.map_err(|error| LifecycleError::Compile(error.to_string()))?;
|
|
107
|
+
let requested = meta
|
|
108
|
+
.get("tools")
|
|
109
|
+
.and_then(Value::as_list)
|
|
110
|
+
.map(|items| {
|
|
111
|
+
items
|
|
112
|
+
.iter()
|
|
113
|
+
.filter_map(Value::as_str)
|
|
114
|
+
.map(str::to_string)
|
|
115
|
+
.collect::<Vec<_>>()
|
|
116
|
+
})
|
|
117
|
+
.unwrap_or_default();
|
|
118
|
+
let ceiling = crate::model::permissions::expand_tool_strings(&leader_tools)
|
|
119
|
+
.into_iter()
|
|
120
|
+
.collect::<std::collections::BTreeSet<_>>();
|
|
121
|
+
let effective = crate::model::permissions::expand_tool_strings(&requested)
|
|
122
|
+
.into_iter()
|
|
123
|
+
.filter(|tool| ceiling.contains(tool))
|
|
124
|
+
.map(Value::Str)
|
|
125
|
+
.collect::<Vec<_>>();
|
|
126
|
+
set_yaml_map_value(&mut meta, "tools", Value::List(effective))?;
|
|
127
|
+
let rendered = format!("---\n{}---\n\n{}", yaml::dumps(&meta), body);
|
|
128
|
+
std::fs::write(materialized, rendered.as_bytes())
|
|
129
|
+
.map_err(|error| LifecycleError::StatePersist(error.to_string()))
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fn resolve_role_source(
|
|
133
|
+
run_workspace: &Path,
|
|
134
|
+
team_dir: &Path,
|
|
135
|
+
state: &serde_json::Value,
|
|
136
|
+
source_agent_id: &AgentId,
|
|
137
|
+
) -> Result<PathBuf, LifecycleError> {
|
|
138
|
+
if let Some(raw) = state
|
|
139
|
+
.get("agents")
|
|
140
|
+
.and_then(|agents| agents.get(source_agent_id.as_str()))
|
|
141
|
+
.and_then(|agent| agent.get("dynamic_role_file"))
|
|
142
|
+
.and_then(serde_json::Value::as_str)
|
|
143
|
+
.filter(|value| !value.is_empty())
|
|
144
|
+
{
|
|
145
|
+
let path = PathBuf::from(raw);
|
|
146
|
+
let resolved = if path.is_absolute() {
|
|
147
|
+
path
|
|
148
|
+
} else {
|
|
149
|
+
run_workspace.join(path)
|
|
150
|
+
};
|
|
151
|
+
if resolved.is_file() {
|
|
152
|
+
return Ok(resolved);
|
|
153
|
+
}
|
|
154
|
+
return Err(LifecycleError::Compile(format!(
|
|
155
|
+
"source dynamic role file not found: {}",
|
|
156
|
+
resolved.display()
|
|
157
|
+
)));
|
|
158
|
+
}
|
|
159
|
+
let path = team_dir
|
|
160
|
+
.join("agents")
|
|
161
|
+
.join(format!("{}.md", source_agent_id.as_str()));
|
|
162
|
+
if path.is_file() {
|
|
163
|
+
Ok(path)
|
|
164
|
+
} else {
|
|
165
|
+
Err(LifecycleError::Compile(format!(
|
|
166
|
+
"source role file not found: {}",
|
|
167
|
+
path.display()
|
|
168
|
+
)))
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -18,7 +18,7 @@ pub(super) fn agent_is_paused(agent: &Value) -> bool {
|
|
|
18
18
|
matches!(agent.get("paused"), Some(Value::Bool(true)))
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
pub(
|
|
21
|
+
pub(crate) fn spawn_timestamp() -> String {
|
|
22
22
|
match std::env::var("TEAM_AGENT_TEST_FIXED_SPAWNED_AT") {
|
|
23
23
|
Ok(value) => value,
|
|
24
24
|
Err(_) => chrono::Utc::now()
|
|
@@ -234,7 +234,7 @@ pub(super) use worker_env::*;
|
|
|
234
234
|
pub(crate) use worker_env::{
|
|
235
235
|
apply_copilot_instructions_overlay, apply_mcp_auto_approval_env, apply_profile_launch_env,
|
|
236
236
|
fill_spawn_placeholders, fill_spawn_placeholders_full, inherited_env_with_team_overrides,
|
|
237
|
-
persist_command_plan_state,
|
|
237
|
+
persist_command_plan_state, spawn_timestamp,
|
|
238
238
|
};
|
|
239
239
|
|
|
240
240
|
mod identity;
|
|
@@ -285,12 +285,24 @@ pub(crate) use add_agent_state::inject_agent_into_spec;
|
|
|
285
285
|
pub(super) use add_agent_state::*;
|
|
286
286
|
|
|
287
287
|
mod fork_agent;
|
|
288
|
+
pub use fork_agent::fork_agent_with_transport;
|
|
288
289
|
pub(super) use fork_agent::*;
|
|
289
|
-
|
|
290
|
+
|
|
291
|
+
mod fork_entry;
|
|
292
|
+
pub use fork_entry::fork_agent;
|
|
290
293
|
|
|
291
294
|
mod fork_state;
|
|
292
295
|
pub(super) use fork_state::*;
|
|
293
296
|
|
|
297
|
+
mod fork_finalize;
|
|
298
|
+
pub(super) use fork_finalize::*;
|
|
299
|
+
|
|
300
|
+
mod role_source;
|
|
301
|
+
pub(super) use role_source::*;
|
|
302
|
+
|
|
303
|
+
mod clone_agent;
|
|
304
|
+
pub use clone_agent::clone_agent;
|
|
305
|
+
|
|
294
306
|
mod ownership;
|
|
295
307
|
pub(super) use ownership::*;
|
|
296
308
|
pub(crate) use ownership::{ensure_owner_allowed, ensure_owner_allowed_for_state, state_path};
|
|
@@ -1149,7 +1149,7 @@ fn mark_agent_started(
|
|
|
1149
1149
|
}
|
|
1150
1150
|
agent.insert(
|
|
1151
1151
|
"spawned_at".to_string(),
|
|
1152
|
-
serde_json::json!(
|
|
1152
|
+
serde_json::json!(spawn.spawned_at.as_str()),
|
|
1153
1153
|
);
|
|
1154
1154
|
agent.insert(
|
|
1155
1155
|
"spawn_cwd".to_string(),
|
|
@@ -2,6 +2,7 @@ use super::*;
|
|
|
2
2
|
|
|
3
3
|
pub(super) struct SpawnedAgentWindow {
|
|
4
4
|
pub spawn: crate::transport::SpawnResult,
|
|
5
|
+
pub spawned_at: String,
|
|
5
6
|
pub plan: crate::provider::CommandPlan,
|
|
6
7
|
pub profile_launch: crate::provider::ProviderProfileLaunch,
|
|
7
8
|
pub layout_placement: Option<crate::lifecycle::launch::LayoutPlacement>,
|
|
@@ -327,6 +328,14 @@ pub(super) fn spawn_agent_window(
|
|
|
327
328
|
);
|
|
328
329
|
crate::lifecycle::launch::apply_profile_launch_env(&mut env, &profile_launch);
|
|
329
330
|
crate::lifecycle::launch::apply_mcp_auto_approval_env(&mut env, safety);
|
|
331
|
+
if provider == crate::provider::Provider::Copilot {
|
|
332
|
+
crate::lifecycle::launch::apply_copilot_instructions_overlay(
|
|
333
|
+
workspace,
|
|
334
|
+
agent_id.as_str(),
|
|
335
|
+
&system_prompt,
|
|
336
|
+
&mut env,
|
|
337
|
+
)?;
|
|
338
|
+
}
|
|
330
339
|
// 0.3.28 Step 3: per Python parity, worker spawn cwd is ALWAYS `workspace`.
|
|
331
340
|
// The persisted-state `agent.spawn_cwd` override is ignored (it was a
|
|
332
341
|
// Rust-only extension that drifted to `.team/runtime/<team_key>/` after
|
|
@@ -359,6 +368,7 @@ pub(super) fn spawn_agent_window(
|
|
|
359
368
|
// failure can now be diagnosed from events.jsonl — the recorded
|
|
360
369
|
// expected_session_id == state._pending_session_id (after
|
|
361
370
|
// mark_agent_started persists the same plan tuple).
|
|
371
|
+
let spawned_at = crate::lifecycle::launch::spawn_timestamp();
|
|
362
372
|
{
|
|
363
373
|
let session_id_in_argv = plan
|
|
364
374
|
.argv
|
|
@@ -385,6 +395,7 @@ pub(super) fn spawn_agent_window(
|
|
|
385
395
|
"env_unset": env_unset,
|
|
386
396
|
"tmux_start_mode": tmux_start_mode_pre_spawn,
|
|
387
397
|
"spawn_epoch": spawn_epoch,
|
|
398
|
+
"spawned_at": spawned_at.as_str(),
|
|
388
399
|
"source": "restart",
|
|
389
400
|
"tmux_endpoint": tmux_endpoint,
|
|
390
401
|
"tmux_endpoint_source": tmux_endpoint_source.unwrap_or("transport"),
|
|
@@ -521,6 +532,7 @@ pub(super) fn spawn_agent_window(
|
|
|
521
532
|
}
|
|
522
533
|
Ok(SpawnedAgentWindow {
|
|
523
534
|
spawn,
|
|
535
|
+
spawned_at,
|
|
524
536
|
plan,
|
|
525
537
|
profile_launch,
|
|
526
538
|
layout_placement: layout_placement.cloned(),
|