@team-agent/installer 0.5.101 → 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/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/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/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/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
|
@@ -1,25 +1,8 @@
|
|
|
1
|
-
//!
|
|
2
|
-
//! purpose: MCP no-compaction contract test with fixture-owned workspace and path provenance
|
|
3
|
-
//! contract:
|
|
4
|
-
//! provides:
|
|
5
|
-
//! - name: mcp_state_fixture
|
|
6
|
-
//! what: supplies explicit process-owned workspace and state-file paths without mutating process-global environment
|
|
7
|
-
//! - name: mcp_state_path_provenance
|
|
8
|
-
//! what: records raw/canonical workspace, resolved state path, and filesystem ownership facts
|
|
9
|
-
//! depends:
|
|
10
|
-
//! - crate::mcp_server::lifecycle_tools::state_status
|
|
11
|
-
//! - crate::state::selector
|
|
12
|
-
//! boundary:
|
|
13
|
-
//! - the owned no-compaction test writes only within its fixture root
|
|
14
|
-
//! - this suite does not alter persistence semantics
|
|
15
|
-
//! maturity: wired
|
|
16
|
-
//! ---
|
|
17
|
-
//! step 14a · mcp_server::tests — WAVE-2 RED contracts (Python v0.2.11 golden).
|
|
1
|
+
//! MCP server unit-test fixtures and focused stdio/wire contract tests.
|
|
18
2
|
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
|
19
3
|
|
|
20
4
|
use super::*;
|
|
21
5
|
use serde_json::json;
|
|
22
|
-
use sha2::{Digest, Sha256};
|
|
23
6
|
use std::path::{Path, PathBuf};
|
|
24
7
|
|
|
25
8
|
// ── helpers ──────────────────────────────────────────────────────────────
|
|
@@ -35,11 +18,9 @@ fn keys(v: &Value) -> Vec<String> {
|
|
|
35
18
|
v.as_object().unwrap().keys().cloned().collect()
|
|
36
19
|
}
|
|
37
20
|
|
|
38
|
-
/// A UNIQUE throwaway workspace dir per test
|
|
39
|
-
///
|
|
40
|
-
|
|
41
|
-
/// Pure-function / dispatch-shape tests that never touch fs/db keep a dummy fixed path.
|
|
42
|
-
fn unique_ws(tag: &str) -> std::path::PathBuf {
|
|
21
|
+
/// A UNIQUE throwaway workspace dir per test. Filesystem and database tests
|
|
22
|
+
/// must not share `/tmp/ws` under parallel cargo.
|
|
23
|
+
fn unique_ws(tag: &str) -> PathBuf {
|
|
43
24
|
use std::io::ErrorKind;
|
|
44
25
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
45
26
|
static N: AtomicU64 = AtomicU64::new(0);
|
|
@@ -54,183 +35,6 @@ fn unique_ws(tag: &str) -> std::path::PathBuf {
|
|
|
54
35
|
}
|
|
55
36
|
}
|
|
56
37
|
|
|
57
|
-
static MCP_FIXTURE_SEQ: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
|
58
|
-
|
|
59
|
-
/// One process-owned root for update_state tests. All paths are passed explicitly
|
|
60
|
-
/// to the code under test; the fixture never changes process-global environment.
|
|
61
|
-
struct McpStateFixture {
|
|
62
|
-
root: PathBuf,
|
|
63
|
-
workspace: PathBuf,
|
|
64
|
-
receipt: PathBuf,
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
impl McpStateFixture {
|
|
68
|
-
fn new(_tag: &str) -> Self {
|
|
69
|
-
let seq = MCP_FIXTURE_SEQ.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
|
70
|
-
let base = if cfg!(target_os = "macos") {
|
|
71
|
-
PathBuf::from("/private/tmp")
|
|
72
|
-
} else {
|
|
73
|
-
PathBuf::from("/tmp")
|
|
74
|
-
};
|
|
75
|
-
let raw_root = base.join(format!("ta-mcp-{}-{seq}", std::process::id()));
|
|
76
|
-
std::fs::create_dir(&raw_root).unwrap();
|
|
77
|
-
let root = std::fs::canonicalize(raw_root).unwrap();
|
|
78
|
-
let workspace = root.join("workspace");
|
|
79
|
-
let receipt = root
|
|
80
|
-
.parent()
|
|
81
|
-
.unwrap()
|
|
82
|
-
.join(format!("ta-mcp-receipt-{}-{seq}.json", std::process::id()));
|
|
83
|
-
std::fs::create_dir(&workspace).unwrap();
|
|
84
|
-
|
|
85
|
-
let fixture = Self {
|
|
86
|
-
root,
|
|
87
|
-
workspace,
|
|
88
|
-
receipt,
|
|
89
|
-
};
|
|
90
|
-
fixture.seed_spec("team_state.md");
|
|
91
|
-
crate::state::repository::StateRepository::new(&fixture.workspace)
|
|
92
|
-
.save(
|
|
93
|
-
crate::state::repository::StateWriteIntent::FakeE2eSeed,
|
|
94
|
-
&json!({
|
|
95
|
-
"session_name": "mcp-fixture",
|
|
96
|
-
"active_team_key": "current",
|
|
97
|
-
"agents": {},
|
|
98
|
-
"tasks": []
|
|
99
|
-
}),
|
|
100
|
-
)
|
|
101
|
-
.unwrap();
|
|
102
|
-
fixture
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
fn seed_spec(&self, state_file: &str) {
|
|
106
|
-
std::fs::write(
|
|
107
|
-
self.workspace.join("team.spec.yaml"),
|
|
108
|
-
format!(
|
|
109
|
-
"team:\n name: mcp-fixture\n objective: path contract\nagents: []\ntasks: []\ncontext:\n state_file: {state_file}\n"
|
|
110
|
-
),
|
|
111
|
-
)
|
|
112
|
-
.unwrap();
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
fn state_file(&self, relative: &str) -> PathBuf {
|
|
116
|
-
self.workspace.join(relative)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
fn record_provenance(&self, raw_workspace: &Path, state_file: &Path) -> PathBuf {
|
|
120
|
-
let canonical_workspace =
|
|
121
|
-
crate::model::paths::canonical_run_workspace(raw_workspace).unwrap();
|
|
122
|
-
assert!(self.under_root(raw_workspace));
|
|
123
|
-
assert!(self.under_root(&canonical_workspace));
|
|
124
|
-
assert!(self.under_root(state_file));
|
|
125
|
-
let team_dir = self.workspace.join(".team");
|
|
126
|
-
let runtime_dir = team_dir.join("runtime");
|
|
127
|
-
let runtime_state = crate::state::persist::runtime_state_path(&self.workspace);
|
|
128
|
-
let paths = [
|
|
129
|
-
("root", self.root.as_path()),
|
|
130
|
-
("raw_workspace", raw_workspace),
|
|
131
|
-
("canonical_workspace", canonical_workspace.as_path()),
|
|
132
|
-
("team_dir", team_dir.as_path()),
|
|
133
|
-
("runtime_dir", runtime_dir.as_path()),
|
|
134
|
-
("state_parent", state_file.parent().unwrap()),
|
|
135
|
-
("runtime_state", runtime_state.as_path()),
|
|
136
|
-
];
|
|
137
|
-
let mut facts = paths
|
|
138
|
-
.into_iter()
|
|
139
|
-
.map(|(name, path)| {
|
|
140
|
-
let metadata = std::fs::symlink_metadata(path).unwrap();
|
|
141
|
-
json!({
|
|
142
|
-
"name": name,
|
|
143
|
-
"path": path.display().to_string(),
|
|
144
|
-
"uid": metadata_uid(&metadata),
|
|
145
|
-
"mode": metadata_mode(&metadata),
|
|
146
|
-
"device": metadata_device(&metadata)
|
|
147
|
-
})
|
|
148
|
-
})
|
|
149
|
-
.collect::<Vec<_>>();
|
|
150
|
-
if let Ok(metadata) = std::fs::symlink_metadata(state_file) {
|
|
151
|
-
facts.push(json!({
|
|
152
|
-
"name": "state_file",
|
|
153
|
-
"path": state_file.display().to_string(),
|
|
154
|
-
"uid": metadata_uid(&metadata),
|
|
155
|
-
"mode": metadata_mode(&metadata),
|
|
156
|
-
"device": metadata_device(&metadata)
|
|
157
|
-
}));
|
|
158
|
-
}
|
|
159
|
-
let payload = json!({
|
|
160
|
-
"raw_workspace": raw_workspace.display().to_string(),
|
|
161
|
-
"canonical_workspace": canonical_workspace.display().to_string(),
|
|
162
|
-
"resolved_state_file": state_file.display().to_string(),
|
|
163
|
-
"runtime_state_path": runtime_state.display().to_string(),
|
|
164
|
-
"facts": facts
|
|
165
|
-
});
|
|
166
|
-
let payload_bytes = serde_json::to_vec(&payload).unwrap();
|
|
167
|
-
let payload_sha256 = format!("{:x}", Sha256::digest(&payload_bytes));
|
|
168
|
-
let receipt = json!({
|
|
169
|
-
"schema": "mcp-state-provenance-v1",
|
|
170
|
-
"payload": payload,
|
|
171
|
-
"payload_sha256": payload_sha256
|
|
172
|
-
});
|
|
173
|
-
let receipt_bytes = serde_json::to_vec_pretty(&receipt).unwrap();
|
|
174
|
-
use std::io::Write;
|
|
175
|
-
let mut file = std::fs::OpenOptions::new()
|
|
176
|
-
.write(true)
|
|
177
|
-
.create_new(true)
|
|
178
|
-
.open(&self.receipt)
|
|
179
|
-
.unwrap();
|
|
180
|
-
file.write_all(&receipt_bytes).unwrap();
|
|
181
|
-
file.sync_all().unwrap();
|
|
182
|
-
self.receipt.clone()
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
fn under_root(&self, path: &Path) -> bool {
|
|
186
|
-
let candidate = if path.exists() {
|
|
187
|
-
std::fs::canonicalize(path).unwrap()
|
|
188
|
-
} else {
|
|
189
|
-
path.to_path_buf()
|
|
190
|
-
};
|
|
191
|
-
candidate.starts_with(&self.root)
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
impl Drop for McpStateFixture {
|
|
196
|
-
fn drop(&mut self) {
|
|
197
|
-
let _ = std::fs::remove_dir_all(&self.root);
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
#[cfg(unix)]
|
|
202
|
-
fn metadata_uid(metadata: &std::fs::Metadata) -> u32 {
|
|
203
|
-
use std::os::unix::fs::MetadataExt;
|
|
204
|
-
metadata.uid()
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
#[cfg(not(unix))]
|
|
208
|
-
fn metadata_uid(_: &std::fs::Metadata) -> u32 {
|
|
209
|
-
0
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
#[cfg(unix)]
|
|
213
|
-
fn metadata_mode(metadata: &std::fs::Metadata) -> u32 {
|
|
214
|
-
use std::os::unix::fs::MetadataExt;
|
|
215
|
-
metadata.mode()
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
#[cfg(not(unix))]
|
|
219
|
-
fn metadata_mode(_: &std::fs::Metadata) -> u32 {
|
|
220
|
-
0
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
#[cfg(unix)]
|
|
224
|
-
fn metadata_device(metadata: &std::fs::Metadata) -> u64 {
|
|
225
|
-
use std::os::unix::fs::MetadataExt;
|
|
226
|
-
metadata.dev()
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
#[cfg(not(unix))]
|
|
230
|
-
fn metadata_device(_: &std::fs::Metadata) -> u64 {
|
|
231
|
-
0
|
|
232
|
-
}
|
|
233
|
-
|
|
234
38
|
include!("tests/normalize.rs");
|
|
235
39
|
include!("tests/wire.rs");
|
|
236
40
|
include!("tests/send.rs");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//!
|
|
2
|
-
//! step 14a · mcp_server::tools — `TeamOrchestratorTools`, the
|
|
2
|
+
//! step 14a · mcp_server::tools — `TeamOrchestratorTools`, the three MCP handlers.
|
|
3
3
|
|
|
4
4
|
use std::path::{Path, PathBuf};
|
|
5
5
|
|
|
@@ -13,16 +13,14 @@ use crate::model::ids::{AgentId, TaskId, TeamKey};
|
|
|
13
13
|
use crate::event_log::EventLog;
|
|
14
14
|
|
|
15
15
|
// ── REUSE: step 5 state persist / projection ────────────────────────────────
|
|
16
|
-
use crate::state::persist::
|
|
17
|
-
load_runtime_state, save_runtime_state, save_runtime_state_reapplying_after_conflict,
|
|
18
|
-
};
|
|
16
|
+
use crate::state::persist::load_runtime_state;
|
|
19
17
|
|
|
20
18
|
// ── REUSE: step 11 messaging delegate surface ───────────────────────────────
|
|
21
19
|
use crate::messaging::{self, DeliveryStatus, MessageTarget, SendOptions, TrustedSender};
|
|
22
20
|
|
|
23
21
|
use super::helpers::{
|
|
24
22
|
current_reportable_message_for, delivery_outcome_value, direct_message_attribution_for,
|
|
25
|
-
ensure_object, enum_value, is_worker_recipient,
|
|
23
|
+
ensure_object, enum_value, is_worker_recipient, latest_task_for_assignee,
|
|
26
24
|
non_empty_string, object_fields, requires_ack_for_target, tool_runtime_error,
|
|
27
25
|
DirectMessageAttribution,
|
|
28
26
|
};
|
|
@@ -35,7 +33,7 @@ use super::types::{
|
|
|
35
33
|
};
|
|
36
34
|
|
|
37
35
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
38
|
-
// TeamOrchestratorTools
|
|
36
|
+
// TeamOrchestratorTools — the three typed MCP handlers.
|
|
39
37
|
// Identity/scope anchored on spawn-time env (TEAM_AGENT_ID / TEAM_AGENT_OWNER_TEAM_ID);
|
|
40
38
|
// every handler delegates to runtime/MessageStore/EventLog. These ARE the
|
|
41
39
|
// contract-callable behavioral entry fns.
|
|
@@ -183,100 +181,6 @@ impl TeamOrchestratorTools {
|
|
|
183
181
|
error
|
|
184
182
|
}
|
|
185
183
|
|
|
186
|
-
/// `assign_task` (`tools.py:84-133`): C8 Family-B task-view reconcile then deliver.
|
|
187
|
-
/// Resolves team key from owner-team env (or `active_team_key`), appends or
|
|
188
|
-
/// field-updates the task in state, then delegates delivery to
|
|
189
|
-
/// [`Self::send_message`] and compacts the result.
|
|
190
|
-
pub fn assign_task(&self, task: &Value, message: Option<&str>) -> ToolResult {
|
|
191
|
-
self.validate_rpc_scope_args("assign_task", task)?;
|
|
192
|
-
let Some(task_obj) = task.as_object() else {
|
|
193
|
-
return Err(ToolError::new(
|
|
194
|
-
ToolErrorReason::InvalidToolArguments,
|
|
195
|
-
"assign_task task must be an object",
|
|
196
|
-
"ValueError",
|
|
197
|
-
));
|
|
198
|
-
};
|
|
199
|
-
let Some(task_id) = task
|
|
200
|
-
.get("id")
|
|
201
|
-
.and_then(Value::as_str)
|
|
202
|
-
.and_then(non_empty_string)
|
|
203
|
-
else {
|
|
204
|
-
return Err(ToolError::new(
|
|
205
|
-
ToolErrorReason::InvalidToolArguments,
|
|
206
|
-
"assign_task task.id is required",
|
|
207
|
-
"ValueError",
|
|
208
|
-
));
|
|
209
|
-
};
|
|
210
|
-
let Some(assignee) = task
|
|
211
|
-
.get("assignee")
|
|
212
|
-
.and_then(Value::as_str)
|
|
213
|
-
.and_then(non_empty_string)
|
|
214
|
-
else {
|
|
215
|
-
return Err(ToolError::new(
|
|
216
|
-
ToolErrorReason::InvalidToolArguments,
|
|
217
|
-
"assign_task task.assignee is required",
|
|
218
|
-
"ValueError",
|
|
219
|
-
));
|
|
220
|
-
};
|
|
221
|
-
if let Some(route) = task.get("result_route") {
|
|
222
|
-
let Some(route) = route.as_str() else {
|
|
223
|
-
return Err(ToolError::new(
|
|
224
|
-
ToolErrorReason::InvalidToolArguments,
|
|
225
|
-
"assign_task task.result_route must be 'leader' or 'pipeline'",
|
|
226
|
-
"ValueError",
|
|
227
|
-
));
|
|
228
|
-
};
|
|
229
|
-
if crate::messaging::results::ResultRoute::parse(route).is_none() {
|
|
230
|
-
return Err(ToolError::new(
|
|
231
|
-
ToolErrorReason::InvalidToolArguments,
|
|
232
|
-
format!("assign_task task.result_route has unknown value: {route}"),
|
|
233
|
-
"ValueError",
|
|
234
|
-
));
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
let task_value = Value::Object(task_obj.clone());
|
|
239
|
-
let recovery = task_recovery_marker(&task_value);
|
|
240
|
-
let mut state = load_runtime_state(&self.workspace).map_err(tool_runtime_error)?;
|
|
241
|
-
ensure_object(&mut state);
|
|
242
|
-
let team_key = self
|
|
243
|
-
.canonical_owner_team_key()?
|
|
244
|
-
.map(|team| team.as_str().to_string())
|
|
245
|
-
.or_else(|| assignment_team_key(&state));
|
|
246
|
-
crate::state::repository::StateRepository::new(&self.workspace)
|
|
247
|
-
.commit(
|
|
248
|
-
crate::state::repository::StateWriteIntent::McpAssignTask {
|
|
249
|
-
team_key: team_key.as_deref(),
|
|
250
|
-
task_id,
|
|
251
|
-
},
|
|
252
|
-
|latest| {
|
|
253
|
-
ensure_object(latest);
|
|
254
|
-
let latest_team_key = team_key.clone().or_else(|| assignment_team_key(latest));
|
|
255
|
-
reconcile_assigned_task(latest, latest_team_key.as_deref(), &task_value);
|
|
256
|
-
},
|
|
257
|
-
)
|
|
258
|
-
.map_err(tool_runtime_error)?;
|
|
259
|
-
|
|
260
|
-
let content = assignment_message(task, message);
|
|
261
|
-
let out = self.send_message(
|
|
262
|
-
&MessageTarget::Single(assignee.to_string()),
|
|
263
|
-
&content,
|
|
264
|
-
Some(task_id),
|
|
265
|
-
None,
|
|
266
|
-
None,
|
|
267
|
-
)?;
|
|
268
|
-
let mut ok = compact_tool_result(&out.to_value())?;
|
|
269
|
-
if recovery {
|
|
270
|
-
ok.fields
|
|
271
|
-
.insert("recovery".to_string(), serde_json::json!(true));
|
|
272
|
-
ok.fields.insert(
|
|
273
|
-
"acceptance_marker".to_string(),
|
|
274
|
-
Value::String("recovery".to_string()),
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
Ok(ok)
|
|
278
|
-
}
|
|
279
|
-
|
|
280
184
|
/// `send_message` (`tools.py:135-183`): C14/C15/C17 scope resolution.
|
|
281
185
|
/// - sender = immutable `TEAM_AGENT_ID` captured when the MCP server starts.
|
|
282
186
|
/// - `requires_ack` defaults from target (`_requires_ack_for_target`).
|
|
@@ -722,207 +626,12 @@ impl TeamOrchestratorTools {
|
|
|
722
626
|
.and_then(|value| compact_tool_result(&value))
|
|
723
627
|
}
|
|
724
628
|
|
|
725
|
-
/// `
|
|
726
|
-
/// facade. S0 preserves the old placeholder behavior.
|
|
727
|
-
pub fn update_state(&self, note: &str) -> ToolResult {
|
|
728
|
-
let owner_team = self.canonical_owner_team_key()?;
|
|
729
|
-
super::lifecycle_tools::update_state(&self.workspace, owner_team.as_ref(), note)
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
/// `get_team_status` (`tools.py:327-328`): delegated through the lifecycle tools
|
|
733
|
-
/// facade. S0 preserves the old placeholder behavior.
|
|
629
|
+
/// `get_team_status`: return the owner-team-scoped machine-readable status.
|
|
734
630
|
pub fn get_team_status(&self) -> ToolResult {
|
|
735
631
|
let owner_team = self.canonical_owner_team_key()?;
|
|
736
632
|
super::lifecycle_tools::get_team_status(&self.workspace, owner_team.as_ref())
|
|
737
633
|
}
|
|
738
634
|
|
|
739
|
-
/// `stop_agent` (`tools.py:330-331`): delegated through the lifecycle tools facade.
|
|
740
|
-
pub fn stop_agent(&self, agent_id: &str) -> ToolResult {
|
|
741
|
-
let owner_team = self.canonical_owner_team_key()?;
|
|
742
|
-
super::lifecycle_tools::stop_agent(&self.workspace, owner_team.as_ref(), agent_id)
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/// `reset_agent` (`tools.py:333-334`): delegated through the lifecycle tools facade.
|
|
746
|
-
pub fn reset_agent(&self, agent_id: &str, discard_session: bool) -> ToolResult {
|
|
747
|
-
let owner_team = self.canonical_owner_team_key()?;
|
|
748
|
-
super::lifecycle_tools::reset_agent(
|
|
749
|
-
&self.workspace,
|
|
750
|
-
owner_team.as_ref(),
|
|
751
|
-
agent_id,
|
|
752
|
-
discard_session,
|
|
753
|
-
)
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/// `add_agent` (`tools.py:336-337`): delegate to real lifecycle add-agent
|
|
757
|
-
/// under the spawn-time owner team.
|
|
758
|
-
pub fn add_agent(&self, new_agent_id: &str, role_file_path: &str) -> ToolResult {
|
|
759
|
-
let owner_team = self
|
|
760
|
-
.canonical_owner_team_key()?
|
|
761
|
-
.ok_or_else(|| self.scope_refused("add_agent requires TEAM_AGENT_OWNER_TEAM_ID"))?;
|
|
762
|
-
let role_file = Path::new(role_file_path);
|
|
763
|
-
let role_file = if role_file.is_absolute() {
|
|
764
|
-
role_file.to_path_buf()
|
|
765
|
-
} else {
|
|
766
|
-
self.workspace.join(role_file)
|
|
767
|
-
};
|
|
768
|
-
crate::lifecycle::launch::add_agent(
|
|
769
|
-
&self.workspace,
|
|
770
|
-
&AgentId::new(new_agent_id.to_string()),
|
|
771
|
-
&role_file,
|
|
772
|
-
false,
|
|
773
|
-
Some(owner_team.as_str()),
|
|
774
|
-
)
|
|
775
|
-
.map_err(tool_runtime_error)
|
|
776
|
-
.and_then(|report| {
|
|
777
|
-
compact_tool_result(&serde_json::json!({
|
|
778
|
-
"ok": true,
|
|
779
|
-
"status": "added",
|
|
780
|
-
"agent_id": new_agent_id,
|
|
781
|
-
"state_file": report.env.state_file.to_string_lossy().to_string(),
|
|
782
|
-
"coordinator_started": report.env.coordinator_started,
|
|
783
|
-
"start_mode": format!("{:?}", report.start_mode),
|
|
784
|
-
"role_file": report.role_file.to_string_lossy().to_string(),
|
|
785
|
-
}))
|
|
786
|
-
})
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/// `fork_agent` (`tools.py:339-340`): delegated through the lifecycle tools facade.
|
|
790
|
-
pub fn fork_agent(
|
|
791
|
-
&self,
|
|
792
|
-
source_agent_id: &str,
|
|
793
|
-
as_agent_id: &str,
|
|
794
|
-
label: Option<&str>,
|
|
795
|
-
) -> ToolResult {
|
|
796
|
-
let owner_team = self.canonical_owner_team_key()?;
|
|
797
|
-
super::lifecycle_tools::fork_agent(
|
|
798
|
-
&self.workspace,
|
|
799
|
-
owner_team.as_ref(),
|
|
800
|
-
source_agent_id,
|
|
801
|
-
as_agent_id,
|
|
802
|
-
label,
|
|
803
|
-
)
|
|
804
|
-
}
|
|
805
|
-
|
|
806
|
-
pub fn clone_agent(
|
|
807
|
-
&self,
|
|
808
|
-
source_agent_id: &str,
|
|
809
|
-
as_agent_id: &str,
|
|
810
|
-
label: Option<&str>,
|
|
811
|
-
) -> ToolResult {
|
|
812
|
-
let owner_team = self.canonical_owner_team_key()?;
|
|
813
|
-
super::lifecycle_tools::clone_agent(
|
|
814
|
-
&self.workspace,
|
|
815
|
-
owner_team.as_ref(),
|
|
816
|
-
source_agent_id,
|
|
817
|
-
as_agent_id,
|
|
818
|
-
label,
|
|
819
|
-
)
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
/// `request_human` (`tools.py:342-346`): create a `requires_ack` leader message via
|
|
823
|
-
/// the shared leader-delivery funnel; sender = env / inferred / `"unknown"`.
|
|
824
|
-
/// Returns `{ok:true, message_id, status:"needs_human"}`.
|
|
825
|
-
pub fn request_human(
|
|
826
|
-
&self,
|
|
827
|
-
question: &str,
|
|
828
|
-
task_id: Option<&str>,
|
|
829
|
-
agent_id: Option<&str>,
|
|
830
|
-
) -> ToolResult {
|
|
831
|
-
let _owner_team = self.canonical_owner_team_key()?;
|
|
832
|
-
let explicit_sender = agent_id.and_then(non_empty_string);
|
|
833
|
-
let sender = explicit_sender
|
|
834
|
-
.or_else(|| self.agent_id.as_ref().map(AgentId::as_str))
|
|
835
|
-
.unwrap_or("unknown");
|
|
836
|
-
let event_log = EventLog::new(&self.workspace);
|
|
837
|
-
if explicit_sender.is_none() && self.agent_id.is_none() {
|
|
838
|
-
event_log
|
|
839
|
-
.write(
|
|
840
|
-
"mcp.identity_inference_failed",
|
|
841
|
-
serde_json::json!({"tool": "request_human"}),
|
|
842
|
-
)
|
|
843
|
-
.map_err(tool_runtime_error)?;
|
|
844
|
-
}
|
|
845
|
-
// #230 N31/N32 funnel: request_human is a leader-bound caller and must go through
|
|
846
|
-
// the same primitive as send_message(to=leader) / report_result / idle reminder.
|
|
847
|
-
// The legacy path was a raw store insert for recipient="leader" that
|
|
848
|
-
// bypassed the leader-delivery audit (no deliver_to_leader.submit emit, no rebind
|
|
849
|
-
// guard, no leader_notification_log dedup). funnel it now.
|
|
850
|
-
let state = crate::state::persist::load_runtime_state(&self.workspace)
|
|
851
|
-
.unwrap_or(serde_json::json!({}));
|
|
852
|
-
let task = task_id.map(|t| TaskId::new(t.to_string()));
|
|
853
|
-
let outcome = crate::messaging::send_to_leader_receiver(
|
|
854
|
-
&self.workspace,
|
|
855
|
-
&state,
|
|
856
|
-
"leader",
|
|
857
|
-
question,
|
|
858
|
-
task.as_ref(),
|
|
859
|
-
sender,
|
|
860
|
-
true,
|
|
861
|
-
None,
|
|
862
|
-
&event_log,
|
|
863
|
-
)
|
|
864
|
-
.map_err(tool_runtime_error)?;
|
|
865
|
-
let mut fields = serde_json::Map::new();
|
|
866
|
-
fields.insert("ok".to_string(), Value::Bool(outcome.ok));
|
|
867
|
-
fields.insert(
|
|
868
|
-
"message_id".to_string(),
|
|
869
|
-
outcome
|
|
870
|
-
.message_id
|
|
871
|
-
.clone()
|
|
872
|
-
.map_or(Value::Null, Value::String),
|
|
873
|
-
);
|
|
874
|
-
fields.insert(
|
|
875
|
-
"status".to_string(),
|
|
876
|
-
Value::String("needs_human".to_string()),
|
|
877
|
-
);
|
|
878
|
-
Ok(ToolOk { fields })
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
/// `stuck_list` (`tools.py:348-349`): delegate to [`messaging::stuck_list`] (the
|
|
882
|
-
/// team-scoped suppressed-alert projection).
|
|
883
|
-
pub fn stuck_list(&self) -> ToolResult {
|
|
884
|
-
let _owner_team = self.canonical_owner_team_key()?;
|
|
885
|
-
messaging::stuck_list(&self.workspace)
|
|
886
|
-
.map_err(tool_runtime_error)
|
|
887
|
-
.map(|v| ToolOk {
|
|
888
|
-
fields: object_fields(v),
|
|
889
|
-
})
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
/// `stuck_cancel` (`tools.py:351-352`): delegate to [`messaging::stuck_cancel`];
|
|
893
|
-
/// `suppressed_by` = env agent_id / `"leader"`.
|
|
894
|
-
pub fn stuck_cancel(&self, agent_id: &str, alert_type: &str) -> ToolResult {
|
|
895
|
-
let _owner_team = self.canonical_owner_team_key()?;
|
|
896
|
-
let alert = match alert_type {
|
|
897
|
-
"stuck" => Some(messaging::AlertType::Stuck),
|
|
898
|
-
"idle_fallback" => Some(messaging::AlertType::IdleFallback),
|
|
899
|
-
"cross_worker_deadlock" => Some(messaging::AlertType::CrossWorkerDeadlock),
|
|
900
|
-
"all" => None,
|
|
901
|
-
// scheduler.py:268-273 — an unknown alert_type refuses with the Python
|
|
902
|
-
// literal instead of silently widening to suppressing ALL alert families.
|
|
903
|
-
_ => {
|
|
904
|
-
return Ok(ToolOk {
|
|
905
|
-
fields: object_fields(serde_json::json!({
|
|
906
|
-
"ok": false,
|
|
907
|
-
"status": "refused",
|
|
908
|
-
"reason": "invalid_alert_type",
|
|
909
|
-
"alert_type": alert_type,
|
|
910
|
-
})),
|
|
911
|
-
});
|
|
912
|
-
}
|
|
913
|
-
};
|
|
914
|
-
let suppressed_by = self
|
|
915
|
-
.agent_id
|
|
916
|
-
.as_ref()
|
|
917
|
-
.map(AgentId::as_str)
|
|
918
|
-
.unwrap_or("leader");
|
|
919
|
-
messaging::stuck_cancel(&self.workspace, agent_id, alert, suppressed_by)
|
|
920
|
-
.map_err(tool_runtime_error)
|
|
921
|
-
.map(|v| ToolOk {
|
|
922
|
-
fields: object_fields(v),
|
|
923
|
-
})
|
|
924
|
-
}
|
|
925
|
-
|
|
926
635
|
/// `get_visible_peers` (`tools.py:226-247`): C16 scope-filtered peer list — live
|
|
927
636
|
/// agents within the spawn-time owner-team scope only; other teams and dead/stopped
|
|
928
637
|
/// agents are filtered server-side and never named.
|
|
@@ -1275,63 +984,6 @@ fn requested_scope_arg(args: &Value) -> Option<String> {
|
|
|
1275
984
|
.or_else(|| args.get("workspace").map(|_| "workspace".to_string()))
|
|
1276
985
|
}
|
|
1277
986
|
|
|
1278
|
-
fn assignment_team_key(state: &Value) -> Option<String> {
|
|
1279
|
-
state
|
|
1280
|
-
.get("active_team_key")
|
|
1281
|
-
.and_then(Value::as_str)
|
|
1282
|
-
.and_then(non_empty_string)
|
|
1283
|
-
.map(ToString::to_string)
|
|
1284
|
-
}
|
|
1285
|
-
|
|
1286
|
-
fn reconcile_assigned_task(state: &mut Value, team_key: Option<&str>, task: &Value) {
|
|
1287
|
-
let mut top = state
|
|
1288
|
-
.get("tasks")
|
|
1289
|
-
.and_then(Value::as_array)
|
|
1290
|
-
.cloned()
|
|
1291
|
-
.unwrap_or_default();
|
|
1292
|
-
upsert_task_in_place(&mut top, task);
|
|
1293
|
-
if let Some(root) = state.as_object_mut() {
|
|
1294
|
-
root.insert("tasks".to_string(), Value::Array(top.clone()));
|
|
1295
|
-
}
|
|
1296
|
-
if let Some(key) = team_key {
|
|
1297
|
-
let mut team_tasks = state
|
|
1298
|
-
.get("teams")
|
|
1299
|
-
.and_then(|v| v.get(key))
|
|
1300
|
-
.and_then(|team| team.get("tasks"))
|
|
1301
|
-
.and_then(Value::as_array)
|
|
1302
|
-
.cloned()
|
|
1303
|
-
.unwrap_or_default();
|
|
1304
|
-
upsert_task_in_place(&mut team_tasks, task);
|
|
1305
|
-
write_team_tasks(state, key, team_tasks);
|
|
1306
|
-
}
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
fn upsert_task_in_place(tasks: &mut Vec<Value>, task: &Value) {
|
|
1310
|
-
let Some(task_id) = task.get("id").and_then(Value::as_str) else {
|
|
1311
|
-
return;
|
|
1312
|
-
};
|
|
1313
|
-
for existing in tasks.iter_mut() {
|
|
1314
|
-
if existing.get("id").and_then(Value::as_str) == Some(task_id) {
|
|
1315
|
-
merge_object_fields(existing, task);
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
tasks.push(task.clone());
|
|
1320
|
-
}
|
|
1321
|
-
|
|
1322
|
-
fn merge_object_fields(existing: &mut Value, incoming: &Value) {
|
|
1323
|
-
let Some(existing_obj) = existing.as_object_mut() else {
|
|
1324
|
-
*existing = incoming.clone();
|
|
1325
|
-
return;
|
|
1326
|
-
};
|
|
1327
|
-
let Some(incoming_obj) = incoming.as_object() else {
|
|
1328
|
-
return;
|
|
1329
|
-
};
|
|
1330
|
-
for (key, value) in incoming_obj {
|
|
1331
|
-
existing_obj.insert(key.clone(), value.clone());
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
987
|
fn push_report_warning(obj: &mut serde_json::Map<String, Value>, warning: Value) {
|
|
1336
988
|
let Some(code) = warning.get("code").and_then(Value::as_str) else {
|
|
1337
989
|
return;
|
|
@@ -1355,49 +1007,6 @@ fn push_report_warning(obj: &mut serde_json::Map<String, Value>, warning: Value)
|
|
|
1355
1007
|
}
|
|
1356
1008
|
}
|
|
1357
1009
|
|
|
1358
|
-
fn write_team_tasks(state: &mut Value, team_key: &str, tasks: Vec<Value>) {
|
|
1359
|
-
let Some(root) = state.as_object_mut() else {
|
|
1360
|
-
return;
|
|
1361
|
-
};
|
|
1362
|
-
let teams = root
|
|
1363
|
-
.entry("teams".to_string())
|
|
1364
|
-
.or_insert_with(|| Value::Object(serde_json::Map::new()));
|
|
1365
|
-
let Some(teams_obj) = teams.as_object_mut() else {
|
|
1366
|
-
return;
|
|
1367
|
-
};
|
|
1368
|
-
let team = teams_obj.entry(team_key.to_string()).or_insert_with(|| {
|
|
1369
|
-
let mut team = serde_json::Map::new();
|
|
1370
|
-
team.insert("tasks".to_string(), Value::Array(Vec::new()));
|
|
1371
|
-
team.insert("status".to_string(), Value::String("alive".to_string()));
|
|
1372
|
-
Value::Object(team)
|
|
1373
|
-
});
|
|
1374
|
-
let Some(team_obj) = team.as_object_mut() else {
|
|
1375
|
-
return;
|
|
1376
|
-
};
|
|
1377
|
-
team_obj.insert("tasks".to_string(), Value::Array(tasks));
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
fn assignment_message(task: &Value, explicit: Option<&str>) -> String {
|
|
1381
|
-
if let Some(message) = explicit.and_then(non_empty_string) {
|
|
1382
|
-
return message.to_string();
|
|
1383
|
-
}
|
|
1384
|
-
for key in ["description", "title"] {
|
|
1385
|
-
if let Some(text) = task
|
|
1386
|
-
.get(key)
|
|
1387
|
-
.and_then(Value::as_str)
|
|
1388
|
-
.and_then(non_empty_string)
|
|
1389
|
-
{
|
|
1390
|
-
return text.to_string();
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
json_dumps_default(task)
|
|
1394
|
-
}
|
|
1395
|
-
|
|
1396
|
-
fn task_recovery_marker(task: &Value) -> bool {
|
|
1397
|
-
task.get("recovery").and_then(Value::as_bool) == Some(true)
|
|
1398
|
-
|| task.get("acceptance_marker").and_then(Value::as_str) == Some("recovery")
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
1010
|
fn scope_override_name(scope: Scope) -> Option<&'static str> {
|
|
1402
1011
|
match scope {
|
|
1403
1012
|
Scope::Team => Some("team"),
|