@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,424 @@
|
|
|
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
|
+
/// `add_agent(workspace, agent_id, role_file_path, open_display, team)`
|
|
18
|
+
/// (`lifecycle/operations.py:143`)。动态 role doc 编译进 spec + 起 worker;失败**字节级回滚**
|
|
19
|
+
/// spec_yaml / workspace_state / **team_state.md** / role_file(Gap 15.11),每步发
|
|
20
|
+
/// `lifecycle.add_step_*` 事件(顺序被测试锁死)。
|
|
21
|
+
pub fn add_agent(
|
|
22
|
+
workspace: &Path,
|
|
23
|
+
agent_id: &AgentId,
|
|
24
|
+
role_file_path: &Path,
|
|
25
|
+
open_display: bool,
|
|
26
|
+
team: Option<&str>,
|
|
27
|
+
) -> Result<AddAgentReport, LifecycleError> {
|
|
28
|
+
let selected = match crate::state::selector::resolve_active_team(
|
|
29
|
+
workspace,
|
|
30
|
+
team,
|
|
31
|
+
crate::state::selector::SelectorMode::RequireSpec,
|
|
32
|
+
) {
|
|
33
|
+
Ok(selected) => selected,
|
|
34
|
+
Err(_) if workspace.join("TEAM.md").exists() => {
|
|
35
|
+
// **0.3.24 add-agent socket drift fix**: even on the TEAM.md fallback
|
|
36
|
+
// path (no spec yet), prefer the state-aware resolver. It reads the
|
|
37
|
+
// team's persisted `tmux_endpoint` (set at `team-agent launch` time)
|
|
38
|
+
// and routes the new agent's spawn to the SAME tmux socket the live
|
|
39
|
+
// team uses. Cold workspaces / first-agent paths safely fall back to
|
|
40
|
+
// `TmuxBackend::for_workspace(team_workspace)` inside the resolver.
|
|
41
|
+
let team_ws = team_workspace(workspace);
|
|
42
|
+
let transport =
|
|
43
|
+
crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
|
|
44
|
+
&team_ws, team,
|
|
45
|
+
)
|
|
46
|
+
.unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&team_ws));
|
|
47
|
+
return add_agent_with_transport(
|
|
48
|
+
workspace,
|
|
49
|
+
agent_id,
|
|
50
|
+
role_file_path,
|
|
51
|
+
open_display,
|
|
52
|
+
team,
|
|
53
|
+
&transport,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
Err(error) => return Err(LifecycleError::TeamSelect(error.to_string())),
|
|
57
|
+
};
|
|
58
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
59
|
+
workspace: &selected.run_workspace,
|
|
60
|
+
operation: "add-agent",
|
|
61
|
+
team: Some(selected.team_key.as_str()),
|
|
62
|
+
agent_id: Some(agent_id),
|
|
63
|
+
})?;
|
|
64
|
+
// E5 §3:compile_team 要角色定义目录(team_dir),不是 spec 落点(spec_workspace=runtime)。
|
|
65
|
+
let team_dir = selected.team_dir;
|
|
66
|
+
// **0.3.24 add-agent socket drift fix**: route to the live team's persisted
|
|
67
|
+
// tmux endpoint (NOT the workspace-hash for_workspace socket). Without this,
|
|
68
|
+
// `add-agent` spawns into an orphan socket (e.g. `ta-<hash>/termclaud`) while
|
|
69
|
+
// the live team runs on its persisted default socket — the leader can't see
|
|
70
|
+
// the new window, state never registers, and the orphaned `claude` process
|
|
71
|
+
// floats forever (macmini repro: `demo-director` startup blocker).
|
|
72
|
+
let transport = crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
|
|
73
|
+
&selected.run_workspace,
|
|
74
|
+
Some(selected.team_key.as_str()),
|
|
75
|
+
)
|
|
76
|
+
.unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace));
|
|
77
|
+
add_agent_with_transport_at_paths(
|
|
78
|
+
&selected.run_workspace,
|
|
79
|
+
&team_dir,
|
|
80
|
+
agent_id,
|
|
81
|
+
role_file_path,
|
|
82
|
+
open_display,
|
|
83
|
+
Some(selected.team_key.as_str()),
|
|
84
|
+
&transport,
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Reconcile a single existing/inconsistent seat, then reuse the normal add
|
|
89
|
+
/// path. The external role source is preserved by remove-agent ownership
|
|
90
|
+
/// checks, so this is a one-command force-recreate rather than a team restart.
|
|
91
|
+
pub fn add_agent_force(
|
|
92
|
+
workspace: &Path,
|
|
93
|
+
agent_id: &AgentId,
|
|
94
|
+
role_file_path: &Path,
|
|
95
|
+
open_display: bool,
|
|
96
|
+
team: Option<&str>,
|
|
97
|
+
force: bool,
|
|
98
|
+
) -> Result<AddAgentReport, LifecycleError> {
|
|
99
|
+
if !force {
|
|
100
|
+
return add_agent(workspace, agent_id, role_file_path, open_display, team);
|
|
101
|
+
}
|
|
102
|
+
let selected = crate::state::selector::resolve_active_team(
|
|
103
|
+
workspace,
|
|
104
|
+
team,
|
|
105
|
+
crate::state::selector::SelectorMode::RequireSpec,
|
|
106
|
+
)
|
|
107
|
+
.map_err(|error| LifecycleError::TeamSelect(error.to_string()))?;
|
|
108
|
+
let canonical_team_key = selected.team_key.clone();
|
|
109
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
110
|
+
workspace: &selected.run_workspace,
|
|
111
|
+
operation: "add-agent-force",
|
|
112
|
+
team: Some(canonical_team_key.as_str()),
|
|
113
|
+
agent_id: Some(agent_id),
|
|
114
|
+
})?;
|
|
115
|
+
let transport = crate::lifecycle::restart::lifecycle_worker_tmux_backend_for_selected_state(
|
|
116
|
+
&selected.run_workspace,
|
|
117
|
+
Some(canonical_team_key.as_str()),
|
|
118
|
+
)
|
|
119
|
+
.unwrap_or_else(|_| crate::tmux_backend::TmuxBackend::for_workspace(&selected.run_workspace));
|
|
120
|
+
force_recreate_with_transport_locked(
|
|
121
|
+
&selected.run_workspace,
|
|
122
|
+
&selected.team_dir,
|
|
123
|
+
agent_id,
|
|
124
|
+
role_file_path,
|
|
125
|
+
open_display,
|
|
126
|
+
Some(canonical_team_key.as_str()),
|
|
127
|
+
&transport,
|
|
128
|
+
)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// `add_agent` with an injected transport — after the recompile+write, wires the new worker spawn
|
|
132
|
+
/// (via start_agent_with_transport) + start_coordinator (rt-host-a sweep: recompiled but never spawned).
|
|
133
|
+
pub fn add_agent_with_transport(
|
|
134
|
+
workspace: &Path,
|
|
135
|
+
agent_id: &AgentId,
|
|
136
|
+
role_file_path: &Path,
|
|
137
|
+
open_display: bool,
|
|
138
|
+
team: Option<&str>,
|
|
139
|
+
transport: &dyn Transport,
|
|
140
|
+
) -> Result<AddAgentReport, LifecycleError> {
|
|
141
|
+
let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
|
|
142
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
143
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
144
|
+
workspace: &run_workspace,
|
|
145
|
+
operation: "add-agent",
|
|
146
|
+
team,
|
|
147
|
+
agent_id: Some(agent_id),
|
|
148
|
+
})?;
|
|
149
|
+
add_agent_with_transport_at_paths(
|
|
150
|
+
&run_workspace,
|
|
151
|
+
workspace,
|
|
152
|
+
agent_id,
|
|
153
|
+
role_file_path,
|
|
154
|
+
open_display,
|
|
155
|
+
team,
|
|
156
|
+
transport,
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
pub fn add_agent_with_transport_force(
|
|
161
|
+
workspace: &Path,
|
|
162
|
+
agent_id: &AgentId,
|
|
163
|
+
role_file_path: &Path,
|
|
164
|
+
open_display: bool,
|
|
165
|
+
team: Option<&str>,
|
|
166
|
+
force: bool,
|
|
167
|
+
transport: &dyn Transport,
|
|
168
|
+
) -> Result<AddAgentReport, LifecycleError> {
|
|
169
|
+
if !force {
|
|
170
|
+
return add_agent_with_transport(
|
|
171
|
+
workspace,
|
|
172
|
+
agent_id,
|
|
173
|
+
role_file_path,
|
|
174
|
+
open_display,
|
|
175
|
+
team,
|
|
176
|
+
transport,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
let run_workspace = crate::model::paths::canonical_run_workspace(workspace)
|
|
180
|
+
.map_err(|error| LifecycleError::StatePersist(error.to_string()))?;
|
|
181
|
+
let _lock = acquire_agent_lifecycle_lock(LifecycleLockRequest {
|
|
182
|
+
workspace: &run_workspace,
|
|
183
|
+
operation: "add-agent-force",
|
|
184
|
+
team,
|
|
185
|
+
agent_id: Some(agent_id),
|
|
186
|
+
})?;
|
|
187
|
+
force_recreate_with_transport_locked(
|
|
188
|
+
&run_workspace,
|
|
189
|
+
workspace,
|
|
190
|
+
agent_id,
|
|
191
|
+
role_file_path,
|
|
192
|
+
open_display,
|
|
193
|
+
team,
|
|
194
|
+
transport,
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pub(super) fn force_recreate_with_transport_locked(
|
|
199
|
+
run_workspace: &Path,
|
|
200
|
+
team_dir: &Path,
|
|
201
|
+
agent_id: &AgentId,
|
|
202
|
+
role_file_path: &Path,
|
|
203
|
+
open_display: bool,
|
|
204
|
+
team: Option<&str>,
|
|
205
|
+
transport: &dyn Transport,
|
|
206
|
+
) -> Result<AddAgentReport, LifecycleError> {
|
|
207
|
+
// Reject an unusable replacement source before consuming the old seat.
|
|
208
|
+
// Deeper compile/spawn failures remain covered by the transaction snapshot.
|
|
209
|
+
if !role_file_path.exists() {
|
|
210
|
+
return Err(LifecycleError::Compile(format!(
|
|
211
|
+
"role file not found: {}",
|
|
212
|
+
role_file_path.display()
|
|
213
|
+
)));
|
|
214
|
+
}
|
|
215
|
+
let snapshot = crate::lifecycle::restart::remove::ForceRecreateSnapshot::capture(
|
|
216
|
+
run_workspace,
|
|
217
|
+
agent_id,
|
|
218
|
+
team,
|
|
219
|
+
transport,
|
|
220
|
+
)?;
|
|
221
|
+
let remove = crate::lifecycle::restart::remove::remove_agent_with_transport_locked(
|
|
222
|
+
run_workspace,
|
|
223
|
+
agent_id,
|
|
224
|
+
true,
|
|
225
|
+
true,
|
|
226
|
+
team,
|
|
227
|
+
transport,
|
|
228
|
+
);
|
|
229
|
+
if let Err(error) = remove {
|
|
230
|
+
let restore_errors = snapshot.restore(team, transport);
|
|
231
|
+
return force_recreate_rollback_error(agent_id, error, restore_errors);
|
|
232
|
+
}
|
|
233
|
+
let operation = add_agent_with_transport_at_paths(
|
|
234
|
+
run_workspace,
|
|
235
|
+
team_dir,
|
|
236
|
+
agent_id,
|
|
237
|
+
role_file_path,
|
|
238
|
+
open_display,
|
|
239
|
+
team,
|
|
240
|
+
transport,
|
|
241
|
+
)
|
|
242
|
+
.and_then(|report| {
|
|
243
|
+
maybe_fail_force_recreate_after_spawn()?;
|
|
244
|
+
Ok(report)
|
|
245
|
+
})
|
|
246
|
+
.and_then(|report| {
|
|
247
|
+
snapshot.require_coherent(agent_id, team, transport)?;
|
|
248
|
+
Ok(report)
|
|
249
|
+
});
|
|
250
|
+
match operation {
|
|
251
|
+
Ok(report) => Ok(report),
|
|
252
|
+
Err(error) => {
|
|
253
|
+
let restore_errors = snapshot.restore_after_consumption(transport);
|
|
254
|
+
force_recreate_rollback_error(agent_id, error, restore_errors)
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
pub(super) fn force_recreate_rollback_error<T>(
|
|
260
|
+
agent_id: &AgentId,
|
|
261
|
+
error: LifecycleError,
|
|
262
|
+
restore_errors: Vec<String>,
|
|
263
|
+
) -> Result<T, LifecycleError> {
|
|
264
|
+
if restore_errors.is_empty() {
|
|
265
|
+
Err(error)
|
|
266
|
+
} else {
|
|
267
|
+
Err(LifecycleError::StatePersist(format!(
|
|
268
|
+
"force-recreate failed for {agent_id}: {error}; rollback_errors={}",
|
|
269
|
+
restore_errors.join("|")
|
|
270
|
+
)))
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
pub(super) fn maybe_fail_force_recreate_after_spawn() -> Result<(), LifecycleError> {
|
|
275
|
+
let Ok(reason) = std::env::var("TEAM_AGENT_TEST_FAIL_FORCE_RECREATE_AFTER_SPAWN") else {
|
|
276
|
+
return Ok(());
|
|
277
|
+
};
|
|
278
|
+
if reason.is_empty() {
|
|
279
|
+
return Ok(());
|
|
280
|
+
}
|
|
281
|
+
Err(LifecycleError::StatePersist(format!(
|
|
282
|
+
"injected force-recreate failure after spawn: {reason}"
|
|
283
|
+
)))
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
pub(super) fn add_agent_with_transport_at_paths(
|
|
287
|
+
run_workspace: &Path,
|
|
288
|
+
team_dir: &Path,
|
|
289
|
+
agent_id: &AgentId,
|
|
290
|
+
role_file_path: &Path,
|
|
291
|
+
open_display: bool,
|
|
292
|
+
team: Option<&str>,
|
|
293
|
+
transport: &dyn Transport,
|
|
294
|
+
) -> Result<AddAgentReport, LifecycleError> {
|
|
295
|
+
let runtime_state = crate::state::persist::load_runtime_state(run_workspace)
|
|
296
|
+
.map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
|
|
297
|
+
let canonical_team_key = team
|
|
298
|
+
.filter(|key| !key.is_empty())
|
|
299
|
+
.map(str::to_string)
|
|
300
|
+
.or_else(|| explicit_active_team_key(&runtime_state))
|
|
301
|
+
.unwrap_or_else(|| crate::state::projection::team_state_key(&runtime_state));
|
|
302
|
+
let owner_state =
|
|
303
|
+
crate::state::projection::select_runtime_state(run_workspace, Some(&canonical_team_key))
|
|
304
|
+
.map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
|
|
305
|
+
ensure_owner_allowed_for_state(&owner_state, Some(agent_id))?;
|
|
306
|
+
if !role_file_path.exists() {
|
|
307
|
+
return Err(LifecycleError::Compile(format!(
|
|
308
|
+
"role file not found: {}",
|
|
309
|
+
role_file_path.display()
|
|
310
|
+
)));
|
|
311
|
+
}
|
|
312
|
+
if runtime_agent_exists(&owner_state, agent_id) {
|
|
313
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
314
|
+
"agent id already exists: {agent_id}"
|
|
315
|
+
)));
|
|
316
|
+
}
|
|
317
|
+
// E5 Bug1:不再 copy role 文件进 <team_dir>/agents(自拷贝 O_TRUNC 截断反模式)。
|
|
318
|
+
// 就地读外部 role 文档编译,注入 base team spec 的 agents/routing。role 文件留在原处。
|
|
319
|
+
let mut spec = crate::compiler::compile_team(team_dir)
|
|
320
|
+
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
321
|
+
override_spec_workspace(&mut spec, run_workspace);
|
|
322
|
+
let workspace_s = spec
|
|
323
|
+
.get("team")
|
|
324
|
+
.and_then(|team| team.get("workspace"))
|
|
325
|
+
.and_then(Value::as_str)
|
|
326
|
+
.unwrap_or_else(|| team_dir.to_str().unwrap_or_default())
|
|
327
|
+
.to_string();
|
|
328
|
+
let team_meta = crate::compiler::read_front_matter(&team_dir.join("TEAM.md"))
|
|
329
|
+
.map(|(meta, _)| meta)
|
|
330
|
+
.unwrap_or(Value::Null);
|
|
331
|
+
let compiled = crate::compiler::compile_role_agent(role_file_path, &team_meta, &workspace_s)
|
|
332
|
+
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
333
|
+
if compiled.id != agent_id.as_str() {
|
|
334
|
+
return Err(LifecycleError::Compile(format!(
|
|
335
|
+
"role file declares name '{}' but add-agent id is '{}'",
|
|
336
|
+
compiled.id, agent_id
|
|
337
|
+
)));
|
|
338
|
+
}
|
|
339
|
+
inject_agent_into_spec(&mut spec, compiled.agent, &compiled.id)?;
|
|
340
|
+
let safety = effective_runtime_config(&spec)?;
|
|
341
|
+
// E5 spec 迁移:重编译的 spec 原子写到 .team/runtime/<team_key>/(不落用户目录 team_dir)。
|
|
342
|
+
let spec_path = crate::model::paths::runtime_spec_path(run_workspace, &canonical_team_key);
|
|
343
|
+
// E42 (0.3.24 P0): capture pre-write bytes for atomic rollback. If anything
|
|
344
|
+
// downstream of write_spec_atomic + upsert_agent_state_from_role + spawn
|
|
345
|
+
// fails, restore the prior bytes so the canonical spec / runtime state never
|
|
346
|
+
// get a half-written row that disagrees with what remove-agent can see.
|
|
347
|
+
let pre_spec_text = match std::fs::read_to_string(&spec_path) {
|
|
348
|
+
Ok(text) => Some(text),
|
|
349
|
+
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
|
|
350
|
+
Err(e) => return Err(LifecycleError::StatePersist(format!("read spec: {e}"))),
|
|
351
|
+
};
|
|
352
|
+
let pre_runtime_state = crate::state::persist::load_runtime_state(run_workspace).ok();
|
|
353
|
+
write_spec_atomic(&spec_path, &spec)?;
|
|
354
|
+
let (meta, _) = crate::compiler::read_front_matter(role_file_path)
|
|
355
|
+
.map_err(|e| LifecycleError::Compile(e.to_string()))?;
|
|
356
|
+
// upsert writes status="starting" (E42) — start_agent_at_paths::mark_agent_started
|
|
357
|
+
// promotes to "running" on Ok. If anything fails between here and the Ok
|
|
358
|
+
// return below, rollback restores the captured pre-bytes.
|
|
359
|
+
if let Err(error) = upsert_agent_state_from_role(
|
|
360
|
+
run_workspace,
|
|
361
|
+
&canonical_team_key,
|
|
362
|
+
agent_id,
|
|
363
|
+
&meta,
|
|
364
|
+
role_file_path,
|
|
365
|
+
&safety,
|
|
366
|
+
) {
|
|
367
|
+
rollback_add_agent_atomic(
|
|
368
|
+
run_workspace,
|
|
369
|
+
&spec_path,
|
|
370
|
+
pre_spec_text.as_deref(),
|
|
371
|
+
pre_runtime_state.as_ref(),
|
|
372
|
+
agent_id,
|
|
373
|
+
"state_upsert_failed",
|
|
374
|
+
);
|
|
375
|
+
return Err(error);
|
|
376
|
+
}
|
|
377
|
+
let started = match crate::lifecycle::restart::start_agent_at_paths(
|
|
378
|
+
run_workspace,
|
|
379
|
+
team_dir,
|
|
380
|
+
agent_id,
|
|
381
|
+
false,
|
|
382
|
+
open_display,
|
|
383
|
+
true,
|
|
384
|
+
Some(&canonical_team_key),
|
|
385
|
+
transport,
|
|
386
|
+
) {
|
|
387
|
+
Ok(started) => started,
|
|
388
|
+
Err(error) => {
|
|
389
|
+
rollback_add_agent_atomic(
|
|
390
|
+
run_workspace,
|
|
391
|
+
&spec_path,
|
|
392
|
+
pre_spec_text.as_deref(),
|
|
393
|
+
pre_runtime_state.as_ref(),
|
|
394
|
+
agent_id,
|
|
395
|
+
"start_agent_failed",
|
|
396
|
+
);
|
|
397
|
+
return Err(error);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
let (env, start_mode) = match started {
|
|
401
|
+
StartAgentOutcome::Running {
|
|
402
|
+
env, start_mode, ..
|
|
403
|
+
} => (env, start_mode),
|
|
404
|
+
StartAgentOutcome::Noop { env, .. } => (env, StartMode::Noop),
|
|
405
|
+
StartAgentOutcome::Paused { .. } => {
|
|
406
|
+
rollback_add_agent_atomic(
|
|
407
|
+
run_workspace,
|
|
408
|
+
&spec_path,
|
|
409
|
+
pre_spec_text.as_deref(),
|
|
410
|
+
pre_runtime_state.as_ref(),
|
|
411
|
+
agent_id,
|
|
412
|
+
"added_agent_paused",
|
|
413
|
+
);
|
|
414
|
+
return Err(LifecycleError::RequirementUnmet(format!(
|
|
415
|
+
"added agent {agent_id} is paused"
|
|
416
|
+
)));
|
|
417
|
+
}
|
|
418
|
+
};
|
|
419
|
+
Ok(AddAgentReport {
|
|
420
|
+
env,
|
|
421
|
+
start_mode,
|
|
422
|
+
role_file: role_file_path.to_path_buf(),
|
|
423
|
+
})
|
|
424
|
+
}
|