@team-agent/installer 0.5.80 → 0.5.82
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/status_port/brief.rs +40 -16
- package/crates/team-agent/src/cli/tests/status_brief.rs +199 -0
- package/crates/team-agent/src/communication_mode/mod.rs +3 -3
- package/crates/team-agent/src/coordinator/tests/mod.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +239 -0
- package/crates/team-agent/src/coordinator/tick.rs +21 -13
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +0 -2
- package/crates/team-agent/src/lifecycle/tests/communication_mode_runtime_contract_red.rs +15 -4
- package/crates/team-agent/src/lifecycle/tests/pi_catalog_red.rs +0 -1
- package/crates/team-agent/src/lifecycle/tests/pi_command_red.rs +3 -4
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +0 -1
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +5 -0
- package/crates/team-agent/src/lifecycle/tests/pi_session_delivery_red.rs +0 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +47 -13
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +2 -11
- package/crates/team-agent/src/mcp_server/tools.rs +1 -3
- package/crates/team-agent/src/messaging/results.rs +1 -2
- package/crates/team-agent/src/provider/adapters/pi.rs +6 -6
- package/crates/team-agent/src/state/persist.rs +42 -2
- package/crates/team-agent/src/state/projection.rs +24 -0
- package/crates/team-agent/src/state/repository/tests.rs +81 -0
- package/crates/team-agent/src/state/repository.rs +169 -1
- package/package.json +4 -4
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
//!
|
|
2
|
-
//! StateRepository facade.
|
|
2
|
+
//! StateRepository facade. Task/note commits apply bounded mutations under the
|
|
3
|
+
//! existing state-save lock; coordinator commits carry only observation deltas.
|
|
4
|
+
//! Other writer clusters retain their legacy dispatch below.
|
|
3
5
|
//!
|
|
4
6
|
//! Ref:
|
|
5
7
|
//! - `.team/artifacts/s1a-state-repository-design.md` (skeleton)
|
|
@@ -142,8 +144,152 @@ impl<'a> StateRepository<'a> {
|
|
|
142
144
|
where
|
|
143
145
|
F: FnOnce(&mut Value),
|
|
144
146
|
{
|
|
147
|
+
#[cfg(test)]
|
|
148
|
+
test_support::before_commit();
|
|
145
149
|
route_reapply(self.workspace, intent, state, reapply)
|
|
146
150
|
}
|
|
151
|
+
|
|
152
|
+
/// Commit a task/note delta once against latest under state-save. Return
|
|
153
|
+
/// the committed Team view for callers that produce Markdown afterwards.
|
|
154
|
+
pub fn commit<F>(&self, intent: StateWriteIntent<'_>, update: F) -> Result<Value, StateError>
|
|
155
|
+
where
|
|
156
|
+
F: FnOnce(&mut Value),
|
|
157
|
+
{
|
|
158
|
+
#[cfg(test)]
|
|
159
|
+
test_support::before_commit();
|
|
160
|
+
let team_key = match intent {
|
|
161
|
+
// Assignment's existing reconcile helper updates the root task
|
|
162
|
+
// array and the explicit Team array independently (legacy parity).
|
|
163
|
+
StateWriteIntent::McpAssignTask { .. } => None,
|
|
164
|
+
StateWriteIntent::McpUpdateStateNote { team_key } => team_key,
|
|
165
|
+
StateWriteIntent::ResultCollection { owner_team_id } => owner_team_id,
|
|
166
|
+
_ => return Err(StateError::SaveFailed("intent does not accept a task/note delta".into())),
|
|
167
|
+
};
|
|
168
|
+
let committed = super::persist::update_runtime_state(self.workspace, |latest| {
|
|
169
|
+
let mut selected = bounded_team_view(latest, team_key)?;
|
|
170
|
+
update(&mut selected);
|
|
171
|
+
Ok(match team_key {
|
|
172
|
+
Some(key) => super::projection::merge_committed_team(latest, &selected, key),
|
|
173
|
+
None => selected,
|
|
174
|
+
})
|
|
175
|
+
})?;
|
|
176
|
+
bounded_team_view(&committed, team_key)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Fence the sampled incarnation with the existing persist guards, then
|
|
180
|
+
/// commit only this tick's agent observations and coordinator bookkeeping.
|
|
181
|
+
pub fn commit_observations(
|
|
182
|
+
&self,
|
|
183
|
+
intent: StateWriteIntent<'_>,
|
|
184
|
+
before: &Value,
|
|
185
|
+
observed: &Value,
|
|
186
|
+
) -> Result<(), StateError> {
|
|
187
|
+
let StateWriteIntent::CoordinatorTick { team_key } = intent else {
|
|
188
|
+
return Err(StateError::SaveFailed("intent does not accept observations".into()));
|
|
189
|
+
};
|
|
190
|
+
let sampled_legacy =
|
|
191
|
+
legacy_single_team_state(before) && legacy_single_team_state(observed);
|
|
192
|
+
let sampled_identity = legacy_team_identity(before);
|
|
193
|
+
super::persist::update_runtime_state(self.workspace, |latest| {
|
|
194
|
+
if sampled_legacy {
|
|
195
|
+
if sampled_identity != legacy_team_identity(observed)
|
|
196
|
+
|| !legacy_single_team_state(latest)
|
|
197
|
+
|| sampled_identity != legacy_team_identity(latest)
|
|
198
|
+
{
|
|
199
|
+
return Err(StateError::SaveConflict(
|
|
200
|
+
"legacy coordinator observation scope changed before commit".to_string(),
|
|
201
|
+
));
|
|
202
|
+
}
|
|
203
|
+
let mut checked = observed.clone();
|
|
204
|
+
super::persist::merge_ordinary_state(&mut checked, latest)?;
|
|
205
|
+
if !legacy_single_team_state(&checked)
|
|
206
|
+
|| sampled_identity != legacy_team_identity(&checked)
|
|
207
|
+
{
|
|
208
|
+
return Err(StateError::SaveConflict(
|
|
209
|
+
"legacy coordinator observation scope changed during validation"
|
|
210
|
+
.to_string(),
|
|
211
|
+
));
|
|
212
|
+
}
|
|
213
|
+
let mut selected = latest.clone();
|
|
214
|
+
for field in ["agents", "coordinator"] {
|
|
215
|
+
if before[field] != checked[field] {
|
|
216
|
+
apply_observation_delta(
|
|
217
|
+
&before[field],
|
|
218
|
+
&checked[field],
|
|
219
|
+
&mut selected[field],
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return Ok(selected);
|
|
224
|
+
}
|
|
225
|
+
let mut checked = super::projection::merge_committed_team(latest, observed, team_key);
|
|
226
|
+
super::persist::merge_ordinary_state(&mut checked, latest)?;
|
|
227
|
+
let checked = bounded_team_view(&checked, Some(team_key))?;
|
|
228
|
+
let mut selected = bounded_team_view(latest, Some(team_key))?;
|
|
229
|
+
for field in ["agents", "coordinator"] {
|
|
230
|
+
if before[field] != checked[field] {
|
|
231
|
+
apply_observation_delta(&before[field], &checked[field], &mut selected[field]);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
Ok(super::projection::merge_committed_team(latest, &selected, team_key))
|
|
235
|
+
})?;
|
|
236
|
+
Ok(())
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn legacy_single_team_state(state: &Value) -> bool {
|
|
241
|
+
match state.get("teams") {
|
|
242
|
+
None => true,
|
|
243
|
+
Some(Value::Object(teams)) => teams.is_empty(),
|
|
244
|
+
Some(_) => false,
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
fn legacy_team_identity(state: &Value) -> [Option<String>; 5] {
|
|
249
|
+
[
|
|
250
|
+
"active_team_key",
|
|
251
|
+
"team_key",
|
|
252
|
+
"session_name",
|
|
253
|
+
"team_dir",
|
|
254
|
+
"spec_path",
|
|
255
|
+
]
|
|
256
|
+
.map(|field| {
|
|
257
|
+
state
|
|
258
|
+
.get(field)
|
|
259
|
+
.and_then(Value::as_str)
|
|
260
|
+
.filter(|value| !value.is_empty())
|
|
261
|
+
.map(str::to_string)
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
fn bounded_team_view(state: &Value, team_key: Option<&str>) -> Result<Value, StateError> {
|
|
266
|
+
let Some(key) = team_key else { return Ok(state.clone()); };
|
|
267
|
+
if state.get("teams").and_then(Value::as_object).is_some_and(|teams| teams.contains_key(key)) {
|
|
268
|
+
return Ok(super::projection::project_top_level_view(state, key));
|
|
269
|
+
}
|
|
270
|
+
if state.get("teams").and_then(Value::as_object).is_some_and(|teams| !teams.is_empty()) {
|
|
271
|
+
return Err(StateError::TeamSelect(format!("team no longer exists: {key}")));
|
|
272
|
+
}
|
|
273
|
+
Ok(state.clone())
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Diffing is limited to the two observation blocks above, never a whole-state
|
|
277
|
+
// merge. Equal fields retain a concurrent writer's value; removed fields clear.
|
|
278
|
+
fn apply_observation_delta(before: &Value, observed: &Value, latest: &mut Value) {
|
|
279
|
+
if before == observed { return; }
|
|
280
|
+
if let Some(observed) = observed.as_object() {
|
|
281
|
+
if !latest.is_object() { *latest = serde_json::json!({}); }
|
|
282
|
+
if let Some(before) = before.as_object() {
|
|
283
|
+
for key in before.keys().filter(|key| !observed.contains_key(*key)) {
|
|
284
|
+
if let Some(latest) = latest.as_object_mut() { latest.remove(key); }
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (key, value) in observed {
|
|
288
|
+
apply_observation_delta(&before[key], value, &mut latest[key]);
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
*latest = observed.clone();
|
|
292
|
+
}
|
|
147
293
|
}
|
|
148
294
|
|
|
149
295
|
/// Closed semantic catalog of workspace state writes. Every allowlisted
|
|
@@ -555,6 +701,11 @@ fn route_reapply<F>(
|
|
|
555
701
|
where
|
|
556
702
|
F: FnOnce(&mut Value),
|
|
557
703
|
{
|
|
704
|
+
if matches!(&intent, StateWriteIntent::McpAssignTask { .. }
|
|
705
|
+
| StateWriteIntent::McpUpdateStateNote { .. } | StateWriteIntent::ResultCollection { .. })
|
|
706
|
+
{
|
|
707
|
+
return StateRepository::new(workspace).commit(intent, reapply).map(|_| ());
|
|
708
|
+
}
|
|
558
709
|
if reapply_scope(&intent) == ReapplyScope::Team {
|
|
559
710
|
helper_write_team_scoped_reapply(workspace, state, reapply)
|
|
560
711
|
} else {
|
|
@@ -564,3 +715,20 @@ where
|
|
|
564
715
|
|
|
565
716
|
#[cfg(test)]
|
|
566
717
|
mod tests;
|
|
718
|
+
|
|
719
|
+
#[cfg(test)]
|
|
720
|
+
pub(crate) mod test_support {
|
|
721
|
+
thread_local! {
|
|
722
|
+
static BEFORE_COMMIT: std::cell::RefCell<Option<Box<dyn FnOnce()>>> = const {
|
|
723
|
+
std::cell::RefCell::new(None)
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
pub(crate) fn set_before_commit(hook: impl FnOnce() + 'static) {
|
|
728
|
+
BEFORE_COMMIT.with(|slot| *slot.borrow_mut() = Some(Box::new(hook)));
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
pub(super) fn before_commit() {
|
|
732
|
+
if let Some(hook) = BEFORE_COMMIT.with(|slot| slot.borrow_mut().take()) { hook(); }
|
|
733
|
+
}
|
|
734
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.82",
|
|
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.82",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.5.82",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.5.82"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|