@team-agent/installer 0.4.11 → 0.5.1
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/adapters.rs +1 -0
- package/crates/team-agent/src/cli/diagnose.rs +2 -15
- package/crates/team-agent/src/cli/emit.rs +98 -5
- package/crates/team-agent/src/cli/mod.rs +2 -0
- package/crates/team-agent/src/cli/named_address.rs +864 -0
- package/crates/team-agent/src/cli/send.rs +104 -0
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/mod.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +455 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +4 -0
- package/crates/team-agent/src/compiler.rs +11 -20
- package/crates/team-agent/src/coordinator/orphan.rs +2 -2
- package/crates/team-agent/src/coordinator/runtime_detectors.rs +5 -4
- package/crates/team-agent/src/coordinator/steps/abnormal.rs +1776 -2
- package/crates/team-agent/src/coordinator/steps/mod.rs +19 -0
- package/crates/team-agent/src/coordinator/tests/a0_lostupdate.rs +78 -7
- package/crates/team-agent/src/coordinator/tests/abnormal.rs +195 -0
- package/crates/team-agent/src/coordinator/tick.rs +31 -932
- package/crates/team-agent/src/diagnose/orphans.rs +66 -8
- package/crates/team-agent/src/layout/worker_window_helpers.rs +5 -7
- package/crates/team-agent/src/leader/provider_attribution.rs +7 -5
- package/crates/team-agent/src/leader/rediscover.rs +1 -11
- package/crates/team-agent/src/lifecycle/launch/plan.rs +19 -8
- package/crates/team-agent/src/lifecycle/launch.rs +135 -58
- package/crates/team-agent/src/lifecycle/lock.rs +302 -0
- package/crates/team-agent/src/lifecycle/mod.rs +1 -0
- package/crates/team-agent/src/lifecycle/profile_smoke.rs +1 -11
- package/crates/team-agent/src/lifecycle/restart/agent.rs +169 -113
- package/crates/team-agent/src/lifecycle/restart/common.rs +108 -17
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +165 -32
- package/crates/team-agent/src/lifecycle/restart/remove.rs +16 -2
- package/crates/team-agent/src/lifecycle/restart/selection.rs +2 -1
- package/crates/team-agent/src/lifecycle/tests/agent_ops.rs +6 -0
- package/crates/team-agent/src/lifecycle/tests/lane_ops.rs +3 -3
- package/crates/team-agent/src/lifecycle/tests/lifecycle_lock.rs +321 -0
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +35 -5
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +675 -0
- package/crates/team-agent/src/lifecycle/tests.rs +3 -0
- package/crates/team-agent/src/lifecycle/types.rs +8 -0
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +24 -9
- package/crates/team-agent/src/mcp_server/tools.rs +157 -68
- package/crates/team-agent/src/messaging/activity.rs +43 -18
- package/crates/team-agent/src/messaging/delivery.rs +161 -97
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +22 -1
- package/crates/team-agent/src/messaging/results.rs +34 -9
- package/crates/team-agent/src/messaging/scheduler.rs +52 -9
- package/crates/team-agent/src/model/spec.rs +10 -6
- package/crates/team-agent/src/provider/adapter.rs +10 -1038
- package/crates/team-agent/src/provider/adapters/claude.rs +3 -8
- package/crates/team-agent/src/provider/adapters/copilot.rs +2 -5
- package/crates/team-agent/src/provider/classify.rs +23 -48
- package/crates/team-agent/src/provider/command.rs +16 -7
- package/crates/team-agent/src/provider/faults.rs +93 -53
- package/crates/team-agent/src/provider/session/capture.rs +4 -15
- package/crates/team-agent/src/provider/session_scan/claude.rs +309 -0
- package/crates/team-agent/src/provider/session_scan/codex.rs +40 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +350 -0
- package/crates/team-agent/src/provider/session_scan/copilot.rs +202 -0
- package/crates/team-agent/src/provider/session_scan.rs +65 -27
- package/crates/team-agent/src/provider/tests/faults.rs +80 -0
- package/crates/team-agent/src/provider/types.rs +33 -1
- package/crates/team-agent/src/provider/wire.rs +171 -1
- package/crates/team-agent/src/state/identity.rs +242 -57
- package/crates/team-agent/src/state/identity_keys.rs +9 -12
- package/crates/team-agent/src/state/mod.rs +5 -1
- package/crates/team-agent/src/state/owner_gate.rs +59 -15
- package/crates/team-agent/src/state/ownership.rs +12 -11
- package/crates/team-agent/src/state/paths.rs +13 -6
- package/crates/team-agent/src/state/persist.rs +671 -128
- package/crates/team-agent/src/state/projection.rs +240 -49
- package/crates/team-agent/src/state/selector.rs +11 -5
- package/crates/team-agent/src/tmux_backend/tests.rs +51 -2
- package/crates/team-agent/src/tmux_backend.rs +42 -5
- package/crates/team-agent/src/transport/test_support.rs +55 -6
- package/package.json +4 -4
|
@@ -26,13 +26,23 @@ pub(crate) fn update_state(
|
|
|
26
26
|
let mut state = selected.state;
|
|
27
27
|
ensure_object(&mut state);
|
|
28
28
|
append_note(&mut state, note);
|
|
29
|
-
crate::state::projection::
|
|
30
|
-
.
|
|
29
|
+
crate::state::projection::save_team_scoped_state_reapplying_after_conflict(
|
|
30
|
+
&selected.run_workspace,
|
|
31
|
+
&state,
|
|
32
|
+
|latest| {
|
|
33
|
+
ensure_object(latest);
|
|
34
|
+
append_note(latest, note);
|
|
35
|
+
},
|
|
36
|
+
)
|
|
37
|
+
.map_err(tool_runtime_error)?;
|
|
31
38
|
let spec_path = selected
|
|
32
39
|
.spec_path
|
|
33
40
|
.ok_or_else(|| tool_runtime_error("active team spec not found for update_state"))?;
|
|
34
41
|
let spec_workspace = spec_path.parent().ok_or_else(|| {
|
|
35
|
-
tool_runtime_error(format!(
|
|
42
|
+
tool_runtime_error(format!(
|
|
43
|
+
"active team spec has no parent: {}",
|
|
44
|
+
spec_path.display()
|
|
45
|
+
))
|
|
36
46
|
})?;
|
|
37
47
|
let spec_text = std::fs::read_to_string(&spec_path).map_err(tool_runtime_error)?;
|
|
38
48
|
let spec = crate::model::yaml::loads(&spec_text).map_err(tool_runtime_error)?;
|
|
@@ -56,8 +66,16 @@ fn update_state_without_spec(
|
|
|
56
66
|
ensure_object(&mut state);
|
|
57
67
|
seed_legacy_team_key(&mut state, &selected.run_workspace, &selected.team_key);
|
|
58
68
|
append_note(&mut state, note);
|
|
59
|
-
crate::state::projection::
|
|
60
|
-
.
|
|
69
|
+
crate::state::projection::save_team_scoped_state_reapplying_after_conflict(
|
|
70
|
+
&selected.run_workspace,
|
|
71
|
+
&state,
|
|
72
|
+
|latest| {
|
|
73
|
+
ensure_object(latest);
|
|
74
|
+
seed_legacy_team_key(latest, &selected.run_workspace, &selected.team_key);
|
|
75
|
+
append_note(latest, note);
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
.map_err(tool_runtime_error)?;
|
|
61
79
|
let path = crate::lifecycle::restart::write_team_state(
|
|
62
80
|
&selected.run_workspace,
|
|
63
81
|
&crate::model::yaml::Value::Null,
|
|
@@ -120,10 +138,7 @@ fn is_missing_active_spec(err: &crate::state::StateError) -> bool {
|
|
|
120
138
|
)
|
|
121
139
|
}
|
|
122
140
|
|
|
123
|
-
pub(crate) fn get_team_status(
|
|
124
|
-
workspace: &Path,
|
|
125
|
-
owner_team: Option<&TeamKey>,
|
|
126
|
-
) -> ToolResult {
|
|
141
|
+
pub(crate) fn get_team_status(workspace: &Path, owner_team: Option<&TeamKey>) -> ToolResult {
|
|
127
142
|
let selected = crate::state::selector::resolve_active_team(
|
|
128
143
|
workspace,
|
|
129
144
|
owner_team.map(TeamKey::as_str),
|
|
@@ -12,7 +12,9 @@ use crate::model::ids::{AgentId, TaskId, TeamKey};
|
|
|
12
12
|
use crate::event_log::EventLog;
|
|
13
13
|
|
|
14
14
|
// ── REUSE: step 5 state persist / projection ────────────────────────────────
|
|
15
|
-
use crate::state::persist::{
|
|
15
|
+
use crate::state::persist::{
|
|
16
|
+
load_runtime_state, save_runtime_state, save_runtime_state_reapplying_after_conflict,
|
|
17
|
+
};
|
|
16
18
|
|
|
17
19
|
// ── REUSE: step 11 messaging delegate surface ───────────────────────────────
|
|
18
20
|
use crate::messaging::{self, MessageTarget, SendOptions};
|
|
@@ -20,13 +22,15 @@ use crate::messaging::{self, MessageTarget, SendOptions};
|
|
|
20
22
|
use super::helpers::{
|
|
21
23
|
delivery_outcome_value, ensure_object, enum_value, insert_array, is_worker_recipient,
|
|
22
24
|
json_dumps_default, latest_task_for_assignee, latest_uncorrelated_delivered_message_for,
|
|
23
|
-
non_empty_string, normalized_envelope_value, object_fields,
|
|
24
|
-
|
|
25
|
+
non_empty_string, normalized_envelope_value, object_fields, requires_ack_for_target,
|
|
26
|
+
tool_runtime_error,
|
|
25
27
|
};
|
|
26
28
|
use super::normalize::{
|
|
27
29
|
compact_tool_result, normalize_report_envelope, normalize_result_status_observed,
|
|
28
30
|
};
|
|
29
|
-
use super::types::{
|
|
31
|
+
use super::types::{
|
|
32
|
+
Scope, SendOutcome, ToolError, ToolErrorReason, ToolOk, ToolResult, VisiblePeers,
|
|
33
|
+
};
|
|
30
34
|
|
|
31
35
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
32
36
|
// TeamOrchestratorTools (tools.py:72) — the 12 typed tool handlers.
|
|
@@ -64,7 +68,11 @@ impl TeamOrchestratorTools {
|
|
|
64
68
|
|
|
65
69
|
/// Test/explicit-injection constructor: bind identity/scope directly instead of
|
|
66
70
|
/// reading env (so contracts can exercise scoped behavior deterministically).
|
|
67
|
-
pub fn with_identity(
|
|
71
|
+
pub fn with_identity(
|
|
72
|
+
workspace: &Path,
|
|
73
|
+
agent_id: Option<AgentId>,
|
|
74
|
+
owner_team_id: Option<TeamKey>,
|
|
75
|
+
) -> Self {
|
|
68
76
|
Self {
|
|
69
77
|
workspace: std::fs::canonicalize(workspace).unwrap_or_else(|_| workspace.to_path_buf()),
|
|
70
78
|
agent_id,
|
|
@@ -85,7 +93,11 @@ impl TeamOrchestratorTools {
|
|
|
85
93
|
"ValueError",
|
|
86
94
|
));
|
|
87
95
|
};
|
|
88
|
-
let Some(task_id) = task
|
|
96
|
+
let Some(task_id) = task
|
|
97
|
+
.get("id")
|
|
98
|
+
.and_then(Value::as_str)
|
|
99
|
+
.and_then(non_empty_string)
|
|
100
|
+
else {
|
|
89
101
|
return Err(ToolError::new(
|
|
90
102
|
ToolErrorReason::InvalidToolArguments,
|
|
91
103
|
"assign_task task.id is required",
|
|
@@ -112,7 +124,12 @@ impl TeamOrchestratorTools {
|
|
|
112
124
|
.map(|team| team.as_str().to_string())
|
|
113
125
|
.or_else(|| assignment_team_key(&state));
|
|
114
126
|
reconcile_assigned_task(&mut state, team_key.as_deref(), &task_value);
|
|
115
|
-
|
|
127
|
+
save_runtime_state_reapplying_after_conflict(&self.workspace, &state, |latest| {
|
|
128
|
+
ensure_object(latest);
|
|
129
|
+
let latest_team_key = team_key.clone().or_else(|| assignment_team_key(latest));
|
|
130
|
+
reconcile_assigned_task(latest, latest_team_key.as_deref(), &task_value);
|
|
131
|
+
})
|
|
132
|
+
.map_err(tool_runtime_error)?;
|
|
116
133
|
|
|
117
134
|
let content = assignment_message(task, message);
|
|
118
135
|
let out = self.send_message(
|
|
@@ -191,7 +208,8 @@ impl TeamOrchestratorTools {
|
|
|
191
208
|
..SendOptions::default()
|
|
192
209
|
};
|
|
193
210
|
if is_worker_recipient(to) {
|
|
194
|
-
let out = messaging::send_message(&self.workspace, to, content, &opts)
|
|
211
|
+
let out = messaging::send_message(&self.workspace, to, content, &opts)
|
|
212
|
+
.map_err(tool_runtime_error)?;
|
|
195
213
|
// tools.py:175-181 — accepted+poll_via ONLY for a REAL message_id; any other
|
|
196
214
|
// outcome falls back to the compacted direct result. Never invent an
|
|
197
215
|
// `mcp_<timestamp>` id: it does not exist in the store and makes the
|
|
@@ -209,7 +227,8 @@ impl TeamOrchestratorTools {
|
|
|
209
227
|
message_id,
|
|
210
228
|
});
|
|
211
229
|
}
|
|
212
|
-
let out = messaging::send_message(&self.workspace, to, content, &opts)
|
|
230
|
+
let out = messaging::send_message(&self.workspace, to, content, &opts)
|
|
231
|
+
.map_err(tool_runtime_error)?;
|
|
213
232
|
let value = delivery_outcome_value(&out);
|
|
214
233
|
let ok = compact_tool_result(&value)?;
|
|
215
234
|
Ok(SendOutcome::Direct(ok))
|
|
@@ -219,7 +238,11 @@ impl TeamOrchestratorTools {
|
|
|
219
238
|
self.rpc_scope_refused("unknown", None, None)
|
|
220
239
|
}
|
|
221
240
|
|
|
222
|
-
pub(crate) fn validate_rpc_scope_args(
|
|
241
|
+
pub(crate) fn validate_rpc_scope_args(
|
|
242
|
+
&self,
|
|
243
|
+
tool: &str,
|
|
244
|
+
args: &Value,
|
|
245
|
+
) -> Result<(), ToolError> {
|
|
223
246
|
if let Some(nested) = args.get("task").or_else(|| args.get("envelope")) {
|
|
224
247
|
self.validate_rpc_scope_args(tool, nested)?;
|
|
225
248
|
}
|
|
@@ -240,10 +263,11 @@ impl TeamOrchestratorTools {
|
|
|
240
263
|
Ok(state) => state,
|
|
241
264
|
Err(error) => return Err(self.scope_unverifiable(&error.to_string())),
|
|
242
265
|
};
|
|
243
|
-
let requested_canonical =
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
266
|
+
let requested_canonical =
|
|
267
|
+
crate::state::projection::resolve_owner_team_id(&state, requested)
|
|
268
|
+
.canonical_key()
|
|
269
|
+
.unwrap_or(requested)
|
|
270
|
+
.to_string();
|
|
247
271
|
requested_canonical != owner.as_str()
|
|
248
272
|
}
|
|
249
273
|
(None, Some(_)) => true,
|
|
@@ -280,13 +304,17 @@ impl TeamOrchestratorTools {
|
|
|
280
304
|
if let Some(envelope) = envelope {
|
|
281
305
|
self.validate_rpc_scope_args("report_result", envelope)?;
|
|
282
306
|
}
|
|
283
|
-
let mut base = envelope
|
|
307
|
+
let mut base = envelope
|
|
308
|
+
.cloned()
|
|
309
|
+
.unwrap_or_else(|| Value::Object(serde_json::Map::new()));
|
|
284
310
|
ensure_object(&mut base);
|
|
285
311
|
if let Some(obj) = base.as_object_mut() {
|
|
286
312
|
if !obj.contains_key("summary") {
|
|
287
313
|
obj.insert(
|
|
288
314
|
"summary".to_string(),
|
|
289
|
-
Value::String(
|
|
315
|
+
Value::String(
|
|
316
|
+
summary.map_or_else(|| "completed".to_string(), ToString::to_string),
|
|
317
|
+
),
|
|
290
318
|
);
|
|
291
319
|
}
|
|
292
320
|
if !obj.contains_key("status") {
|
|
@@ -302,33 +330,38 @@ impl TeamOrchestratorTools {
|
|
|
302
330
|
// task id and no result yet (message-scope correlation —
|
|
303
331
|
// collect path: is_message_scoped_result)
|
|
304
332
|
// 4. "manual" — truly uncorrelated; collect still rejects
|
|
305
|
-
let owner_team_id_str = self
|
|
306
|
-
.owner_team_id
|
|
307
|
-
.as_ref()
|
|
308
|
-
.map(|t| t.as_str().to_string());
|
|
333
|
+
let owner_team_id_str = self.owner_team_id.as_ref().map(|t| t.as_str().to_string());
|
|
309
334
|
let resolved = task_id
|
|
310
335
|
.map(ToString::to_string)
|
|
311
|
-
.or_else(||
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
336
|
+
.or_else(|| {
|
|
337
|
+
self.agent_id.as_ref().and_then(|agent| {
|
|
338
|
+
latest_task_for_assignee(
|
|
339
|
+
&self.workspace,
|
|
340
|
+
agent.as_str(),
|
|
341
|
+
owner_team_id_str.as_deref(),
|
|
342
|
+
)
|
|
343
|
+
})
|
|
344
|
+
})
|
|
345
|
+
.or_else(|| {
|
|
346
|
+
self.agent_id.as_ref().and_then(|agent| {
|
|
347
|
+
latest_uncorrelated_delivered_message_for(
|
|
348
|
+
&self.workspace,
|
|
349
|
+
agent.as_str(),
|
|
350
|
+
owner_team_id_str.as_deref(),
|
|
351
|
+
)
|
|
352
|
+
})
|
|
353
|
+
})
|
|
325
354
|
.unwrap_or_else(|| "manual".to_string());
|
|
326
355
|
obj.insert("task_id".to_string(), Value::String(resolved));
|
|
327
356
|
}
|
|
328
357
|
if !obj.contains_key("agent_id") {
|
|
329
358
|
let resolved = agent_id
|
|
330
359
|
.map(ToString::to_string)
|
|
331
|
-
.or_else(||
|
|
360
|
+
.or_else(|| {
|
|
361
|
+
self.agent_id
|
|
362
|
+
.as_ref()
|
|
363
|
+
.map(|env_agent| env_agent.as_str().to_string())
|
|
364
|
+
})
|
|
332
365
|
.unwrap_or_else(|| "unknown".to_string());
|
|
333
366
|
obj.insert("agent_id".to_string(), Value::String(resolved));
|
|
334
367
|
}
|
|
@@ -364,8 +397,8 @@ impl TeamOrchestratorTools {
|
|
|
364
397
|
&env_value,
|
|
365
398
|
owner_team.as_ref().map(TeamKey::as_str),
|
|
366
399
|
)
|
|
367
|
-
|
|
368
|
-
|
|
400
|
+
.map_err(tool_runtime_error)
|
|
401
|
+
.and_then(|value| compact_tool_result(&value))
|
|
369
402
|
}
|
|
370
403
|
|
|
371
404
|
/// T3-1 cr verdict: the observable record of an unknown→Partial status
|
|
@@ -405,7 +438,12 @@ impl TeamOrchestratorTools {
|
|
|
405
438
|
/// `reset_agent` (`tools.py:333-334`): delegated through the lifecycle tools facade.
|
|
406
439
|
pub fn reset_agent(&self, agent_id: &str, discard_session: bool) -> ToolResult {
|
|
407
440
|
let owner_team = self.canonical_owner_team_key()?;
|
|
408
|
-
super::lifecycle_tools::reset_agent(
|
|
441
|
+
super::lifecycle_tools::reset_agent(
|
|
442
|
+
&self.workspace,
|
|
443
|
+
owner_team.as_ref(),
|
|
444
|
+
agent_id,
|
|
445
|
+
discard_session,
|
|
446
|
+
)
|
|
409
447
|
}
|
|
410
448
|
|
|
411
449
|
/// `add_agent` (`tools.py:336-337`): delegate to real lifecycle add-agent
|
|
@@ -442,7 +480,12 @@ impl TeamOrchestratorTools {
|
|
|
442
480
|
}
|
|
443
481
|
|
|
444
482
|
/// `fork_agent` (`tools.py:339-340`): delegated through the lifecycle tools facade.
|
|
445
|
-
pub fn fork_agent(
|
|
483
|
+
pub fn fork_agent(
|
|
484
|
+
&self,
|
|
485
|
+
source_agent_id: &str,
|
|
486
|
+
as_agent_id: &str,
|
|
487
|
+
label: Option<&str>,
|
|
488
|
+
) -> ToolResult {
|
|
446
489
|
let owner_team = self.canonical_owner_team_key()?;
|
|
447
490
|
super::lifecycle_tools::fork_agent(
|
|
448
491
|
&self.workspace,
|
|
@@ -456,7 +499,12 @@ impl TeamOrchestratorTools {
|
|
|
456
499
|
/// `request_human` (`tools.py:342-346`): create a `requires_ack` leader message via
|
|
457
500
|
/// the shared leader-delivery funnel; sender = env / inferred / `"unknown"`.
|
|
458
501
|
/// Returns `{ok:true, message_id, status:"needs_human"}`.
|
|
459
|
-
pub fn request_human(
|
|
502
|
+
pub fn request_human(
|
|
503
|
+
&self,
|
|
504
|
+
question: &str,
|
|
505
|
+
task_id: Option<&str>,
|
|
506
|
+
agent_id: Option<&str>,
|
|
507
|
+
) -> ToolResult {
|
|
460
508
|
let _owner_team = self.canonical_owner_team_key()?;
|
|
461
509
|
let explicit_sender = agent_id.and_then(non_empty_string);
|
|
462
510
|
let sender = explicit_sender
|
|
@@ -495,9 +543,15 @@ impl TeamOrchestratorTools {
|
|
|
495
543
|
fields.insert("ok".to_string(), Value::Bool(outcome.ok));
|
|
496
544
|
fields.insert(
|
|
497
545
|
"message_id".to_string(),
|
|
498
|
-
outcome
|
|
546
|
+
outcome
|
|
547
|
+
.message_id
|
|
548
|
+
.clone()
|
|
549
|
+
.map_or(Value::Null, Value::String),
|
|
550
|
+
);
|
|
551
|
+
fields.insert(
|
|
552
|
+
"status".to_string(),
|
|
553
|
+
Value::String("needs_human".to_string()),
|
|
499
554
|
);
|
|
500
|
-
fields.insert("status".to_string(), Value::String("needs_human".to_string()));
|
|
501
555
|
Ok(ToolOk { fields })
|
|
502
556
|
}
|
|
503
557
|
|
|
@@ -507,7 +561,9 @@ impl TeamOrchestratorTools {
|
|
|
507
561
|
let _owner_team = self.canonical_owner_team_key()?;
|
|
508
562
|
messaging::stuck_list(&self.workspace)
|
|
509
563
|
.map_err(tool_runtime_error)
|
|
510
|
-
.map(|v| ToolOk {
|
|
564
|
+
.map(|v| ToolOk {
|
|
565
|
+
fields: object_fields(v),
|
|
566
|
+
})
|
|
511
567
|
}
|
|
512
568
|
|
|
513
569
|
/// `stuck_cancel` (`tools.py:351-352`): delegate to [`messaging::stuck_cancel`];
|
|
@@ -532,10 +588,16 @@ impl TeamOrchestratorTools {
|
|
|
532
588
|
});
|
|
533
589
|
}
|
|
534
590
|
};
|
|
535
|
-
let suppressed_by = self
|
|
591
|
+
let suppressed_by = self
|
|
592
|
+
.agent_id
|
|
593
|
+
.as_ref()
|
|
594
|
+
.map(AgentId::as_str)
|
|
595
|
+
.unwrap_or("leader");
|
|
536
596
|
messaging::stuck_cancel(&self.workspace, agent_id, alert, suppressed_by)
|
|
537
597
|
.map_err(tool_runtime_error)
|
|
538
|
-
.map(|v| ToolOk {
|
|
598
|
+
.map(|v| ToolOk {
|
|
599
|
+
fields: object_fields(v),
|
|
600
|
+
})
|
|
539
601
|
}
|
|
540
602
|
|
|
541
603
|
/// `get_visible_peers` (`tools.py:226-247`): C16 scope-filtered peer list — live
|
|
@@ -577,7 +639,11 @@ impl TeamOrchestratorTools {
|
|
|
577
639
|
/// non-`*`/non-leader string target NOT in the visible-peer scope and not the
|
|
578
640
|
/// sender itself, with `scope != workspace`, → `Some(ToolError{PeerNotInScope})`
|
|
579
641
|
/// (also writes `mcp.send_message_refused`). `None` = allowed to proceed.
|
|
580
|
-
pub fn refuse_cross_team_peer(
|
|
642
|
+
pub fn refuse_cross_team_peer(
|
|
643
|
+
&self,
|
|
644
|
+
to: &MessageTarget,
|
|
645
|
+
scope_override: Option<Scope>,
|
|
646
|
+
) -> Option<ToolError> {
|
|
581
647
|
let owner_team = match self.canonical_owner_team_key() {
|
|
582
648
|
Ok(team) => team,
|
|
583
649
|
Err(error) => return Some(error),
|
|
@@ -599,7 +665,10 @@ impl TeamOrchestratorTools {
|
|
|
599
665
|
|| target == "*"
|
|
600
666
|
|| target == "leader"
|
|
601
667
|
|| target == "Leader"
|
|
602
|
-
|| self
|
|
668
|
+
|| self
|
|
669
|
+
.agent_id
|
|
670
|
+
.as_ref()
|
|
671
|
+
.is_some_and(|id| id.as_str() == target)
|
|
603
672
|
{
|
|
604
673
|
return None;
|
|
605
674
|
}
|
|
@@ -620,10 +689,7 @@ impl TeamOrchestratorTools {
|
|
|
620
689
|
);
|
|
621
690
|
let mut extra = serde_json::Map::new();
|
|
622
691
|
extra.insert("status".to_string(), Value::String("refused".to_string()));
|
|
623
|
-
extra.insert(
|
|
624
|
-
"hint".to_string(),
|
|
625
|
-
Value::String(hint.to_string()),
|
|
626
|
-
);
|
|
692
|
+
extra.insert("hint".to_string(), Value::String(hint.to_string()));
|
|
627
693
|
Some(ToolError {
|
|
628
694
|
reason: ToolErrorReason::PeerNotInScope,
|
|
629
695
|
exc_type: "PeerNotInScope".to_string(),
|
|
@@ -649,7 +715,9 @@ impl TeamOrchestratorTools {
|
|
|
649
715
|
.and_then(Value::as_object)
|
|
650
716
|
.is_some_and(|teams| !teams.is_empty())
|
|
651
717
|
{
|
|
652
|
-
return Err(
|
|
718
|
+
return Err(
|
|
719
|
+
self.scope_refused("TEAM_AGENT_OWNER_TEAM_ID is required for multi-team MCP")
|
|
720
|
+
);
|
|
653
721
|
}
|
|
654
722
|
return Ok(None);
|
|
655
723
|
};
|
|
@@ -674,7 +742,9 @@ impl TeamOrchestratorTools {
|
|
|
674
742
|
.and_then(Value::as_object)
|
|
675
743
|
.is_some_and(|teams| !teams.is_empty())
|
|
676
744
|
{
|
|
677
|
-
return Err(
|
|
745
|
+
return Err(
|
|
746
|
+
self.scope_refused("TEAM_AGENT_OWNER_TEAM_ID is required for multi-team MCP")
|
|
747
|
+
);
|
|
678
748
|
}
|
|
679
749
|
return Ok(None);
|
|
680
750
|
};
|
|
@@ -701,7 +771,10 @@ impl TeamOrchestratorTools {
|
|
|
701
771
|
);
|
|
702
772
|
let mut extra = serde_json::Map::new();
|
|
703
773
|
extra.insert("status".to_string(), Value::String("refused".to_string()));
|
|
704
|
-
extra.insert(
|
|
774
|
+
extra.insert(
|
|
775
|
+
"kind".to_string(),
|
|
776
|
+
Value::String("scope_unverifiable".to_string()),
|
|
777
|
+
);
|
|
705
778
|
extra.insert(
|
|
706
779
|
"next_action".to_string(),
|
|
707
780
|
Value::String("retry shortly or check the runtime state path".to_string()),
|
|
@@ -744,7 +817,11 @@ impl TeamOrchestratorTools {
|
|
|
744
817
|
requested_scope: Option<&str>,
|
|
745
818
|
) -> ToolError {
|
|
746
819
|
let owner_team_id = self.canonical_owner_team_key_for_event();
|
|
747
|
-
let agent_id = self
|
|
820
|
+
let agent_id = self
|
|
821
|
+
.agent_id
|
|
822
|
+
.as_ref()
|
|
823
|
+
.map(AgentId::as_str)
|
|
824
|
+
.unwrap_or("unknown");
|
|
748
825
|
let _ = EventLog::new(&self.workspace).write(
|
|
749
826
|
"mcp.scope_refused",
|
|
750
827
|
serde_json::json!({
|
|
@@ -794,10 +871,20 @@ fn canonicalize_owner_team_id(state: &Value, owner_team_id: &str) -> Option<Stri
|
|
|
794
871
|
}
|
|
795
872
|
|
|
796
873
|
fn requested_team_arg(args: &Value) -> Option<String> {
|
|
797
|
-
[
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
874
|
+
[
|
|
875
|
+
"team",
|
|
876
|
+
"team_id",
|
|
877
|
+
"owner_team_id",
|
|
878
|
+
"owner_team",
|
|
879
|
+
"target_team",
|
|
880
|
+
]
|
|
881
|
+
.iter()
|
|
882
|
+
.find_map(|key| {
|
|
883
|
+
args.get(*key)
|
|
884
|
+
.and_then(Value::as_str)
|
|
885
|
+
.filter(|s| !s.is_empty())
|
|
886
|
+
})
|
|
887
|
+
.map(ToString::to_string)
|
|
801
888
|
}
|
|
802
889
|
|
|
803
890
|
fn requested_scope_arg(args: &Value) -> Option<String> {
|
|
@@ -875,14 +962,12 @@ fn write_team_tasks(state: &mut Value, team_key: &str, tasks: Vec<Value>) {
|
|
|
875
962
|
let Some(teams_obj) = teams.as_object_mut() else {
|
|
876
963
|
return;
|
|
877
964
|
};
|
|
878
|
-
let team = teams_obj
|
|
879
|
-
|
|
880
|
-
.
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
Value::Object(team)
|
|
885
|
-
});
|
|
965
|
+
let team = teams_obj.entry(team_key.to_string()).or_insert_with(|| {
|
|
966
|
+
let mut team = serde_json::Map::new();
|
|
967
|
+
team.insert("tasks".to_string(), Value::Array(Vec::new()));
|
|
968
|
+
team.insert("status".to_string(), Value::String("alive".to_string()));
|
|
969
|
+
Value::Object(team)
|
|
970
|
+
});
|
|
886
971
|
let Some(team_obj) = team.as_object_mut() else {
|
|
887
972
|
return;
|
|
888
973
|
};
|
|
@@ -894,7 +979,11 @@ fn assignment_message(task: &Value, explicit: Option<&str>) -> String {
|
|
|
894
979
|
return message.to_string();
|
|
895
980
|
}
|
|
896
981
|
for key in ["description", "title"] {
|
|
897
|
-
if let Some(text) = task
|
|
982
|
+
if let Some(text) = task
|
|
983
|
+
.get(key)
|
|
984
|
+
.and_then(Value::as_str)
|
|
985
|
+
.and_then(non_empty_string)
|
|
986
|
+
{
|
|
898
987
|
return text.to_string();
|
|
899
988
|
}
|
|
900
989
|
}
|
|
@@ -59,7 +59,10 @@ pub fn detect_idle_fallbacks(
|
|
|
59
59
|
let idle_workers = stmt
|
|
60
60
|
.query_map(params![team.as_str()], |row| row.get::<_, String>(0))?
|
|
61
61
|
.collect::<Result<Vec<_>, _>>()?;
|
|
62
|
-
let worker_set = workers
|
|
62
|
+
let worker_set = workers
|
|
63
|
+
.iter()
|
|
64
|
+
.cloned()
|
|
65
|
+
.collect::<std::collections::BTreeSet<_>>();
|
|
63
66
|
let idle_set = idle_workers
|
|
64
67
|
.iter()
|
|
65
68
|
.cloned()
|
|
@@ -74,8 +77,16 @@ pub fn detect_idle_fallbacks(
|
|
|
74
77
|
}
|
|
75
78
|
let now = chrono::Utc::now().to_rfc3339();
|
|
76
79
|
let mut next_state = state.clone();
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
let suppression_snapshots =
|
|
81
|
+
idle_fallback_suppression_snapshots(&next_state, store, &team, &idle_workers)?;
|
|
82
|
+
register_idle_fallback_suppression(&mut next_state, &team, &now, &suppression_snapshots);
|
|
83
|
+
crate::state::persist::save_runtime_state_reapplying_after_conflict(
|
|
84
|
+
workspace,
|
|
85
|
+
&next_state,
|
|
86
|
+
|latest| {
|
|
87
|
+
register_idle_fallback_suppression(latest, &team, &now, &suppression_snapshots);
|
|
88
|
+
},
|
|
89
|
+
)?;
|
|
79
90
|
let alert_count = idle_workers.len();
|
|
80
91
|
let content = format!(
|
|
81
92
|
"Idle fallback: all workers idle while {obligation_count} undelivered obligation(s) remain."
|
|
@@ -213,7 +224,11 @@ fn active_team_key(workspace: &Path, state: &Value) -> String {
|
|
|
213
224
|
.and_then(Value::as_str)
|
|
214
225
|
.filter(|team| !team.is_empty())
|
|
215
226
|
.map(ToString::to_string)
|
|
216
|
-
.or_else(||
|
|
227
|
+
.or_else(|| {
|
|
228
|
+
workspace
|
|
229
|
+
.file_name()
|
|
230
|
+
.map(|name| name.to_string_lossy().to_string())
|
|
231
|
+
})
|
|
217
232
|
.unwrap_or_else(|| "current".to_string())
|
|
218
233
|
}
|
|
219
234
|
|
|
@@ -298,14 +313,13 @@ fn idle_fallback_suppressed(
|
|
|
298
313
|
&& delivered == delivered_message_ids(store, team, agent_id)?)
|
|
299
314
|
}
|
|
300
315
|
|
|
301
|
-
fn
|
|
302
|
-
state: &
|
|
316
|
+
fn idle_fallback_suppression_snapshots(
|
|
317
|
+
state: &Value,
|
|
303
318
|
store: &MessageStore,
|
|
304
319
|
team: &str,
|
|
305
320
|
idle_workers: &[String],
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
let snapshots = idle_workers
|
|
321
|
+
) -> Result<Vec<(String, Vec<String>, Vec<String>)>, MessagingError> {
|
|
322
|
+
idle_workers
|
|
309
323
|
.iter()
|
|
310
324
|
.map(|agent_id| {
|
|
311
325
|
Ok((
|
|
@@ -314,23 +328,31 @@ fn register_idle_fallback_suppression(
|
|
|
314
328
|
delivered_message_ids(store, team, agent_id)?,
|
|
315
329
|
))
|
|
316
330
|
})
|
|
317
|
-
.collect::<Result<Vec<_>, MessagingError>>()
|
|
331
|
+
.collect::<Result<Vec<_>, MessagingError>>()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
fn register_idle_fallback_suppression(
|
|
335
|
+
state: &mut Value,
|
|
336
|
+
team: &str,
|
|
337
|
+
now: &str,
|
|
338
|
+
snapshots: &[(String, Vec<String>, Vec<String>)],
|
|
339
|
+
) {
|
|
318
340
|
let Some(root) = state.as_object_mut() else {
|
|
319
|
-
return
|
|
341
|
+
return;
|
|
320
342
|
};
|
|
321
343
|
let Some(coordinator) = root
|
|
322
344
|
.entry("coordinator")
|
|
323
345
|
.or_insert_with(|| serde_json::json!({}))
|
|
324
346
|
.as_object_mut()
|
|
325
347
|
else {
|
|
326
|
-
return
|
|
348
|
+
return;
|
|
327
349
|
};
|
|
328
350
|
let Some(last) = coordinator
|
|
329
351
|
.entry("idle_fallback_last_fired_at")
|
|
330
352
|
.or_insert_with(|| serde_json::json!({}))
|
|
331
353
|
.as_object_mut()
|
|
332
354
|
else {
|
|
333
|
-
return
|
|
355
|
+
return;
|
|
334
356
|
};
|
|
335
357
|
last.insert(team.to_string(), serde_json::Value::String(now.to_string()));
|
|
336
358
|
let Some(all) = coordinator
|
|
@@ -338,18 +360,18 @@ fn register_idle_fallback_suppression(
|
|
|
338
360
|
.or_insert_with(|| serde_json::json!({}))
|
|
339
361
|
.as_object_mut()
|
|
340
362
|
else {
|
|
341
|
-
return
|
|
363
|
+
return;
|
|
342
364
|
};
|
|
343
365
|
let Some(team_map) = all
|
|
344
366
|
.entry(team.to_string())
|
|
345
367
|
.or_insert_with(|| serde_json::json!({}))
|
|
346
368
|
.as_object_mut()
|
|
347
369
|
else {
|
|
348
|
-
return
|
|
370
|
+
return;
|
|
349
371
|
};
|
|
350
372
|
for (agent_id, assigned, delivered) in snapshots {
|
|
351
373
|
let Some(agent_map) = team_map
|
|
352
|
-
.entry(agent_id)
|
|
374
|
+
.entry(agent_id.clone())
|
|
353
375
|
.or_insert_with(|| serde_json::json!({}))
|
|
354
376
|
.as_object_mut()
|
|
355
377
|
else {
|
|
@@ -367,7 +389,6 @@ fn register_idle_fallback_suppression(
|
|
|
367
389
|
}),
|
|
368
390
|
);
|
|
369
391
|
}
|
|
370
|
-
Ok(())
|
|
371
392
|
}
|
|
372
393
|
|
|
373
394
|
fn timestamp_in_future(ts: &str) -> bool {
|
|
@@ -407,7 +428,11 @@ fn assigned_task_ids(state: &Value, agent_id: &str) -> Vec<String> {
|
|
|
407
428
|
tasks
|
|
408
429
|
.iter()
|
|
409
430
|
.filter(|task| task.get("assignee").and_then(Value::as_str) == Some(agent_id))
|
|
410
|
-
.filter_map(|task|
|
|
431
|
+
.filter_map(|task| {
|
|
432
|
+
task.get("id")
|
|
433
|
+
.and_then(Value::as_str)
|
|
434
|
+
.map(ToString::to_string)
|
|
435
|
+
})
|
|
411
436
|
.collect::<Vec<_>>()
|
|
412
437
|
})
|
|
413
438
|
.unwrap_or_default();
|