@team-agent/installer 0.5.101 → 0.5.103
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/emit.rs +2 -1
- package/crates/team-agent/src/cli/models.rs +92 -226
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -74
- package/crates/team-agent/src/compiler/tests.rs +20 -23
- package/crates/team-agent/src/compiler.rs +49 -65
- package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +16 -48
- package/crates/team-agent/src/leader/start.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +37 -161
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +0 -28
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +11 -9
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -401
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +22 -8
- package/crates/team-agent/src/mcp_server/helpers.rs +0 -16
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +2 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -127
- package/crates/team-agent/src/mcp_server/mod.rs +7 -12
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -132
- package/crates/team-agent/src/mcp_server/tests/send.rs +1 -93
- package/crates/team-agent/src/mcp_server/tests/tools.rs +0 -61
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -51
- package/crates/team-agent/src/mcp_server/tests.rs +4 -200
- package/crates/team-agent/src/mcp_server/tools.rs +5 -396
- package/crates/team-agent/src/mcp_server/types.rs +3 -36
- package/crates/team-agent/src/mcp_server/wire.rs +2 -223
- package/crates/team-agent/src/messaging/leader_receiver.rs +1 -1
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +11 -42
- package/crates/team-agent/src/messaging/scheduler.rs +2 -205
- package/crates/team-agent/src/messaging/tests/basic.rs +0 -31
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +0 -73
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -26
- package/crates/team-agent/src/messaging/types.rs +0 -20
- package/crates/team-agent/src/model/spec.rs +14 -18
- package/crates/team-agent/src/provider/mod.rs +1 -0
- package/crates/team-agent/src/provider/model_catalog.rs +650 -0
- package/crates/team-agent/src/state/repository/tests.rs +1 -16
- package/crates/team-agent/src/state/repository.rs +1 -33
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +5 -5
- package/skills/team-agent/command-coverage.json +7 -0
- package/skills/team-agent/references/team-agent-operator.md +24 -116
- package/skills/team-agent/references/team-in-team.md +4 -19
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +0 -484
|
@@ -5,135 +5,9 @@ use serde_json::Value;
|
|
|
5
5
|
|
|
6
6
|
use crate::model::ids::TeamKey;
|
|
7
7
|
|
|
8
|
-
use super::super::helpers::{
|
|
8
|
+
use super::super::helpers::{object_fields, tool_runtime_error};
|
|
9
9
|
use super::super::{ToolOk, ToolResult};
|
|
10
10
|
|
|
11
|
-
pub(crate) fn update_state(
|
|
12
|
-
workspace: &Path,
|
|
13
|
-
owner_team: Option<&TeamKey>,
|
|
14
|
-
note: &str,
|
|
15
|
-
) -> ToolResult {
|
|
16
|
-
let selected = match crate::state::selector::resolve_active_team(
|
|
17
|
-
workspace,
|
|
18
|
-
owner_team.map(TeamKey::as_str),
|
|
19
|
-
crate::state::selector::SelectorMode::RequireSpec,
|
|
20
|
-
) {
|
|
21
|
-
Ok(selected) => selected,
|
|
22
|
-
Err(err) if is_missing_active_spec(&err) => {
|
|
23
|
-
return update_state_without_spec(workspace, owner_team, note);
|
|
24
|
-
}
|
|
25
|
-
Err(err) => return Err(tool_runtime_error(err)),
|
|
26
|
-
};
|
|
27
|
-
let state = crate::state::repository::StateRepository::new(&selected.run_workspace).commit(
|
|
28
|
-
crate::state::repository::StateWriteIntent::McpUpdateStateNote {
|
|
29
|
-
team_key: Some(&selected.team_key),
|
|
30
|
-
},
|
|
31
|
-
|latest| {
|
|
32
|
-
ensure_object(latest);
|
|
33
|
-
append_note(latest, note);
|
|
34
|
-
},
|
|
35
|
-
)
|
|
36
|
-
.map_err(tool_runtime_error)?;
|
|
37
|
-
let spec_path = selected
|
|
38
|
-
.spec_path
|
|
39
|
-
.ok_or_else(|| tool_runtime_error("active team spec not found for update_state"))?;
|
|
40
|
-
let spec_workspace = spec_path.parent().ok_or_else(|| {
|
|
41
|
-
tool_runtime_error(format!(
|
|
42
|
-
"active team spec has no parent: {}",
|
|
43
|
-
spec_path.display()
|
|
44
|
-
))
|
|
45
|
-
})?;
|
|
46
|
-
let spec_text = std::fs::read_to_string(&spec_path).map_err(tool_runtime_error)?;
|
|
47
|
-
let spec = crate::model::yaml::loads(&spec_text).map_err(tool_runtime_error)?;
|
|
48
|
-
let path = crate::lifecycle::restart::write_team_state(spec_workspace, &spec, &state)
|
|
49
|
-
.map_err(tool_runtime_error)?;
|
|
50
|
-
Ok(update_state_ok(path))
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
fn update_state_without_spec(
|
|
54
|
-
workspace: &Path,
|
|
55
|
-
owner_team: Option<&TeamKey>,
|
|
56
|
-
note: &str,
|
|
57
|
-
) -> ToolResult {
|
|
58
|
-
let selected = crate::state::selector::resolve_active_team(
|
|
59
|
-
workspace,
|
|
60
|
-
owner_team.map(TeamKey::as_str),
|
|
61
|
-
crate::state::selector::SelectorMode::RuntimeOnly,
|
|
62
|
-
)
|
|
63
|
-
.map_err(tool_runtime_error)?;
|
|
64
|
-
let state = crate::state::repository::StateRepository::new(&selected.run_workspace).commit(
|
|
65
|
-
crate::state::repository::StateWriteIntent::McpUpdateStateNote {
|
|
66
|
-
team_key: Some(&selected.team_key),
|
|
67
|
-
},
|
|
68
|
-
|latest| {
|
|
69
|
-
ensure_object(latest);
|
|
70
|
-
seed_legacy_team_key(latest, &selected.run_workspace, &selected.team_key);
|
|
71
|
-
append_note(latest, note);
|
|
72
|
-
},
|
|
73
|
-
)
|
|
74
|
-
.map_err(tool_runtime_error)?;
|
|
75
|
-
let path = crate::lifecycle::restart::write_team_state(
|
|
76
|
-
&selected.run_workspace,
|
|
77
|
-
&crate::model::yaml::Value::Null,
|
|
78
|
-
&state,
|
|
79
|
-
)
|
|
80
|
-
.map_err(tool_runtime_error)?;
|
|
81
|
-
Ok(update_state_ok(path))
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
fn append_note(state: &mut Value, note: &str) {
|
|
85
|
-
if let Some(obj) = state.as_object_mut() {
|
|
86
|
-
let notes = obj
|
|
87
|
-
.entry("notes".to_string())
|
|
88
|
-
.or_insert_with(|| Value::Array(Vec::new()));
|
|
89
|
-
if !notes.is_array() {
|
|
90
|
-
*notes = Value::Array(Vec::new());
|
|
91
|
-
}
|
|
92
|
-
if let Some(items) = notes.as_array_mut() {
|
|
93
|
-
items.push(Value::String(note.to_string()));
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
fn seed_legacy_team_key(state: &mut Value, run_workspace: &Path, team_key: &str) {
|
|
99
|
-
if state.get("team_dir").and_then(Value::as_str).is_some()
|
|
100
|
-
|| state.get("spec_path").and_then(Value::as_str).is_some()
|
|
101
|
-
|| state.get("session_name").and_then(Value::as_str).is_some()
|
|
102
|
-
{
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if let Some(obj) = state.as_object_mut() {
|
|
106
|
-
obj.insert(
|
|
107
|
-
"team_dir".to_string(),
|
|
108
|
-
Value::String(
|
|
109
|
-
run_workspace
|
|
110
|
-
.join(".team")
|
|
111
|
-
.join(team_key)
|
|
112
|
-
.to_string_lossy()
|
|
113
|
-
.to_string(),
|
|
114
|
-
),
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
fn update_state_ok(path: std::path::PathBuf) -> ToolOk {
|
|
120
|
-
let mut fields = serde_json::Map::new();
|
|
121
|
-
fields.insert("ok".to_string(), Value::Bool(true));
|
|
122
|
-
fields.insert(
|
|
123
|
-
"state_file".to_string(),
|
|
124
|
-
Value::String(path.to_string_lossy().to_string()),
|
|
125
|
-
);
|
|
126
|
-
ToolOk { fields }
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
fn is_missing_active_spec(err: &crate::state::StateError) -> bool {
|
|
130
|
-
matches!(
|
|
131
|
-
err,
|
|
132
|
-
crate::state::StateError::TeamSelect(message)
|
|
133
|
-
if message.starts_with("active team spec not found:")
|
|
134
|
-
)
|
|
135
|
-
}
|
|
136
|
-
|
|
137
11
|
pub(crate) fn get_team_status(workspace: &Path, owner_team: Option<&TeamKey>) -> ToolResult {
|
|
138
12
|
let selected = crate::state::selector::resolve_active_team(
|
|
139
13
|
workspace,
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
//! Card: `docs/phase0/subsystems/14-mcp_cli.md` (MCP half).
|
|
5
5
|
//! Truth source (READ-ONLY) `team-agent-public` @ v0.2.11 / `439bef8`:
|
|
6
6
|
//! - `mcp_server/server.py` — stdio loop + JSON-RPC route (`dispatch`/`handle_mcp`/`main`)
|
|
7
|
-
//! - `mcp_server/tools.py` — `TeamOrchestratorTools`: the
|
|
8
|
-
//! - `mcp_server/contracts.py` —
|
|
7
|
+
//! - `mcp_server/tools.py` — `TeamOrchestratorTools`: the three retained handlers
|
|
8
|
+
//! - `mcp_server/contracts.py` — retained tool names/descriptions/inputSchema
|
|
9
9
|
//! - `mcp_server/normalize.py` — result envelope / compact-result regularization
|
|
10
10
|
//! - `mcp_server/__init__.py` — package re-export surface locked by boundary tests
|
|
11
11
|
//!
|
|
@@ -17,13 +17,9 @@
|
|
|
17
17
|
//! `unimplemented!("step14 port: …")`.
|
|
18
18
|
//!
|
|
19
19
|
//! REUSE (do NOT redefine):
|
|
20
|
-
//! - [`
|
|
21
|
-
//! - [`
|
|
22
|
-
//!
|
|
23
|
-
//! - [`load_runtime_state`] / [`save_runtime_state`] (step 5 persist) — `assign_task`
|
|
24
|
-
//! / `update_state` read-modify-write; `get_visible_peers` reads team scope.
|
|
25
|
-
//! - [`messaging`] (step 11) — `send_message` / auto-finalizing `report_result` /
|
|
26
|
-
//! `stuck_list` / `stuck_cancel` delegated by the tool handlers.
|
|
20
|
+
//! - [`EventLog`] (step 4) — retained message/result audit events.
|
|
21
|
+
//! - [`load_runtime_state`] (step 5 persist) — message scope and peer checks.
|
|
22
|
+
//! - [`messaging`] (step 11) — `send_message` and durable `report_result` delegation.
|
|
27
23
|
//! - [`crate::model::enums`] (step 2) — [`ResultStatus`] / [`ChangeKind`] /
|
|
28
24
|
//! [`TestStatus`] / [`RiskSeverity`] are the normalized result-envelope value
|
|
29
25
|
//! enums; this layer ONLY does string-alias regularization onto them.
|
|
@@ -43,9 +39,8 @@
|
|
|
43
39
|
//! a frame here would corrupt the stdout JSON-RPC stream.
|
|
44
40
|
//! - **stdout 是传输通道** (server.py:135): every error is surfaced ON stdout as a
|
|
45
41
|
//! JSON-RPC frame; logs/warnings MUST go to stderr/file, never stdout.
|
|
46
|
-
//! - **worker-recipient 异步 accepted
|
|
47
|
-
//!
|
|
48
|
-
//! `poll_via = "team-agent inbox <id>"`; leader/`*` → [`SendOutcome::Direct`].
|
|
42
|
+
//! - **worker-recipient 异步 accepted**: a worker recipient with a message_id →
|
|
43
|
+
//! [`SendOutcome::WorkerAccepted`]; leader/`*` → [`SendOutcome::Direct`].
|
|
49
44
|
//! - **兜底字符串字节级保留** (bug-085): `_infer_task_id` failure → `"manual"` (not
|
|
50
45
|
//! None); `_infer_agent_id` failure → `None` → caller routes to `"unknown"`.
|
|
51
46
|
//!
|
|
@@ -1,72 +1,4 @@
|
|
|
1
|
-
///
|
|
2
|
-
/// purpose: scoped MCP no-compaction update_state filesystem contract
|
|
3
|
-
/// contract:
|
|
4
|
-
/// provides:
|
|
5
|
-
/// - name: update_state_path_contracts
|
|
6
|
-
/// what: proves the raw no-compaction response preserves its state-file path in an owned fixture
|
|
7
|
-
/// depends:
|
|
8
|
-
/// - name: mcp_state_fixture
|
|
9
|
-
/// what: fixture-owned root and provenance from tests.rs
|
|
10
|
-
/// boundary:
|
|
11
|
-
/// - retain the raw {ok,state_file} result shape
|
|
12
|
-
/// - the owned selector checks workspace, state-file, and runtime paths remain fixture-owned
|
|
13
|
-
/// maturity: wired
|
|
14
|
-
/// ---
|
|
15
|
-
|
|
16
|
-
fn verify_cleanup_surviving_receipt(
|
|
17
|
-
receipt_path: &std::path::Path,
|
|
18
|
-
raw_workspace: &std::path::Path,
|
|
19
|
-
canonical_workspace: &std::path::Path,
|
|
20
|
-
state_file: &std::path::Path,
|
|
21
|
-
runtime_state: &std::path::Path,
|
|
22
|
-
) {
|
|
23
|
-
let bytes = std::fs::read(receipt_path)
|
|
24
|
-
.expect("provenance receipt must survive fixture Drop");
|
|
25
|
-
let receipt: Value = serde_json::from_slice(&bytes).expect("provenance receipt must be JSON");
|
|
26
|
-
assert_eq!(receipt["schema"], json!("mcp-state-provenance-v1"));
|
|
27
|
-
let payload = &receipt["payload"];
|
|
28
|
-
let payload_bytes = serde_json::to_vec(payload).unwrap();
|
|
29
|
-
assert_eq!(
|
|
30
|
-
receipt["payload_sha256"],
|
|
31
|
-
json!(format!("{:x}", sha2::Sha256::digest(&payload_bytes)))
|
|
32
|
-
);
|
|
33
|
-
assert_eq!(
|
|
34
|
-
payload["raw_workspace"],
|
|
35
|
-
json!(raw_workspace.display().to_string())
|
|
36
|
-
);
|
|
37
|
-
assert_eq!(
|
|
38
|
-
payload["canonical_workspace"],
|
|
39
|
-
json!(canonical_workspace.display().to_string())
|
|
40
|
-
);
|
|
41
|
-
assert_eq!(
|
|
42
|
-
payload["resolved_state_file"],
|
|
43
|
-
json!(state_file.display().to_string())
|
|
44
|
-
);
|
|
45
|
-
assert_eq!(
|
|
46
|
-
payload["runtime_state_path"],
|
|
47
|
-
json!(runtime_state.display().to_string())
|
|
48
|
-
);
|
|
49
|
-
let facts = payload["facts"].as_array().unwrap();
|
|
50
|
-
for name in [
|
|
51
|
-
"root",
|
|
52
|
-
"raw_workspace",
|
|
53
|
-
"canonical_workspace",
|
|
54
|
-
"team_dir",
|
|
55
|
-
"runtime_dir",
|
|
56
|
-
"state_parent",
|
|
57
|
-
"runtime_state",
|
|
58
|
-
"state_file",
|
|
59
|
-
] {
|
|
60
|
-
let fact = facts
|
|
61
|
-
.iter()
|
|
62
|
-
.find(|fact| fact["name"] == json!(name))
|
|
63
|
-
.unwrap_or_else(|| panic!("provenance fact missing: {name}"));
|
|
64
|
-
assert!(fact["path"].as_str().is_some());
|
|
65
|
-
assert!(fact["uid"].as_u64().is_some());
|
|
66
|
-
assert!(fact["mode"].as_u64().is_some());
|
|
67
|
-
assert!(fact["device"].as_u64().is_some());
|
|
68
|
-
}
|
|
69
|
-
}
|
|
1
|
+
/// Scoped retained-tool contracts exercise owner binding and team isolation.
|
|
70
2
|
|
|
71
3
|
#[test]
|
|
72
4
|
fn dispatch_send_message_worker_accepted_returned_verbatim() {
|
|
@@ -100,69 +32,6 @@ fn verify_cleanup_surviving_receipt(
|
|
|
100
32
|
assert_eq!(v.get("poll_via"), Some(&json!(format!("team-agent inbox {mid}"))));
|
|
101
33
|
}
|
|
102
34
|
|
|
103
|
-
// ── #32/#47 request_human key order ok,message_id,status (no compaction) ────
|
|
104
|
-
// GOLDEN (probe_events_red.py REQUEST_HUMAN-KEYS): ['ok','message_id','status'].
|
|
105
|
-
// Rust compact_tool_result reorders to ok,status,message_id.
|
|
106
|
-
#[test]
|
|
107
|
-
fn request_human_key_order_is_ok_message_id_status() {
|
|
108
|
-
let tools = TeamOrchestratorTools::with_identity(
|
|
109
|
-
&unique_ws("reqhuman-order"),
|
|
110
|
-
Some(AgentId::new("worker-3")),
|
|
111
|
-
None,
|
|
112
|
-
);
|
|
113
|
-
let ok = tools.request_human("need approval", Some("task-1"), None).expect("request_human ok");
|
|
114
|
-
let v = serde_json::to_value(&ok).unwrap();
|
|
115
|
-
assert_eq!(keys(&v), vec!["ok", "message_id", "status"]);
|
|
116
|
-
assert_eq!(v.get("status"), Some(&json!("needs_human")));
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// ── #32 update_state returns RAW {ok, state_file} (NO compaction) ───────────
|
|
120
|
-
// GOLDEN (tools.py:316-325 + probe_passthrough): update_state is NOT compacted;
|
|
121
|
-
// state_file survives. Rust runs compact_tool_result whose ok-whitelist DROPS
|
|
122
|
-
// state_file (not a golden whitelist key), so the key vanishes.
|
|
123
|
-
#[test]
|
|
124
|
-
fn update_state_state_file_survives_no_compaction() {
|
|
125
|
-
let fixture = McpStateFixture::new("writable");
|
|
126
|
-
let expected = fixture.state_file("team_state.md");
|
|
127
|
-
let tools = TeamOrchestratorTools::with_identity(
|
|
128
|
-
&fixture.workspace,
|
|
129
|
-
Some(AgentId::new("leader")),
|
|
130
|
-
None,
|
|
131
|
-
);
|
|
132
|
-
let ok = tools.update_state("note").expect("update_state ok");
|
|
133
|
-
let v = serde_json::to_value(&ok).unwrap();
|
|
134
|
-
assert!(v.get("state_file").and_then(Value::as_str).is_some(),
|
|
135
|
-
"state_file must survive (update_state is not _compact_tool_result'd)");
|
|
136
|
-
assert_eq!(keys(&v), vec!["ok", "state_file"]);
|
|
137
|
-
let returned_state_file = PathBuf::from(v["state_file"].as_str().unwrap());
|
|
138
|
-
assert_eq!(returned_state_file, expected);
|
|
139
|
-
let runtime_state = crate::state::persist::runtime_state_path(&fixture.workspace);
|
|
140
|
-
assert!(fixture.under_root(&returned_state_file));
|
|
141
|
-
assert!(fixture.under_root(&runtime_state));
|
|
142
|
-
let canonical_workspace =
|
|
143
|
-
crate::model::paths::canonical_run_workspace(&fixture.workspace).unwrap();
|
|
144
|
-
let receipt = fixture.record_provenance(&fixture.workspace, &returned_state_file);
|
|
145
|
-
let root = fixture.root.clone();
|
|
146
|
-
drop(fixture);
|
|
147
|
-
assert!(!root.exists(), "fixture root must be removed before receipt verification");
|
|
148
|
-
verify_cleanup_surviving_receipt(
|
|
149
|
-
&receipt,
|
|
150
|
-
&root.join("workspace"),
|
|
151
|
-
&canonical_workspace,
|
|
152
|
-
&returned_state_file,
|
|
153
|
-
&runtime_state,
|
|
154
|
-
);
|
|
155
|
-
let receipt_bytes = std::fs::read(&receipt).unwrap();
|
|
156
|
-
println!(
|
|
157
|
-
"F10_PROVENANCE_RECEIPT path={} sha256={:x} payload={}",
|
|
158
|
-
receipt.display(),
|
|
159
|
-
sha2::Sha256::digest(&receipt_bytes),
|
|
160
|
-
String::from_utf8_lossy(&receipt_bytes)
|
|
161
|
-
);
|
|
162
|
-
std::fs::remove_file(&receipt).unwrap();
|
|
163
|
-
assert!(!receipt.exists(), "cleanup must remove only the exact receipt path");
|
|
164
|
-
}
|
|
165
|
-
|
|
166
35
|
// ── #36 report_result identity: neither envelope nor args can override capture ─
|
|
167
36
|
#[test]
|
|
168
37
|
fn report_result_rejects_envelope_and_argument_identity_mismatch() {
|
|
@@ -43,31 +43,6 @@ fn is_worker_recipient_classification() {
|
|
|
43
43
|
])));
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
47
|
-
// merge_tasks_by_id — prefer wins, prefer-first insertion order (tools.py:30)
|
|
48
|
-
// Golden: prefer t1(done),t2 + fallback t1(pending),t3,{no id},"notdict"
|
|
49
|
-
// → [t1(done), t2, t3] (t1 from prefer wins; non-dict / no-id dropped)
|
|
50
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
51
|
-
#[test]
|
|
52
|
-
fn merge_tasks_by_id_prefer_wins_no_done_regression() {
|
|
53
|
-
let prefer = vec![
|
|
54
|
-
json!({"id": "t1", "status": "done"}),
|
|
55
|
-
json!({"id": "t2", "status": "pending"}),
|
|
56
|
-
];
|
|
57
|
-
let fallback = vec![
|
|
58
|
-
json!({"id": "t1", "status": "pending"}), // must NOT regress t1
|
|
59
|
-
json!({"id": "t3", "status": "ready"}),
|
|
60
|
-
json!({"no": "id"}), // dropped (no id)
|
|
61
|
-
json!("notdict"), // dropped (not object)
|
|
62
|
-
];
|
|
63
|
-
let merged = merge_tasks_by_id(&prefer, &fallback);
|
|
64
|
-
assert_eq!(merged.len(), 3);
|
|
65
|
-
assert_eq!(merged[0]["id"], json!("t1"));
|
|
66
|
-
assert_eq!(merged[0]["status"], json!("done")); // prefer wins → no regression
|
|
67
|
-
assert_eq!(merged[1]["id"], json!("t2"));
|
|
68
|
-
assert_eq!(merged[2]["id"], json!("t3"));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
46
|
// ════════════════════════════════════════════════════════════════════════
|
|
72
47
|
// SendOutcome::to_value — worker-accepted async envelope (tools.py:177-182)
|
|
73
48
|
// byte-stable: {status:"accepted",delivery_pending:true,
|
|
@@ -171,7 +146,7 @@ fn send_message_worker_recipient_returns_accepted_with_poll_hint() {
|
|
|
171
146
|
}
|
|
172
147
|
|
|
173
148
|
#[test]
|
|
174
|
-
fn
|
|
149
|
+
fn ordinary_send_shape_has_no_recovery_marker() {
|
|
175
150
|
let ws = seed_current_worker_state("ordinary-no-recovery");
|
|
176
151
|
let tools = TeamOrchestratorTools::with_identity(
|
|
177
152
|
&ws,
|
|
@@ -198,46 +173,6 @@ fn ordinary_send_assign_shape_has_no_recovery_marker() {
|
|
|
198
173
|
"ordinary send must not carry acceptance marker: {sent}"
|
|
199
174
|
);
|
|
200
175
|
|
|
201
|
-
let assigned = tools
|
|
202
|
-
.assign_task(
|
|
203
|
-
&json!({"id": "ordinary-task", "assignee": "worker-1", "title": "ordinary"}),
|
|
204
|
-
None,
|
|
205
|
-
)
|
|
206
|
-
.expect("ordinary assign ok");
|
|
207
|
-
let assigned_value = serde_json::to_value(&assigned).expect("serialize ordinary assign");
|
|
208
|
-
assert!(
|
|
209
|
-
!assigned_value.as_object().unwrap().contains_key("recovery"),
|
|
210
|
-
"ordinary assign must not carry recovery marker: {assigned_value}"
|
|
211
|
-
);
|
|
212
|
-
assert!(
|
|
213
|
-
assigned_value.get("acceptance_marker").is_none(),
|
|
214
|
-
"ordinary assign must not carry acceptance marker: {assigned_value}"
|
|
215
|
-
);
|
|
216
|
-
|
|
217
|
-
let recovery_false_assigned = tools
|
|
218
|
-
.assign_task(
|
|
219
|
-
&json!({
|
|
220
|
-
"id": "ordinary-recovery-false-task",
|
|
221
|
-
"assignee": "worker-1",
|
|
222
|
-
"title": "ordinary recovery false",
|
|
223
|
-
"recovery": false,
|
|
224
|
-
}),
|
|
225
|
-
None,
|
|
226
|
-
)
|
|
227
|
-
.expect("recovery=false assign ok");
|
|
228
|
-
let recovery_false_value =
|
|
229
|
-
serde_json::to_value(&recovery_false_assigned).expect("serialize recovery=false assign");
|
|
230
|
-
assert!(
|
|
231
|
-
!recovery_false_value
|
|
232
|
-
.as_object()
|
|
233
|
-
.unwrap()
|
|
234
|
-
.contains_key("recovery"),
|
|
235
|
-
"recovery=false assign must not carry recovery marker: {recovery_false_value}"
|
|
236
|
-
);
|
|
237
|
-
assert!(
|
|
238
|
-
recovery_false_value.get("acceptance_marker").is_none(),
|
|
239
|
-
"recovery=false assign must not carry acceptance marker: {recovery_false_value}"
|
|
240
|
-
);
|
|
241
176
|
}
|
|
242
177
|
|
|
243
178
|
#[test]
|
|
@@ -256,33 +191,6 @@ fn send_message_without_framework_identity_fails_closed() {
|
|
|
256
191
|
assert_eq!(error.reason, ToolErrorReason::McpScopeRefused);
|
|
257
192
|
}
|
|
258
193
|
|
|
259
|
-
#[test]
|
|
260
|
-
fn recovery_assign_shape_has_structured_marker() {
|
|
261
|
-
let ws = seed_current_worker_state("recovery-marker");
|
|
262
|
-
let tools = TeamOrchestratorTools::with_identity(
|
|
263
|
-
&ws,
|
|
264
|
-
Some(AgentId::new("leader")),
|
|
265
|
-
Some(TeamKey::new("current")),
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
let assigned = tools
|
|
269
|
-
.assign_task(
|
|
270
|
-
&json!({
|
|
271
|
-
"id": "recovery-task",
|
|
272
|
-
"assignee": "worker-1",
|
|
273
|
-
"title": "recover",
|
|
274
|
-
"recovery": true,
|
|
275
|
-
}),
|
|
276
|
-
None,
|
|
277
|
-
)
|
|
278
|
-
.expect("recovery assign ok");
|
|
279
|
-
assert_eq!(assigned.fields.get("recovery"), Some(&json!(true)));
|
|
280
|
-
assert_eq!(
|
|
281
|
-
assigned.fields.get("acceptance_marker"),
|
|
282
|
-
Some(&json!("recovery"))
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
194
|
#[test]
|
|
287
195
|
fn send_message_worker_recipient_surfaces_dead_coordinator_warning() {
|
|
288
196
|
let ws = unique_ws("send-worker-dead-coord");
|
|
@@ -290,67 +290,6 @@
|
|
|
290
290
|
);
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
294
|
-
// CONTROL-PLANE: request_human creates a requires_ack leader message → needs_human
|
|
295
|
-
// (tools.py:342-346). sender = explicit > env > "unknown" (never leader).
|
|
296
|
-
//
|
|
297
|
-
// Post-#230 N31/N32 funnel (cr-approved I-3): request_human routes through the
|
|
298
|
-
// shared leader-delivery primitive (`send_to_leader_receiver`) instead of doing a
|
|
299
|
-
// raw `store.create_message` bypass. Return shape from the caller's perspective is
|
|
300
|
-
// unchanged: `status="needs_human"` + a populated `message_id`. With no leader
|
|
301
|
-
// pane bound in this fixture, the primitive's I-4 rebind_required path STILL
|
|
302
|
-
// persists the message row and returns its `message_id` — audit + rebind replay
|
|
303
|
-
// both depend on it.
|
|
304
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
305
|
-
#[test]
|
|
306
|
-
fn request_human_returns_needs_human_with_message_id() {
|
|
307
|
-
let tools = TeamOrchestratorTools::with_identity(
|
|
308
|
-
&unique_ws("request-human"),
|
|
309
|
-
Some(AgentId::new("worker-3")),
|
|
310
|
-
Some(TeamKey::new("teamA")),
|
|
311
|
-
);
|
|
312
|
-
let ok = tools.request_human("need approval", Some("task-1"), None)
|
|
313
|
-
.expect("request_human ok");
|
|
314
|
-
let v = serde_json::to_value(&ok).unwrap();
|
|
315
|
-
assert_eq!(v.get("status"), Some(&json!("needs_human")));
|
|
316
|
-
assert!(v.get("message_id").and_then(Value::as_str).is_some(),
|
|
317
|
-
"request_human must return the created leader message_id (persisted for rebind audit even on I-4 rebind_required)");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
321
|
-
// CONTROL-PLANE: update_state appends a note + returns state_file (tools.py:316-325)
|
|
322
|
-
// ════════════════════════════════════════════════════════════════════════
|
|
323
|
-
#[test]
|
|
324
|
-
fn update_state_returns_ok_and_state_file_path() {
|
|
325
|
-
let ws = seed_state_ws(
|
|
326
|
-
"update-state",
|
|
327
|
-
&json!({
|
|
328
|
-
"active_team_key": "teamA",
|
|
329
|
-
"team_key": "teamA",
|
|
330
|
-
"session_name": "teamA",
|
|
331
|
-
"status": "alive",
|
|
332
|
-
"teams": {
|
|
333
|
-
"teamA": {
|
|
334
|
-
"team_key": "teamA",
|
|
335
|
-
"session_name": "teamA",
|
|
336
|
-
"status": "alive",
|
|
337
|
-
"agents": {}
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}),
|
|
341
|
-
);
|
|
342
|
-
let tools = TeamOrchestratorTools::with_identity(
|
|
343
|
-
&ws,
|
|
344
|
-
Some(AgentId::new("leader")),
|
|
345
|
-
Some(TeamKey::new("teamA")),
|
|
346
|
-
);
|
|
347
|
-
let ok = tools.update_state("checkpoint note").expect("update_state ok");
|
|
348
|
-
let v = serde_json::to_value(&ok).unwrap();
|
|
349
|
-
assert_eq!(v.get("ok"), Some(&json!(true)));
|
|
350
|
-
assert!(v.get("state_file").and_then(Value::as_str).is_some(),
|
|
351
|
-
"update_state returns the rewritten team_state.md path");
|
|
352
|
-
}
|
|
353
|
-
|
|
354
293
|
// ════════════════════════════════════════════════════════════════════════
|
|
355
294
|
// RpcId / RpcResponse byte-stability — null id echoed, error frame shape.
|
|
356
295
|
// ════════════════════════════════════════════════════════════════════════
|
|
@@ -1,19 +1,9 @@
|
|
|
1
1
|
#[test]
|
|
2
2
|
fn mcp_tool_wire_names_and_parse_roundtrip() {
|
|
3
3
|
let names = [
|
|
4
|
-
(McpTool::AssignTask, "assign_task"),
|
|
5
4
|
(McpTool::SendMessage, "send_message"),
|
|
6
5
|
(McpTool::ReportResult, "report_result"),
|
|
7
|
-
(McpTool::UpdateState, "update_state"),
|
|
8
6
|
(McpTool::GetTeamStatus, "get_team_status"),
|
|
9
|
-
(McpTool::StopAgent, "stop_agent"),
|
|
10
|
-
(McpTool::ResetAgent, "reset_agent"),
|
|
11
|
-
(McpTool::AddAgent, "add_agent"),
|
|
12
|
-
(McpTool::CloneAgent, "clone_agent"),
|
|
13
|
-
(McpTool::ForkAgent, "fork_agent"),
|
|
14
|
-
(McpTool::RequestHuman, "request_human"),
|
|
15
|
-
(McpTool::StuckList, "stuck_list"),
|
|
16
|
-
(McpTool::StuckCancel, "stuck_cancel"),
|
|
17
7
|
];
|
|
18
8
|
for (tool, name) in names {
|
|
19
9
|
assert_eq!(tool.wire_name(), name);
|
|
@@ -45,27 +35,13 @@ fn rpc_method_classify() {
|
|
|
45
35
|
// tools_contract — TOOLS wire list, exact names+order
|
|
46
36
|
// ════════════════════════════════════════════════════════════════════════
|
|
47
37
|
#[test]
|
|
48
|
-
fn
|
|
38
|
+
fn tools_contract_has_three_tools_in_order() {
|
|
49
39
|
let tools = tools_contract();
|
|
50
|
-
assert_eq!(tools.len(),
|
|
40
|
+
assert_eq!(tools.len(), 3);
|
|
51
41
|
let got: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
|
|
52
42
|
assert_eq!(
|
|
53
43
|
got,
|
|
54
|
-
vec![
|
|
55
|
-
"assign_task",
|
|
56
|
-
"send_message",
|
|
57
|
-
"report_result",
|
|
58
|
-
"update_state",
|
|
59
|
-
"get_team_status",
|
|
60
|
-
"stop_agent",
|
|
61
|
-
"reset_agent",
|
|
62
|
-
"add_agent",
|
|
63
|
-
"clone_agent",
|
|
64
|
-
"fork_agent",
|
|
65
|
-
"request_human",
|
|
66
|
-
"stuck_list",
|
|
67
|
-
"stuck_cancel",
|
|
68
|
-
]
|
|
44
|
+
vec!["send_message", "report_result", "get_team_status"]
|
|
69
45
|
);
|
|
70
46
|
// each carries description + inputSchema
|
|
71
47
|
for t in &tools {
|
|
@@ -89,32 +65,11 @@ fn tools_contract_has_thirteen_tools_in_order() {
|
|
|
89
65
|
"{internal} is framework-owned, not caller-supplied"
|
|
90
66
|
);
|
|
91
67
|
}
|
|
92
|
-
let
|
|
68
|
+
let status = tools
|
|
93
69
|
.iter()
|
|
94
|
-
.find(|tool| tool["name"] == json!("
|
|
70
|
+
.find(|tool| tool["name"] == json!("get_team_status"))
|
|
95
71
|
.unwrap();
|
|
96
|
-
assert_eq!(
|
|
97
|
-
clone["description"],
|
|
98
|
-
json!("Clone a worker role into a fresh provider session.")
|
|
99
|
-
);
|
|
100
|
-
assert_eq!(clone["inputSchema"]["additionalProperties"], json!(false));
|
|
101
|
-
assert_eq!(
|
|
102
|
-
clone["inputSchema"]["required"],
|
|
103
|
-
json!(["source_agent_id", "as_agent_id"])
|
|
104
|
-
);
|
|
105
|
-
assert_eq!(
|
|
106
|
-
clone["inputSchema"]["properties"]
|
|
107
|
-
.as_object()
|
|
108
|
-
.unwrap()
|
|
109
|
-
.keys()
|
|
110
|
-
.cloned()
|
|
111
|
-
.collect::<BTreeSet<_>>(),
|
|
112
|
-
BTreeSet::from([
|
|
113
|
-
"as_agent_id".to_string(),
|
|
114
|
-
"label".to_string(),
|
|
115
|
-
"source_agent_id".to_string(),
|
|
116
|
-
])
|
|
117
|
-
);
|
|
72
|
+
assert_eq!(status["description"], json!("Return machine-readable team status."));
|
|
118
73
|
}
|
|
119
74
|
|
|
120
75
|
#[test]
|