@team-agent/installer 0.5.100 → 0.5.101
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 +29 -7
- package/crates/team-agent/src/cli/mod.rs +24 -8
- package/crates/team-agent/src/compiler.rs +3 -0
- package/crates/team-agent/src/lifecycle/launch/add_agent_state.rs +211 -16
- package/crates/team-agent/src/lifecycle/launch/clone_agent.rs +1 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +15 -5
- package/crates/team-agent/src/lifecycle/launch/fork_entry.rs +89 -6
- package/crates/team-agent/src/lifecycle/launch/role_source.rs +168 -32
- package/crates/team-agent/src/lifecycle/launch.rs +11 -3
- package/crates/team-agent/src/lifecycle/profile_launch.rs +23 -8
- package/crates/team-agent/src/lifecycle/restart/agent.rs +2 -2
- package/crates/team-agent/src/lifecycle/restart/avatar.rs +1806 -0
- package/crates/team-agent/src/lifecycle/restart/common.rs +114 -1
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +8 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +14 -2
- package/crates/team-agent/src/lifecycle/restart.rs +11 -4
- package/crates/team-agent/src/lifecycle/tests/pi_fork_session_inheritance_red.rs +1398 -0
- package/crates/team-agent/src/lifecycle/tests.rs +1 -0
- package/crates/team-agent/src/lifecycle/types.rs +10 -2
- package/crates/team-agent/src/model/spec.rs +1 -0
- package/crates/team-agent/src/provider/types.rs +2 -0
- package/crates/team-agent/src/state/persist.rs +58 -2
- package/crates/team-agent/src/state/projection.rs +5 -1
- package/crates/team-agent/src/state/repository.rs +125 -4
- package/crates/team-agent/src/transport/test_support.rs +23 -3
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +17 -46
- package/skills/team-agent/references/team-agent-operator.md +4 -2
|
@@ -605,8 +605,8 @@ pub struct AddAgentReport {
|
|
|
605
605
|
pub role_file: PathBuf,
|
|
606
606
|
}
|
|
607
607
|
|
|
608
|
-
/// `fork_agent(...)`
|
|
609
|
-
///
|
|
608
|
+
/// `fork_agent(...)` 结果。Pi 的 snapshot fork 附带可审计的 source/target backing 细节;
|
|
609
|
+
/// 已支持的其他 provider 保持既有报告字段。
|
|
610
610
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
611
611
|
pub struct ForkAgentReport {
|
|
612
612
|
pub source_agent_id: AgentId,
|
|
@@ -614,6 +614,14 @@ pub struct ForkAgentReport {
|
|
|
614
614
|
pub env: AgentActionEnvelope,
|
|
615
615
|
pub session_id: Option<SessionId>,
|
|
616
616
|
pub backing_state: ForkBackingState,
|
|
617
|
+
pub pi_fork: Option<PiForkDetails>,
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
621
|
+
pub struct PiForkDetails {
|
|
622
|
+
pub source_session_id: SessionId,
|
|
623
|
+
pub session_id: SessionId,
|
|
624
|
+
pub backing_path: RolloutPath,
|
|
617
625
|
}
|
|
618
626
|
|
|
619
627
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
@@ -232,6 +232,7 @@ pub fn save_runtime_state(workspace: &Path, state: &Value) -> Result<(), StateEr
|
|
|
232
232
|
&[],
|
|
233
233
|
None,
|
|
234
234
|
None,
|
|
235
|
+
false,
|
|
235
236
|
)
|
|
236
237
|
}
|
|
237
238
|
|
|
@@ -251,6 +252,7 @@ pub(crate) fn save_runtime_state_with_receiver_authority(
|
|
|
251
252
|
&[],
|
|
252
253
|
Some(receiver_team_key),
|
|
253
254
|
exact_owner_seed_to_clear,
|
|
255
|
+
false,
|
|
254
256
|
)
|
|
255
257
|
}
|
|
256
258
|
|
|
@@ -289,6 +291,7 @@ pub(crate) fn save_runtime_state_with_lifecycle_topology_authority(
|
|
|
289
291
|
agent_ids,
|
|
290
292
|
None,
|
|
291
293
|
None,
|
|
294
|
+
false,
|
|
292
295
|
)
|
|
293
296
|
}
|
|
294
297
|
|
|
@@ -309,6 +312,7 @@ pub(crate) fn save_runtime_state_with_lifecycle_topology_authority_and_capture_b
|
|
|
309
312
|
topology_agent_ids,
|
|
310
313
|
None,
|
|
311
314
|
None,
|
|
315
|
+
false,
|
|
312
316
|
)
|
|
313
317
|
}
|
|
314
318
|
|
|
@@ -327,6 +331,7 @@ pub(crate) fn save_runtime_state_with_deleted_agents(
|
|
|
327
331
|
&[],
|
|
328
332
|
None,
|
|
329
333
|
None,
|
|
334
|
+
true,
|
|
330
335
|
)
|
|
331
336
|
}
|
|
332
337
|
|
|
@@ -346,6 +351,7 @@ pub(crate) fn save_runtime_state_with_team_tombstoned_agents(
|
|
|
346
351
|
&[],
|
|
347
352
|
None,
|
|
348
353
|
None,
|
|
354
|
+
false,
|
|
349
355
|
)
|
|
350
356
|
}
|
|
351
357
|
|
|
@@ -365,6 +371,7 @@ pub(crate) fn save_runtime_state_with_team_tombstone_lifecycle_topology_authorit
|
|
|
365
371
|
agent_ids,
|
|
366
372
|
None,
|
|
367
373
|
None,
|
|
374
|
+
false,
|
|
368
375
|
)
|
|
369
376
|
}
|
|
370
377
|
|
|
@@ -378,6 +385,7 @@ fn save_runtime_state_with_merge_options(
|
|
|
378
385
|
topology_update_agent_ids: &[&str],
|
|
379
386
|
receiver_update_team_key: Option<&str>,
|
|
380
387
|
exact_owner_seed_to_clear: Option<&Value>,
|
|
388
|
+
preserve_latest_fork_targets: bool,
|
|
381
389
|
) -> Result<(), StateError> {
|
|
382
390
|
persist_runtime_state_with_merge_options_and_expected(
|
|
383
391
|
workspace,
|
|
@@ -392,6 +400,7 @@ fn save_runtime_state_with_merge_options(
|
|
|
392
400
|
None,
|
|
393
401
|
None,
|
|
394
402
|
None,
|
|
403
|
+
preserve_latest_fork_targets,
|
|
395
404
|
)
|
|
396
405
|
}
|
|
397
406
|
|
|
@@ -415,6 +424,7 @@ pub(crate) fn save_runtime_state_with_receiver_authority_and_expected(
|
|
|
415
424
|
Some((expected_owner, expected_receiver)),
|
|
416
425
|
None,
|
|
417
426
|
None,
|
|
427
|
+
false,
|
|
418
428
|
)
|
|
419
429
|
}
|
|
420
430
|
|
|
@@ -438,6 +448,7 @@ pub(crate) fn save_runtime_state_with_exact_owner_receiver_cleanup(
|
|
|
438
448
|
None,
|
|
439
449
|
Some((team_key, expected_owner, expected_receiver)),
|
|
440
450
|
None,
|
|
451
|
+
false,
|
|
441
452
|
)
|
|
442
453
|
}
|
|
443
454
|
|
|
@@ -469,6 +480,7 @@ pub(crate) fn save_runtime_state_with_exact_owner_receiver_restore(
|
|
|
469
480
|
previous_owner,
|
|
470
481
|
previous_receiver,
|
|
471
482
|
)),
|
|
483
|
+
false,
|
|
472
484
|
)
|
|
473
485
|
}
|
|
474
486
|
|
|
@@ -485,6 +497,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
|
|
|
485
497
|
expected_owner_receiver: Option<(&Value, &Value)>,
|
|
486
498
|
exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
|
|
487
499
|
exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
|
|
500
|
+
preserve_latest_fork_targets: bool,
|
|
488
501
|
) -> Result<(), StateError> {
|
|
489
502
|
let path = runtime_state_path(workspace);
|
|
490
503
|
// Python `state.py:497`:先对入参 state 跑 `_migrate_state_identity`(就地填缺失 leader uuid)。
|
|
@@ -585,7 +598,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
|
|
|
585
598
|
.filter(|team_key| !team_key.is_empty())
|
|
586
599
|
.map(str::to_string)
|
|
587
600
|
.collect::<BTreeSet<_>>();
|
|
588
|
-
|
|
601
|
+
apply_persist_merge_contract_with_fork_target_preservation(
|
|
589
602
|
&mut migrated,
|
|
590
603
|
latest,
|
|
591
604
|
&deleted,
|
|
@@ -596,6 +609,7 @@ fn persist_runtime_state_with_merge_options_and_expected(
|
|
|
596
609
|
receiver_update_team_key.zip(exact_owner_seed_to_clear),
|
|
597
610
|
exact_owner_receiver_to_clear,
|
|
598
611
|
exact_owner_receiver_to_restore,
|
|
612
|
+
preserve_latest_fork_targets,
|
|
599
613
|
)?;
|
|
600
614
|
}
|
|
601
615
|
super::abnormal_watch::compact_after_merge(&mut migrated, latest.as_ref(), deleted_agent_ids);
|
|
@@ -707,6 +721,34 @@ fn apply_persist_merge_contract(
|
|
|
707
721
|
exact_owner_seed_to_clear: Option<(&str, &Value)>,
|
|
708
722
|
exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
|
|
709
723
|
exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
|
|
724
|
+
) -> Result<(), StateError> {
|
|
725
|
+
apply_persist_merge_contract_with_fork_target_preservation(
|
|
726
|
+
incoming,
|
|
727
|
+
latest,
|
|
728
|
+
deleted_agent_ids,
|
|
729
|
+
skip_capture_backfill_team_key,
|
|
730
|
+
skip_capture_backfill_agent_ids,
|
|
731
|
+
topology_updates,
|
|
732
|
+
receiver_updates,
|
|
733
|
+
exact_owner_seed_to_clear,
|
|
734
|
+
exact_owner_receiver_to_clear,
|
|
735
|
+
exact_owner_receiver_to_restore,
|
|
736
|
+
false,
|
|
737
|
+
)
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
fn apply_persist_merge_contract_with_fork_target_preservation(
|
|
741
|
+
incoming: &mut Value,
|
|
742
|
+
latest: &Value,
|
|
743
|
+
deleted_agent_ids: &BTreeSet<String>,
|
|
744
|
+
skip_capture_backfill_team_key: Option<&str>,
|
|
745
|
+
skip_capture_backfill_agent_ids: &BTreeSet<String>,
|
|
746
|
+
topology_updates: &BTreeSet<(String, String)>,
|
|
747
|
+
receiver_updates: &BTreeSet<String>,
|
|
748
|
+
exact_owner_seed_to_clear: Option<(&str, &Value)>,
|
|
749
|
+
exact_owner_receiver_to_clear: Option<(&str, &Value, &Value)>,
|
|
750
|
+
exact_owner_receiver_to_restore: Option<(&str, &Value, &Value, &Value, &Value)>,
|
|
751
|
+
preserve_latest_fork_targets: bool,
|
|
710
752
|
) -> Result<(), StateError> {
|
|
711
753
|
// A0/R1: the projection gate only guards the TOP-LEVEL passes (top-level agents and
|
|
712
754
|
// the top-level<->active-team cross projections depend on which team is active); the
|
|
@@ -740,6 +782,7 @@ fn apply_persist_merge_contract(
|
|
|
740
782
|
skip_capture_backfill_agent_ids,
|
|
741
783
|
&topology_update_agent_ids,
|
|
742
784
|
None,
|
|
785
|
+
preserve_latest_fork_targets,
|
|
743
786
|
)?;
|
|
744
787
|
// Stage 3c (identity-boundary unified plan, architect direction
|
|
745
788
|
// 2026-06-23): top-level owner copy-back removed. Pre-3c this
|
|
@@ -818,6 +861,7 @@ fn apply_persist_merge_contract(
|
|
|
818
861
|
skip_capture_backfill_agent_ids,
|
|
819
862
|
&topology_update_agent_ids,
|
|
820
863
|
team_alive,
|
|
864
|
+
preserve_latest_fork_targets,
|
|
821
865
|
)?;
|
|
822
866
|
preserve_latest_ownership_fields(
|
|
823
867
|
incoming_entry,
|
|
@@ -1196,6 +1240,7 @@ fn merge_agent_projection(
|
|
|
1196
1240
|
// the merge must NOT protect stale topology so a live sibling's write
|
|
1197
1241
|
// can go through.
|
|
1198
1242
|
team_alive: Option<bool>,
|
|
1243
|
+
preserve_latest_fork_targets: bool,
|
|
1199
1244
|
) -> Result<(), StateError> {
|
|
1200
1245
|
let Some(incoming_agents) = incoming_agents else {
|
|
1201
1246
|
return Ok(());
|
|
@@ -1212,8 +1257,17 @@ fn merge_agent_projection(
|
|
|
1212
1257
|
}
|
|
1213
1258
|
let tombstoned_for_projection =
|
|
1214
1259
|
skip_capture_backfill && skip_capture_backfill_agent_ids.contains(agent_id);
|
|
1260
|
+
let fork_target = preserve_latest_fork_targets
|
|
1261
|
+
&& latest_agent
|
|
1262
|
+
.get("forked_from")
|
|
1263
|
+
.and_then(Value::as_str)
|
|
1264
|
+
.is_some_and(|source| !source.is_empty());
|
|
1215
1265
|
match incoming_map.entry(agent_id.clone()) {
|
|
1216
1266
|
serde_json::map::Entry::Vacant(slot) => {
|
|
1267
|
+
if fork_target {
|
|
1268
|
+
slot.insert(latest_agent.clone());
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
1217
1271
|
if topology_update_agent_ids.contains(agent_id) {
|
|
1218
1272
|
continue;
|
|
1219
1273
|
}
|
|
@@ -1244,7 +1298,9 @@ fn merge_agent_projection(
|
|
|
1244
1298
|
if !tombstoned_for_projection && !topology_update_agent_ids.contains(agent_id) {
|
|
1245
1299
|
let fields = topology_conflict_fields(existing.get(), latest_agent, team_alive);
|
|
1246
1300
|
if !fields.is_empty() {
|
|
1247
|
-
if
|
|
1301
|
+
if fork_target {
|
|
1302
|
+
preserve_latest_topology(existing.get_mut(), latest_agent);
|
|
1303
|
+
} else if !topology_update_agent_ids.is_empty()
|
|
1248
1304
|
&& lifecycle_peer_snapshot_is_stale(existing.get(), latest_agent)
|
|
1249
1305
|
{
|
|
1250
1306
|
// A lifecycle writer may have loaded a complete
|
|
@@ -972,7 +972,11 @@ fn save_team_scoped_state_with_merge_options(
|
|
|
972
972
|
);
|
|
973
973
|
}
|
|
974
974
|
if tombstoned_agent_ids.is_empty() {
|
|
975
|
-
return save_runtime_state_with_deleted_agents(
|
|
975
|
+
return save_runtime_state_with_deleted_agents(
|
|
976
|
+
workspace,
|
|
977
|
+
&merged,
|
|
978
|
+
deleted_agent_ids,
|
|
979
|
+
);
|
|
976
980
|
}
|
|
977
981
|
return save_runtime_state_with_team_tombstoned_agents(
|
|
978
982
|
workspace,
|
|
@@ -128,6 +128,9 @@ impl<'a> StateRepository<'a> {
|
|
|
128
128
|
/// forward call to the same legacy helper family that the caller used
|
|
129
129
|
/// before S1a; the intent selects the family, not the merge semantics.
|
|
130
130
|
pub fn save(&self, intent: StateWriteIntent<'_>, state: &Value) -> Result<(), StateError> {
|
|
131
|
+
if let StateWriteIntent::CoordinatorTick { team_key } = &intent {
|
|
132
|
+
return self.save_coordinator_tick(team_key, state);
|
|
133
|
+
}
|
|
131
134
|
route_direct(self.workspace, intent, state)
|
|
132
135
|
}
|
|
133
136
|
|
|
@@ -149,8 +152,8 @@ impl<'a> StateRepository<'a> {
|
|
|
149
152
|
route_reapply(self.workspace, intent, state, reapply)
|
|
150
153
|
}
|
|
151
154
|
|
|
152
|
-
/// Commit a
|
|
153
|
-
/// the committed Team view for callers that produce Markdown afterwards.
|
|
155
|
+
/// Commit a bounded Team delta once against latest under state-save.
|
|
156
|
+
/// Return the committed Team view for callers that produce Markdown afterwards.
|
|
154
157
|
pub fn commit<F>(&self, intent: StateWriteIntent<'_>, update: F) -> Result<Value, StateError>
|
|
155
158
|
where
|
|
156
159
|
F: FnOnce(&mut Value),
|
|
@@ -163,7 +166,12 @@ impl<'a> StateRepository<'a> {
|
|
|
163
166
|
StateWriteIntent::McpAssignTask { .. } => None,
|
|
164
167
|
StateWriteIntent::McpUpdateStateNote { team_key } => team_key,
|
|
165
168
|
StateWriteIntent::ResultCollection { owner_team_id } => owner_team_id,
|
|
166
|
-
|
|
169
|
+
StateWriteIntent::ForkAgent { team_key, .. } => Some(team_key),
|
|
170
|
+
_ => {
|
|
171
|
+
return Err(StateError::SaveFailed(
|
|
172
|
+
"intent does not accept a bounded Team delta".into(),
|
|
173
|
+
))
|
|
174
|
+
}
|
|
167
175
|
};
|
|
168
176
|
let committed = super::persist::update_runtime_state(self.workspace, |latest| {
|
|
169
177
|
let mut selected = bounded_team_view(latest, team_key)?;
|
|
@@ -176,6 +184,64 @@ impl<'a> StateRepository<'a> {
|
|
|
176
184
|
bounded_team_view(&committed, team_key)
|
|
177
185
|
}
|
|
178
186
|
|
|
187
|
+
/// Atomically replace only the fork target row in the selected team.
|
|
188
|
+
pub(crate) fn commit_fork_agent(
|
|
189
|
+
&self,
|
|
190
|
+
intent: StateWriteIntent<'_>,
|
|
191
|
+
target_row: &Value,
|
|
192
|
+
) -> Result<(), StateError> {
|
|
193
|
+
let (team_key, agent_id) = match &intent {
|
|
194
|
+
StateWriteIntent::ForkAgent {
|
|
195
|
+
team_key,
|
|
196
|
+
agent_id,
|
|
197
|
+
} => (*team_key, *agent_id),
|
|
198
|
+
_ => {
|
|
199
|
+
return Err(StateError::SaveFailed(
|
|
200
|
+
"fork target delta requires ForkAgent intent".into(),
|
|
201
|
+
))
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
let target_row = target_row.clone();
|
|
205
|
+
let mut found = false;
|
|
206
|
+
self.commit(
|
|
207
|
+
StateWriteIntent::ForkAgent { team_key, agent_id },
|
|
208
|
+
|selected| {
|
|
209
|
+
if let Some(row) = selected
|
|
210
|
+
.get_mut("agents")
|
|
211
|
+
.and_then(Value::as_object_mut)
|
|
212
|
+
.and_then(|agents| agents.get_mut(agent_id))
|
|
213
|
+
{
|
|
214
|
+
*row = target_row;
|
|
215
|
+
found = true;
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
)?;
|
|
219
|
+
if found {
|
|
220
|
+
Ok(())
|
|
221
|
+
} else {
|
|
222
|
+
Err(StateError::SaveFailed(
|
|
223
|
+
"ForkAgent target row is missing from selected team".into(),
|
|
224
|
+
))
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
fn save_coordinator_tick(&self, team_key: &str, state: &Value) -> Result<(), StateError> {
|
|
229
|
+
let incoming = state.clone();
|
|
230
|
+
super::persist::update_runtime_state(self.workspace, |latest| {
|
|
231
|
+
let mut incoming_team = bounded_team_view(&incoming, Some(team_key))?;
|
|
232
|
+
let latest_team = bounded_team_view(latest, Some(team_key))?;
|
|
233
|
+
preserve_latest_fork_rows(&mut incoming_team, &latest_team);
|
|
234
|
+
let mut checked =
|
|
235
|
+
super::projection::merge_committed_team(latest, &incoming_team, team_key);
|
|
236
|
+
super::persist::merge_ordinary_state(&mut checked, latest)?;
|
|
237
|
+
let checked = bounded_team_view(&checked, Some(team_key))?;
|
|
238
|
+
Ok(super::projection::merge_committed_team(
|
|
239
|
+
latest, &checked, team_key,
|
|
240
|
+
))
|
|
241
|
+
})?;
|
|
242
|
+
Ok(())
|
|
243
|
+
}
|
|
244
|
+
|
|
179
245
|
/// Fence the sampled incarnation with the existing persist guards, then
|
|
180
246
|
/// commit only this tick's agent observations and coordinator bookkeeping.
|
|
181
247
|
pub fn commit_observations(
|
|
@@ -201,6 +267,7 @@ impl<'a> StateRepository<'a> {
|
|
|
201
267
|
));
|
|
202
268
|
}
|
|
203
269
|
let mut checked = observed.clone();
|
|
270
|
+
preserve_latest_fork_rows(&mut checked, latest);
|
|
204
271
|
super::persist::merge_ordinary_state(&mut checked, latest)?;
|
|
205
272
|
if !legacy_single_team_state(&checked)
|
|
206
273
|
|| sampled_identity != legacy_team_identity(&checked)
|
|
@@ -222,7 +289,13 @@ impl<'a> StateRepository<'a> {
|
|
|
222
289
|
}
|
|
223
290
|
return Ok(selected);
|
|
224
291
|
}
|
|
225
|
-
let mut
|
|
292
|
+
let mut observed_with_delta = observed.clone();
|
|
293
|
+
sync_team_observation_delta(before, &mut observed_with_delta, team_key);
|
|
294
|
+
let mut observed_team = bounded_team_view(&observed_with_delta, Some(team_key))?;
|
|
295
|
+
let latest_team = bounded_team_view(latest, Some(team_key))?;
|
|
296
|
+
preserve_latest_fork_rows(&mut observed_team, &latest_team);
|
|
297
|
+
let mut checked =
|
|
298
|
+
super::projection::merge_committed_team(latest, &observed_team, team_key);
|
|
226
299
|
super::persist::merge_ordinary_state(&mut checked, latest)?;
|
|
227
300
|
let checked = bounded_team_view(&checked, Some(team_key))?;
|
|
228
301
|
let mut selected = bounded_team_view(latest, Some(team_key))?;
|
|
@@ -231,12 +304,32 @@ impl<'a> StateRepository<'a> {
|
|
|
231
304
|
apply_observation_delta(&before[field], &checked[field], &mut selected[field]);
|
|
232
305
|
}
|
|
233
306
|
}
|
|
307
|
+
sync_team_observation_delta(before, &mut selected, team_key);
|
|
234
308
|
Ok(super::projection::merge_committed_team(latest, &selected, team_key))
|
|
235
309
|
})?;
|
|
236
310
|
Ok(())
|
|
237
311
|
}
|
|
238
312
|
}
|
|
239
313
|
|
|
314
|
+
// Coordinator observations do not own fork-target lifecycle rows.
|
|
315
|
+
fn preserve_latest_fork_rows(incoming: &mut Value, latest: &Value) {
|
|
316
|
+
let Some(latest_agents) = latest.get("agents").and_then(Value::as_object) else {
|
|
317
|
+
return;
|
|
318
|
+
};
|
|
319
|
+
let Some(incoming_agents) = incoming.get_mut("agents").and_then(Value::as_object_mut) else {
|
|
320
|
+
return;
|
|
321
|
+
};
|
|
322
|
+
for (agent_id, latest_agent) in latest_agents {
|
|
323
|
+
if latest_agent
|
|
324
|
+
.get("forked_from")
|
|
325
|
+
.and_then(Value::as_str)
|
|
326
|
+
.is_some_and(|source| !source.is_empty())
|
|
327
|
+
{
|
|
328
|
+
incoming_agents.insert(agent_id.clone(), latest_agent.clone());
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
240
333
|
fn legacy_single_team_state(state: &Value) -> bool {
|
|
241
334
|
match state.get("teams") {
|
|
242
335
|
None => true,
|
|
@@ -292,6 +385,34 @@ fn apply_observation_delta(before: &Value, observed: &Value, latest: &mut Value)
|
|
|
292
385
|
}
|
|
293
386
|
}
|
|
294
387
|
|
|
388
|
+
// The coordinator observes a projected team view, while its nested `teams`
|
|
389
|
+
// snapshot remains stale. Carry only observation deltas back to the canonical
|
|
390
|
+
// team entry before `bounded_team_view` projects it again.
|
|
391
|
+
fn sync_team_observation_delta(before: &Value, observed: &mut Value, team_key: &str) {
|
|
392
|
+
for field in ["agents", "coordinator"] {
|
|
393
|
+
let before_value = before.get(field).cloned().unwrap_or(Value::Null);
|
|
394
|
+
let observed_value = observed.get(field).cloned().unwrap_or(Value::Null);
|
|
395
|
+
if before_value == observed_value {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
let Some(team_entry) = observed
|
|
399
|
+
.get_mut("teams")
|
|
400
|
+
.and_then(Value::as_object_mut)
|
|
401
|
+
.and_then(|teams| teams.get_mut(team_key))
|
|
402
|
+
.and_then(Value::as_object_mut)
|
|
403
|
+
else {
|
|
404
|
+
continue;
|
|
405
|
+
};
|
|
406
|
+
if field == "coordinator" && !team_entry.contains_key(field) {
|
|
407
|
+
team_entry.insert(field.to_string(), serde_json::json!({}));
|
|
408
|
+
}
|
|
409
|
+
let slot = team_entry
|
|
410
|
+
.entry(field.to_string())
|
|
411
|
+
.or_insert_with(|| before_value.clone());
|
|
412
|
+
apply_observation_delta(&before_value, &observed_value, slot);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
295
416
|
/// Closed semantic catalog of workspace state writes. Every allowlisted
|
|
296
417
|
/// callsite maps to exactly one variant, and there is intentionally no
|
|
297
418
|
/// generic escape bucket. Any new state write must earn its own variant so
|
|
@@ -360,7 +360,20 @@ impl OfflineTransport {
|
|
|
360
360
|
if kind == "spawn_first" && !state.session_absent_after_spawn_first {
|
|
361
361
|
state.session_present = true;
|
|
362
362
|
}
|
|
363
|
-
let pane_index = state.spawns.len().saturating_sub(1);
|
|
363
|
+
let mut pane_index = state.spawns.len().saturating_sub(1);
|
|
364
|
+
// Existing sessions reserve `%0` for the already-running pane.
|
|
365
|
+
if kind == "spawn_into" {
|
|
366
|
+
pane_index = pane_index.max(1);
|
|
367
|
+
}
|
|
368
|
+
while state.pane_presence.contains_key(&format!("%{pane_index}"))
|
|
369
|
+
|| state
|
|
370
|
+
.targets
|
|
371
|
+
.iter()
|
|
372
|
+
.chain(state.target_snapshots.iter().flatten())
|
|
373
|
+
.any(|target| target.pane_id.as_str() == format!("%{pane_index}"))
|
|
374
|
+
{
|
|
375
|
+
pane_index = pane_index.saturating_add(1);
|
|
376
|
+
}
|
|
364
377
|
state
|
|
365
378
|
.pane_presence
|
|
366
379
|
.insert(format!("%{pane_index}"), state.spawned_panes_addressable);
|
|
@@ -541,10 +554,17 @@ impl Transport for OfflineTransport {
|
|
|
541
554
|
});
|
|
542
555
|
}
|
|
543
556
|
Ok(self.with_state(|state| {
|
|
544
|
-
state
|
|
557
|
+
let targets = state
|
|
545
558
|
.target_snapshots
|
|
546
559
|
.pop_front()
|
|
547
|
-
.unwrap_or_else(|| state.targets.clone())
|
|
560
|
+
.unwrap_or_else(|| state.targets.clone());
|
|
561
|
+
for target in &targets {
|
|
562
|
+
state
|
|
563
|
+
.pane_presence
|
|
564
|
+
.entry(target.pane_id.as_str().to_string())
|
|
565
|
+
.or_insert(true);
|
|
566
|
+
}
|
|
567
|
+
targets
|
|
548
568
|
}))
|
|
549
569
|
}
|
|
550
570
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.101",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.5.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.5.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.5.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.5.101",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.101",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.101"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|
|
@@ -4,65 +4,36 @@ description: Use only when the user explicitly asks to start, operate, inspect,
|
|
|
4
4
|
requires_team_agent: ">=0.5.0"
|
|
5
5
|
last_verified_against: "0.5.66"
|
|
6
6
|
---
|
|
7
|
-
|
|
8
7
|
# Team Agent
|
|
9
|
-
|
|
10
8
|
Sealed appliance for someone who just got the CLI. Operator handbook (permissions, models, routing, recovery): `references/team-agent-operator.md`.
|
|
11
|
-
|
|
12
|
-
If `team-agent --version` does not match `last_verified_against`, do not copy examples as current truth. Learn from the live CLI:
|
|
13
|
-
|
|
9
|
+
If `team-agent --version` differs from `last_verified_against`, learn from the live CLI before copying examples:
|
|
14
10
|
```bash
|
|
15
11
|
team-agent --version
|
|
16
12
|
team-agent --help
|
|
17
13
|
team-agent doctor --help
|
|
18
14
|
```
|
|
19
|
-
|
|
20
|
-
**Launch** from a tmux-addressable pane: `team-agent claude` or `team-agent codex`, then `team-agent quick-start .team/current`. Do not start a real team from a naked terminal. Workers always run in independent background windows on the workspace-scoped tmux server.
|
|
21
|
-
|
|
15
|
+
**Launch** from a tmux-addressable pane: `team-agent claude` or `team-agent codex`, then `team-agent quick-start .team/current`. Do not start a real team from a naked terminal; workers use independent background windows on the workspace-scoped tmux server.
|
|
22
16
|
**Operate**
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- Roles: every `agents/*.md` must declare boolean `dangerously_skip_permissions`. Never rewrite a user-supplied model id. Never read `.env` files.
|
|
29
|
-
|
|
30
|
-
**On failure:** if the CLI prints a structured `action`, run that `action` first, then stop. Do not guess flags. `coordinator.session_missing` is a self-healing transient — re-check `status --json`; do not shutdown because of it.
|
|
31
|
-
|
|
17
|
+
- Dispatch: `team-agent send TO MESSAGE` (positional TO; `--watch-result` is deprecated). After success, do not poll with `sleep` / `status` / `inbox` / `collect`. TO has two co-equal logical forms: an in-team short name (`team-agent send reviewer "..."`) and a fully qualified `<workspace>::<team>/<agent>`. Use the qualified form across workspaces.
|
|
18
|
+
- Inspect: `team-agent status` / `status --json`; `ok: true` plus `ready: false` is not a crash.
|
|
19
|
+
- Lifecycle: `restart .` resumes a stopped team; `add-agent NAME --role-file FILE` adds or `--force` recreates one worker; `shutdown --workspace .` stops. Do not shutdown the team to add a worker.
|
|
20
|
+
- Roles: every `agents/*.md` must declare boolean `dangerously_skip_permissions`. Never rewrite user-supplied model ids or read `.env` files.
|
|
21
|
+
- On a structured failure `action`, run that action first, then stop. Do not guess flags. `coordinator.session_missing` is self-healing; re-check `status --json`, do not shutdown.
|
|
32
22
|
The current user-facing agent is the leader (orchestrate only). Workers call `report_result` exactly once. Nested teams: `references/team-in-team.md`.
|
|
33
|
-
|
|
34
23
|
## Provider Capability Matrix
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
`quick-start` / `restart` / `claim-leader` JSON is compact by default (`ok`, `status`/`reason`, next action, attach/send, and `readiness.all_workers_spawned`); pass `--detail` only for internal diagnostics.
|
|
38
|
-
|
|
39
|
-
| Provider | Resume | Turn-state detection | Per-worker model override | Native session fork |
|
|
24
|
+
See `references/team-agent-operator.md` for Claude / Codex / Copilot / Gemini / fake. `quick-start` / `restart` / `claim-leader` JSON is compact by default; pass `--detail` only for internal diagnostics.
|
|
25
|
+
| Provider | Resume | Turn-state detection | Per-worker model override | Team Agent fork |
|
|
40
26
|
|---|---|---|---|---|
|
|
41
27
|
| `grok` | yes (`--resume <id>`, archive-gated) | no | yes (role `model` required) | yes (`--fork-session` + new `--session-id`) |
|
|
42
|
-
| `cursor_agent` | yes (
|
|
43
|
-
|
|
44
|
-
Grok / `cursor_agent` have no JSONL turn-state reader (classify → Unknown).
|
|
45
|
-
|
|
28
|
+
| `cursor_agent` | yes (`--resume <chatId>`, archive-gated) | no | required on role; same-family catalog id may take effect; unknown id silent-fallback; pane chrome ≠ proof | **no — `CapabilityUnsupported`** |
|
|
29
|
+
| `pi` | yes (exact `--session <backing-path>`, not selector/continue) | Unknown (no JSONL turn-state reader) | yes (role `model` / `effort`, catalog-validated) | yes (full snapshot into separate seat/session; exact backing resume) |
|
|
30
|
+
Grok / `cursor_agent` have no JSONL turn-state reader (classify → Unknown). Pi `fork-agent SOURCE --as TARGET` requires a captured Pi subscription source and reports both session ids plus the target backing path; it does not mutate the source session.
|
|
46
31
|
## Provider Prep
|
|
47
|
-
|
|
48
32
|
### Cursor provider notes
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
`
|
|
53
|
-
|
|
54
|
-
One `cursor_agent` per workspace. A second seat (including `add-agent --force` of a different id) fail-closes:
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
error: cursor_agent seat already occupies this workspace
|
|
58
|
-
reason: <workspace>/.cursor/mcp.json is directory-scoped; a second seat overwrites TEAM_AGENT_ID (last-writer)
|
|
59
|
-
action: do not add another CursorAgent in this workspace until per-seat MCP identity is isolated
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Several cursor seats → one workspace directory each. Same seat, fresh context → `reset-agent --discard-session`.
|
|
63
|
-
|
|
64
|
-
`clone-agent` copies the source role (provider unchanged). Runtime add: `clone-agent` → `stop-agent` → `remove-agent --confirm` (deletes `.team/dynamic-role-files/`) → write the role file → `add-agent --role-file` → dispatch.
|
|
65
|
-
|
|
66
|
-
- Restart emits `--resume <chatId>` when `store.db`/`meta.json` exist; the gate does not read chat text. Persist anything that must survive restart.
|
|
33
|
+
Frontmatter: `provider: cursor_agent` (not `cursor`; launcher verb `team-agent cursor`), `auth_mode: subscription`, `name:` required, plus `role:`, `tools:`, and boolean `dangerously_skip_permissions:`. Subscription needs no `profile`.
|
|
34
|
+
`model:` is required. The flag stays on argv; same-family catalog ids can change pane chrome, while unknown ids silent-fallback without warning. Pick a catalog id; after spawn, `capture-pane` once for chrome. The role field and pane chrome do not prove the live model.
|
|
35
|
+
One `cursor_agent` per workspace; a second seat fail-closes because `.cursor/mcp.json` is directory-scoped and overwrites `TEAM_AGENT_ID`. Several Cursor seats need separate workspace directories. Same seat, fresh context: `reset-agent --discard-session`.
|
|
36
|
+
`clone-agent` copies the source role but starts fresh. Runtime role replacement: `clone-agent` → `stop-agent` → `remove-agent --confirm` (deletes `.team/dynamic-role-files/`) → write role file → `add-agent --role-file` → dispatch.
|
|
37
|
+
- Restart emits `--resume <chatId>` when `store.db` / `meta.json` exist; the gate does not read chat text. Persist anything that must survive restart.
|
|
67
38
|
- Delivery sends one Enter; a second Enter interrupts the turn.
|
|
68
39
|
- After spawn, the pane footer should show `Cursor Agent v<version>`. Do not use `strings` to probe the binary.
|
|
@@ -122,11 +122,12 @@ Run `claim-leader` only from the **leader** pane. From a worker pane it refuses
|
|
|
122
122
|
|
|
123
123
|
## Provider Capability Matrix
|
|
124
124
|
|
|
125
|
-
| Provider | Resume | Turn-state detection | Per-worker model override |
|
|
125
|
+
| Provider | Resume | Turn-state detection | Per-worker model override | Team Agent fork |
|
|
126
126
|
|---|---|---|---|---|
|
|
127
127
|
| `claude` / `claude_code` | yes (`--resume <id>`, transcript-verified) | yes (JSONL stream) | yes (role `model` overrides `provider_models`) | yes (snapshot copy + only `--resume <snapshot-id>`) |
|
|
128
128
|
| `codex` | yes (`codex resume <id>`, session-store-verified) | yes (turn JSONL) | yes (role `model`) | yes (`codex fork`) |
|
|
129
129
|
| `copilot` | yes (`copilot --resume <id|name>`, sqlite `sessions` row) | not yet (phase 1: `provider.classify.unsupported` event) | yes (role `model`) | yes (isolated `COPILOT_HOME` store fork) |
|
|
130
|
+
| `pi` | yes (exact `--session <backing-path>`, not selector/continue) | Unknown (no JSONL turn-state reader) | yes (role `model` / `effort`, catalog-validated) | yes (full snapshot into separate seat/session; exact backing resume) |
|
|
130
131
|
| `gemini_cli` | no | no | yes | no |
|
|
131
132
|
| `fake` (testing only) | no | no | n/a | no |
|
|
132
133
|
|
|
@@ -134,6 +135,7 @@ Notes:
|
|
|
134
135
|
- Per-worker model override means a role-doc `model:` value wins over `TEAM.md` `provider_models.<provider>` at **compile** time; subscription defaults still fill blanks when there is no profile-deferred null.
|
|
135
136
|
- Copilot fork copies the source session into an isolated `COPILOT_HOME` and rekeys its SQLite session references atomically. Missing or incomplete backing fails closed; it never falls back to a fresh spawn.
|
|
136
137
|
- Copilot phase-1 idle/turn detection is intentionally Unknown; tick emits a single explicit `provider.classify.unsupported` event per state change (P4 dedup), never a silent default.
|
|
138
|
+
- `pi` `fork-agent SOURCE --as TARGET` requires a captured Pi subscription source. It copies the complete JSONL history into a distinct target session with a new id and exact `--session <path>` resume; it does not alter the source file or fall back to a fresh session.
|
|
137
139
|
|
|
138
140
|
## Provider Prep
|
|
139
141
|
|
|
@@ -567,7 +569,7 @@ Semantic distinction:
|
|
|
567
569
|
- `team-agent add-agent <agent> --role-file <file>` — add a **new** worker not yet in team state.
|
|
568
570
|
- `team-agent add-agent <agent> --role-file <file> --force` — force-recreate that worker from the role file.
|
|
569
571
|
- `team-agent clone-agent <source> --as <new>` — reread the source worker's latest role file and start a fresh provider seat. It never copies conversation context. Success is initially honest `capture_state: pending_first_turn` with `session_id`, `new_session_id`, and `backing_path` all null; after the first turn, canonical capture changes the state to `captured` and fills the backing tuple.
|
|
570
|
-
- `team-agent fork-agent <source> --as <new>` — reread the
|
|
572
|
+
- `team-agent fork-agent <source> --as <new>` — reread the source's latest role file and create a distinct provider session carrying its context. Pi subscription forks a verified full snapshot into a separate target seat/backing and reports source id, target id, and backing path; supported non-Pi providers keep their existing fork behavior. Unverified backing fails closed instead of silently cloning fresh.
|
|
571
573
|
- `team-agent start-agent <agent>` — (re)launch a worker that **already exists** in team state but whose window is missing.
|
|
572
574
|
- `team-agent reset-agent <agent> --discard-session` — keep the same seat and deliberately start it with fresh context.
|
|
573
575
|
- `team-agent restart .` — resume a fully **stopped** team from stored worker sessions.
|