@team-agent/installer 0.4.10 → 0.5.0
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 +62 -4
- 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/leader/start.rs +169 -25
- 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 +436 -121
- 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 +193 -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 +54 -15
- package/crates/team-agent/src/lifecycle/tests/phase_b_contracts.rs +802 -0
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +648 -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/lifecycle/worker_command_context.rs +24 -34
- 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
|
@@ -7,45 +7,35 @@ use crate::model::permissions::{resolve_permissions, AgentPermissionInput};
|
|
|
7
7
|
|
|
8
8
|
const RUNTIME_CONTRACT_SECTION: &str = r#"# Team Agent Teammate Runtime Contract
|
|
9
9
|
|
|
10
|
-
You are a teammate in a Team Agent runtime
|
|
11
|
-
|
|
12
|
-
session is local to this session and is not a team message.
|
|
10
|
+
You are a teammate in a Team Agent runtime. The leader cannot see your terminal
|
|
11
|
+
output. All communication must go through Team Agent MCP tools.
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
- Send progress, blockers, permission needs, tool failures, scope changes, and
|
|
16
|
-
long-running status updates with team_orchestrator.send_message(to='leader',
|
|
17
|
-
content='<short message>').
|
|
18
|
-
- Send to another teammate by agent id when coordination is useful, or use
|
|
19
|
-
to='*' to notify every other team member. The runtime resolves only this team
|
|
20
|
-
and excludes your own worker.
|
|
21
|
-
- When the task is complete, call team_orchestrator.report_result exactly once.
|
|
22
|
-
- Do not pass sender, task_id, agent_id, schema_version, or ack fields unless
|
|
23
|
-
doing a low-level compatibility diagnostic. The MCP runtime fills protocol
|
|
24
|
-
fields from the current worker and task state.
|
|
25
|
-
- Emergency fallback exception: if a leader-bound MCP send/report payload is
|
|
26
|
-
already built but the MCP transport itself fails or the primary delivery path
|
|
27
|
-
errors (for example `Transport closed`, `Connection refused`, `Broken pipe`,
|
|
28
|
-
`EOF`, timeout, or internal delivery error), use `team-agent fallback-send-leader`
|
|
29
|
-
or `team-agent fallback-report-result` exactly once with `--primary-error`.
|
|
30
|
-
Do not use fallback for business refusals such as permission, quota, or unknown
|
|
31
|
-
target. After fallback, tell the leader to run `team-agent restart-agent` to
|
|
32
|
-
refresh the worker MCP transport.
|
|
13
|
+
## Communication (mandatory)
|
|
33
14
|
|
|
34
|
-
|
|
35
|
-
|
|
15
|
+
- Progress, blockers, questions: team_orchestrator.send_message(to='leader', content='...')
|
|
16
|
+
- Coordinate with teammate: team_orchestrator.send_message(to='<agent_id>', content='...')
|
|
17
|
+
- Broadcast to all teammates: team_orchestrator.send_message(to='*', content='...')
|
|
18
|
+
- Task complete: team_orchestrator.report_result(summary='...') — call exactly once
|
|
36
19
|
|
|
37
|
-
When
|
|
38
|
-
|
|
39
|
-
|
|
20
|
+
When you receive a message from the leader or a teammate, you MUST respond
|
|
21
|
+
through MCP tools. Writing a reply in your terminal does nothing — the sender
|
|
22
|
+
will never see it.
|
|
40
23
|
|
|
24
|
+
## Rules
|
|
25
|
+
|
|
26
|
+
- Do not pass sender, task_id, or schema_version — the MCP runtime fills them.
|
|
27
|
+
- If blocked or waiting, send_message to the leader. Do not wait silently.
|
|
28
|
+
- On 500/529/rate-limit errors, wait 1-2 minutes before retrying."#;
|
|
29
|
+
|
|
30
|
+
// 0.4.11 trimmed: the runtime contract section above already covers
|
|
31
|
+
// send_message signatures and report_result exactly-once. The output
|
|
32
|
+
// contract now only carries the RESULT-ENVELOPE-SPECIFIC delivery
|
|
33
|
+
// semantics (leader-attach dependence + fallback status) that the
|
|
34
|
+
// generic runtime section deliberately leaves out.
|
|
41
35
|
const RESULT_ENVELOPE_OUTPUT_CONTRACT: &str =
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
the runtime injects it into the attached Codex leader pane when the leader has run attach-leader. \
|
|
46
|
-
If no leader is attached, the tool returns a fallback/failed result instead of completion. \
|
|
47
|
-
Final completion must call team_orchestrator.report_result exactly once with a short summary \
|
|
48
|
-
and optional status/changes/tests; MCP fills schema_version, task_id, and agent_id.";
|
|
36
|
+
"Final completion must call team_orchestrator.report_result exactly once with a short summary \
|
|
37
|
+
and optional status/changes/tests; the MCP runtime injects the result into the attached leader pane. \
|
|
38
|
+
If no leader is attached, the tool returns a fallback/failed result instead of completion.";
|
|
49
39
|
|
|
50
40
|
pub(crate) struct WorkerCommandAgent {
|
|
51
41
|
id: Option<String>,
|
|
@@ -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
|
}
|