@team-agent/installer 0.5.100 → 0.5.102
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/cli/tests/main_preserved.rs +0 -74
- package/crates/team-agent/src/compiler/tests.rs +20 -23
- package/crates/team-agent/src/compiler.rs +52 -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/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/spawn.rs +1 -1
- 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 +115 -2
- 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/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/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/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/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 +15 -18
- 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/tests.rs +1 -16
- package/crates/team-agent/src/state/repository.rs +126 -37
- package/crates/team-agent/src/transport/test_support.rs +23 -3
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +18 -47
- package/skills/team-agent/command-coverage.json +7 -0
- package/skills/team-agent/references/team-agent-operator.md +28 -118
- 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
|
@@ -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)]
|
|
@@ -30,6 +30,9 @@ All communication must go through Team Agent MCP tools.
|
|
|
30
30
|
- Teammate: {send_message}(to='<agent_id>', content='...')
|
|
31
31
|
- Broadcast: {send_message}(to='*', content='...')
|
|
32
32
|
- Complete: {report_result}(summary='...') — call exactly once
|
|
33
|
+
- Optional roster/status: {get_team_status} — use only when useful; it is not a communication or completion prerequisite.
|
|
34
|
+
|
|
35
|
+
Only these three Team Agent MCP tools are available. Lifecycle management remains on the separately authorized operator CLI.
|
|
33
36
|
|
|
34
37
|
## Rules:
|
|
35
38
|
|
|
@@ -55,7 +58,7 @@ pub(crate) struct WorkerCommandAgent {
|
|
|
55
58
|
system_prompt_file: Option<String>,
|
|
56
59
|
output_contract_format: Option<String>,
|
|
57
60
|
communication_mode: CommunicationMode,
|
|
58
|
-
///
|
|
61
|
+
/// Missing `dangerously_skip_permissions` defaults to true; an explicit false remains respected.
|
|
59
62
|
dangerously_skip_permissions: bool,
|
|
60
63
|
}
|
|
61
64
|
|
|
@@ -107,9 +110,9 @@ impl WorkerCommandAgent {
|
|
|
107
110
|
.get("communication_mode")
|
|
108
111
|
.and_then(crate::model::yaml::Value::as_str),
|
|
109
112
|
)?,
|
|
110
|
-
dangerously_skip_permissions: matches!(
|
|
113
|
+
dangerously_skip_permissions: !matches!(
|
|
111
114
|
agent.get("dangerously_skip_permissions"),
|
|
112
|
-
Some(crate::model::yaml::Value::Bool(
|
|
115
|
+
Some(crate::model::yaml::Value::Bool(false))
|
|
113
116
|
),
|
|
114
117
|
})
|
|
115
118
|
}
|
|
@@ -164,7 +167,7 @@ impl WorkerCommandAgent {
|
|
|
164
167
|
dangerously_skip_permissions: agent
|
|
165
168
|
.get("dangerously_skip_permissions")
|
|
166
169
|
.and_then(serde_json::Value::as_bool)
|
|
167
|
-
.unwrap_or(
|
|
170
|
+
.unwrap_or(true),
|
|
168
171
|
})
|
|
169
172
|
}
|
|
170
173
|
}
|
|
@@ -182,15 +185,20 @@ pub(crate) fn compile_worker_system_prompt(
|
|
|
182
185
|
// C-1 cr verdict / B2 灵魂件 — identity 必须 FIRST(MUST-4 行为层守:空白上下文问
|
|
183
186
|
// "你是谁"必须先答 Team Agent worker 身份)。runtime contract 跟后。
|
|
184
187
|
let send_message = mcp_tool_name(agent.provider, "team_orchestrator", "send_message");
|
|
188
|
+
let get_team_status = if agent.provider == Provider::Pi {
|
|
189
|
+
r#"mcp({tool:"team_orchestrator_get_team_status", args:{}})"#.to_string()
|
|
190
|
+
} else {
|
|
191
|
+
format!("{}()", mcp_tool_name(agent.provider, "team_orchestrator", "get_team_status"))
|
|
192
|
+
};
|
|
185
193
|
let report_result = if agent.provider == Provider::Pi {
|
|
186
194
|
r#"mcp({tool:"team_orchestrator_report_result", args:{summary:"..."}})"#.to_string()
|
|
187
195
|
} else {
|
|
188
196
|
mcp_tool_name(agent.provider, "team_orchestrator", "report_result")
|
|
189
197
|
};
|
|
190
198
|
let runtime_contract = if agent.provider == Provider::Pi {
|
|
191
|
-
pi_runtime_contract_section()
|
|
199
|
+
pi_runtime_contract_section(&get_team_status)
|
|
192
200
|
} else {
|
|
193
|
-
runtime_contract_section(&send_message, &report_result)
|
|
201
|
+
runtime_contract_section(&send_message, &report_result, &get_team_status)
|
|
194
202
|
};
|
|
195
203
|
let communication_contract = if agent.provider == Provider::Pi {
|
|
196
204
|
pi_communication_contract(agent.communication_mode)
|
|
@@ -241,13 +249,18 @@ fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
|
|
|
241
249
|
}
|
|
242
250
|
}
|
|
243
251
|
|
|
244
|
-
fn runtime_contract_section(
|
|
252
|
+
fn runtime_contract_section(
|
|
253
|
+
send_message: &str,
|
|
254
|
+
report_result: &str,
|
|
255
|
+
get_team_status: &str,
|
|
256
|
+
) -> String {
|
|
245
257
|
RUNTIME_CONTRACT_SECTION
|
|
246
258
|
.replace("{send_message}", send_message)
|
|
247
259
|
.replace("{report_result}", report_result)
|
|
260
|
+
.replace("{get_team_status}", get_team_status)
|
|
248
261
|
}
|
|
249
262
|
|
|
250
|
-
fn pi_runtime_contract_section() -> String {
|
|
263
|
+
fn pi_runtime_contract_section(get_team_status: &str) -> String {
|
|
251
264
|
RUNTIME_CONTRACT_SECTION
|
|
252
265
|
.replace(
|
|
253
266
|
"{send_message}(to='<agent_id>', content='...')",
|
|
@@ -261,6 +274,7 @@ fn pi_runtime_contract_section() -> String {
|
|
|
261
274
|
"{report_result}(summary='...')",
|
|
262
275
|
r#"mcp({tool:"team_orchestrator_report_result", args:{summary:"..."}})"#,
|
|
263
276
|
)
|
|
277
|
+
.replace("{get_team_status}", get_team_status)
|
|
264
278
|
}
|
|
265
279
|
|
|
266
280
|
fn pi_communication_contract(mode: CommunicationMode) -> String {
|
|
@@ -40,22 +40,6 @@ pub fn is_worker_recipient(to: &MessageTarget) -> bool {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/// `_merge_tasks_by_id` (`tools.py:30-49`): dedupe a task list keyed by `id`,
|
|
44
|
-
/// `prefer` entries winning on duplicates (so an earlier `done` is not regressed).
|
|
45
|
-
pub fn merge_tasks_by_id(prefer: &[Value], fallback: &[Value]) -> Vec<Value> {
|
|
46
|
-
let mut seen = std::collections::BTreeSet::new();
|
|
47
|
-
let mut out = Vec::new();
|
|
48
|
-
for item in prefer.iter().chain(fallback.iter()) {
|
|
49
|
-
let Some(id) = item.get("id").and_then(Value::as_str) else {
|
|
50
|
-
continue;
|
|
51
|
-
};
|
|
52
|
-
if seen.insert(id.to_string()) && item.is_object() {
|
|
53
|
-
out.push(item.clone());
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
out
|
|
57
|
-
}
|
|
58
|
-
|
|
59
43
|
pub(crate) fn tool_error_reason_wire(reason: ToolErrorReason) -> &'static str {
|
|
60
44
|
match reason {
|
|
61
45
|
ToolErrorReason::UnknownTool => "unknown_tool",
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
//!
|
|
2
|
-
//! MCP
|
|
3
|
-
//!
|
|
4
|
-
//! S0 keeps the old placeholder behavior behind stable module boundaries so
|
|
5
|
-
//! follow-up lanes can implement real lifecycle logic without touching tools.rs.
|
|
2
|
+
//! MCP status query facade.
|
|
6
3
|
|
|
7
|
-
mod agent_ops;
|
|
8
4
|
mod state_status;
|
|
9
5
|
|
|
10
|
-
pub(crate) use
|
|
11
|
-
pub(crate) use state_status::{get_team_status, update_state};
|
|
6
|
+
pub(crate) use state_status::get_team_status;
|
|
@@ -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");
|