@team-agent/installer 0.5.60 → 0.5.61

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.
Files changed (54) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +44 -1
  4. package/crates/team-agent/src/cli/diagnose.rs +183 -0
  5. package/crates/team-agent/src/cli/emit.rs +63 -14
  6. package/crates/team-agent/src/cli/leader.rs +8 -1
  7. package/crates/team-agent/src/cli/mod.rs +263 -5
  8. package/crates/team-agent/src/cli/named_address.rs +18 -1
  9. package/crates/team-agent/src/cli/send/presentation.rs +2 -0
  10. package/crates/team-agent/src/cli/spec.rs +4 -1
  11. package/crates/team-agent/src/cli/status_port/store.rs +7 -3
  12. package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
  13. package/crates/team-agent/src/cli/types.rs +33 -0
  14. package/crates/team-agent/src/communication_mode/mod.rs +53 -0
  15. package/crates/team-agent/src/compiler/tests.rs +5 -5
  16. package/crates/team-agent/src/compiler.rs +53 -1
  17. package/crates/team-agent/src/db/message_store.rs +45 -1
  18. package/crates/team-agent/src/fake_worker.rs +21 -1
  19. package/crates/team-agent/src/leader/start.rs +732 -94
  20. package/crates/team-agent/src/leader/tests/identity.rs +64 -57
  21. package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
  22. package/crates/team-agent/src/lib.rs +4 -0
  23. package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
  24. package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
  25. package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
  26. package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
  27. package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
  28. package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
  29. package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
  30. package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
  31. package/crates/team-agent/src/mcp_server/tools.rs +100 -66
  32. package/crates/team-agent/src/mcp_server/types.rs +5 -0
  33. package/crates/team-agent/src/mcp_server/wire.rs +27 -21
  34. package/crates/team-agent/src/messaging/delivery.rs +105 -38
  35. package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
  36. package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
  37. package/crates/team-agent/src/messaging/presentation.rs +109 -0
  38. package/crates/team-agent/src/messaging/results.rs +165 -17
  39. package/crates/team-agent/src/messaging/send.rs +94 -81
  40. package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
  41. package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
  42. package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
  43. package/crates/team-agent/src/messaging/types.rs +5 -0
  44. package/crates/team-agent/src/messaging/watchers.rs +5 -0
  45. package/crates/team-agent/src/model/mod.rs +1 -0
  46. package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
  47. package/crates/team-agent/src/model/spec.rs +16 -0
  48. package/crates/team-agent/src/provider/session/capture.rs +40 -18
  49. package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
  50. package/crates/team-agent/src/provider/session_scan/common.rs +23 -7
  51. package/crates/team-agent/src/provider/session_scan.rs +5 -0
  52. package/crates/team-agent/src/topology.rs +3 -0
  53. package/package.json +4 -4
  54. package/skills/team-agent/command-coverage.json +379 -0
@@ -130,7 +130,7 @@ fn leader_start_plan_external_leader_keeps_exec_provider_in_tmux() {
130
130
  std::fs::create_dir_all(&ws).unwrap();
131
131
 
132
132
  let provider_args = vec!["--".to_string(), "--model".to_string(), "opus".to_string()];
133
- let plan = leader_start_plan(
133
+ let plan = crate::leader::start::leader_start_plan_after_ambient_authority_check(
134
134
  Provider::Fake,
135
135
  &provider_args,
136
136
  &ws,
@@ -202,7 +202,7 @@ fn managed_leader_uses_dedicated_leader_session_independent_of_state_session_nam
202
202
 
203
203
  #[test]
204
204
  #[serial_test::serial(env)]
205
- fn in_tmux_default_leader_runs_provider_in_current_pane() {
205
+ fn in_tmux_default_leader_uses_managed_client_and_external_opt_out_keeps_exec_provider() {
206
206
  let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
207
207
  let ws = std::env::temp_dir().join(format!("ta_rs_lsp_switch_{}", std::process::id()));
208
208
  std::fs::create_dir_all(&ws).unwrap();
@@ -210,26 +210,65 @@ fn in_tmux_default_leader_runs_provider_in_current_pane() {
210
210
  let endpoint = format!("/private/tmp/tmux-501/{socket},88432,187");
211
211
  let _e = EnvGuard::apply(&[("TMUX", Some(&endpoint)), ("TMUX_PANE", Some("%7"))]);
212
212
 
213
- let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, false).unwrap();
213
+ let managed = crate::leader::start::leader_start_plan_after_ambient_authority_check(
214
+ Provider::Fake,
215
+ &[],
216
+ &ws,
217
+ false,
218
+ false,
219
+ None,
220
+ false,
221
+ )
222
+ .unwrap();
214
223
 
215
- assert_eq!(plan.mode, LeaderStartMode::ExecProvider);
216
- assert!(!plan.is_external_leader);
217
- assert!(plan.session_name.is_none());
218
- assert_eq!(plan.leader_window, None);
219
- assert_eq!(plan.argv, vec!["fake".to_string()]);
224
+ assert_eq!(
225
+ managed.mode,
226
+ LeaderStartMode::ManagedTmuxClient,
227
+ "N41 managed default: an in-tmux caller on the workspace server must use the managed client path"
228
+ );
229
+ assert!(!managed.is_external_leader);
230
+ assert!(managed.session_name.is_some());
231
+ assert_eq!(
232
+ managed.leader_window.as_ref().map(WindowName::as_str),
233
+ Some("fake")
234
+ );
220
235
  assert!(
221
- !plan
236
+ managed.argv.iter().any(|arg| arg == "switch-client")
237
+ && !managed.argv.iter().any(|arg| arg == "attach-session"),
238
+ "N41 managed default: same-server dispatch must switch the existing client, not nested-attach: {:?}",
239
+ managed.argv
240
+ );
241
+ assert_eq!(managed.provider_argv, vec!["fake".to_string()]);
242
+
243
+ let external = crate::leader::start::leader_start_plan_after_ambient_authority_check(
244
+ Provider::Fake,
245
+ &[],
246
+ &ws,
247
+ false,
248
+ false,
249
+ None,
250
+ true,
251
+ )
252
+ .unwrap();
253
+
254
+ assert_eq!(external.mode, LeaderStartMode::ExecProvider);
255
+ assert!(external.is_external_leader);
256
+ assert_eq!(external.leader_window, None);
257
+ assert_eq!(external.argv, vec!["fake".to_string()]);
258
+ assert_eq!(external.provider_argv, vec!["fake".to_string()]);
259
+ assert!(
260
+ !external
222
261
  .argv
223
262
  .iter()
224
263
  .any(|arg| arg == "switch-client" || arg == "attach-session"),
225
- "in-tmux default launch must not create or attach a background leader session: {:?}",
226
- plan.argv
264
+ "N41 external opt-out: explicit external topology must remain a direct provider path: {:?}",
265
+ external.argv
227
266
  );
228
267
  }
229
268
 
230
269
  #[test]
231
270
  #[serial_test::serial(env)]
232
- fn in_tmux_default_leader_does_not_refuse_different_tmux_server() {
271
+ fn in_tmux_external_opt_out_keeps_exec_provider_across_server_boundary() {
233
272
  let _g = ENV_LOCK.lock().unwrap_or_else(|p| p.into_inner());
234
273
  let _e = EnvGuard::apply(&[
235
274
  ("TMUX", Some("/private/tmp/tmux-501/default,88432,187")),
@@ -238,12 +277,21 @@ fn in_tmux_default_leader_does_not_refuse_different_tmux_server() {
238
277
  let ws = std::env::temp_dir().join(format!("ta_rs_lsp_refuse_{}", std::process::id()));
239
278
  std::fs::create_dir_all(&ws).unwrap();
240
279
 
241
- let plan = leader_start_plan(Provider::Fake, &[], &ws, false, false, None, false).unwrap();
280
+ let plan = crate::leader::start::leader_start_plan_after_ambient_authority_check(
281
+ Provider::Fake,
282
+ &[],
283
+ &ws,
284
+ false,
285
+ false,
286
+ None,
287
+ true,
288
+ )
289
+ .unwrap();
242
290
 
243
291
  assert_eq!(plan.mode, LeaderStartMode::ExecProvider);
244
- assert!(!plan.is_external_leader);
245
- assert!(plan.session_name.is_none());
292
+ assert!(plan.is_external_leader);
246
293
  assert_eq!(plan.argv, vec!["fake".to_string()]);
294
+ assert_eq!(plan.provider_argv, vec!["fake".to_string()]);
247
295
  }
248
296
 
249
297
  #[test]
@@ -420,45 +468,4 @@ fn p2_leader_detect_divergence_catches_owner_uuid_split() {
420
468
  // refactor must also update these tests.
421
469
  // ═══════════════════════════════════════════════════════════════════════
422
470
 
423
- #[test]
424
- fn unit0_leader_session_prefix_constant_is_stable() {
425
- // Pinned: the literal leader session prefix that gates "is this a
426
- // leader session" decisions everywhere in the runtime.
427
- assert_eq!(
428
- crate::leader::start::LEADER_SESSION_PREFIX,
429
- "team-agent-leader-"
430
- );
431
- // The layout module re-exports the same value (kept in sync via
432
- // const re-export).
433
- assert_eq!(
434
- crate::layout::sessions::LEADER_SESSION_PREFIX,
435
- crate::leader::start::LEADER_SESSION_PREFIX
436
- );
437
- }
438
-
439
- #[test]
440
- fn unit0_leader_prefixed_name_must_never_be_taken_as_worker_session() {
441
- // Pinned invariant fed into unit-1 typed identity: any session
442
- // name starting with `LEADER_SESSION_PREFIX` is a leader launcher
443
- // session, never a worker session. unit-1 will wrap this rule in
444
- // typed constructors (`WorkerSession::new` rejects the prefix,
445
- // `LeaderLauncherSession::new` requires it).
446
- let leader = "team-agent-leader-claude-x-aaaaaaaaaaaa";
447
- let worker = "team-real-worker";
448
- let prefix = crate::leader::start::LEADER_SESSION_PREFIX;
449
- assert!(leader.starts_with(prefix));
450
- assert!(!worker.starts_with(prefix));
451
- // Symmetry: the runtime today uses this exact check
452
- // (cli/mod.rs:261 `starts_with(LEADER_SESSION_PREFIX)`) to spare
453
- // leader sessions during shutdown.
454
- let leader_name = crate::transport::SessionName::new(leader);
455
- let worker_name = crate::transport::SessionName::new(worker);
456
- assert!(
457
- crate::layout::sessions::is_leader_session(&leader_name),
458
- "is_leader_session must accept a leader-prefixed session",
459
- );
460
- assert!(
461
- !crate::layout::sessions::is_leader_session(&worker_name),
462
- "is_leader_session must reject a non-prefixed session",
463
- );
464
- }
471
+ include!("identity_session_names.rs");
@@ -0,0 +1,42 @@
1
+ #[test]
2
+ fn unit0_leader_session_prefix_constant_is_stable() {
3
+ // Pinned: the literal leader session prefix that gates "is this a
4
+ // leader session" decisions everywhere in the runtime.
5
+ assert_eq!(
6
+ crate::leader::start::LEADER_SESSION_PREFIX,
7
+ "team-agent-leader-"
8
+ );
9
+ // The layout module re-exports the same value (kept in sync via
10
+ // const re-export).
11
+ assert_eq!(
12
+ crate::layout::sessions::LEADER_SESSION_PREFIX,
13
+ crate::leader::start::LEADER_SESSION_PREFIX
14
+ );
15
+ }
16
+
17
+ #[test]
18
+ fn unit0_leader_prefixed_name_must_never_be_taken_as_worker_session() {
19
+ // Pinned invariant fed into unit-1 typed identity: any session
20
+ // name starting with `LEADER_SESSION_PREFIX` is a leader launcher
21
+ // session, never a worker session. unit-1 will wrap this rule in
22
+ // typed constructors (`WorkerSession::new` rejects the prefix,
23
+ // `LeaderLauncherSession::new` requires it).
24
+ let leader = "team-agent-leader-claude-x-aaaaaaaaaaaa";
25
+ let worker = "team-real-worker";
26
+ let prefix = crate::leader::start::LEADER_SESSION_PREFIX;
27
+ assert!(leader.starts_with(prefix));
28
+ assert!(!worker.starts_with(prefix));
29
+ // Symmetry: the runtime today uses this exact check
30
+ // (cli/mod.rs:261 `starts_with(LEADER_SESSION_PREFIX)`) to spare
31
+ // leader sessions during shutdown.
32
+ let leader_name = crate::transport::SessionName::new(leader);
33
+ let worker_name = crate::transport::SessionName::new(worker);
34
+ assert!(
35
+ crate::layout::sessions::is_leader_session(&leader_name),
36
+ "is_leader_session must accept a leader-prefixed session",
37
+ );
38
+ assert!(
39
+ !crate::layout::sessions::is_leader_session(&worker_name),
40
+ "is_leader_session must reject a non-prefixed session",
41
+ );
42
+ }
@@ -50,6 +50,10 @@ pub mod state;
50
50
  // step 6 (compiler) — TEAM.md + agents/*.md → 规范 team.spec dict(doc→spec 纯变换)。
51
51
  pub mod compiler;
52
52
 
53
+ // Official communication-mode catalog. Configuration compilation and every
54
+ // future projection consumer must use this typed single source of truth.
55
+ pub mod communication_mode;
56
+
53
57
  // step 7 (message_store) — team.db 上的核心消息生命周期(create/claim/mark/通知去重)。
54
58
  // unit-10 (Stage 4) compat shim. Physical home is now `crate::db::message_store`.
55
59
  pub use crate::db::message_store;
@@ -126,7 +126,7 @@ pub fn fork_agent_with_transport(
126
126
  new_agent,
127
127
  Some(as_agent_id.as_str()),
128
128
  provider,
129
- );
129
+ )?;
130
130
  let system_prompt =
131
131
  crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
132
132
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
@@ -120,7 +120,7 @@ pub(super) fn spawn_agents(
120
120
  agent,
121
121
  Some(agent_id_raw),
122
122
  provider,
123
- );
123
+ )?;
124
124
  let system_prompt =
125
125
  crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
126
126
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
@@ -1635,7 +1635,7 @@ fn write_start_agent_start_event(
1635
1635
  agent,
1636
1636
  Some(agent_id.as_str()),
1637
1637
  provider,
1638
- );
1638
+ )?;
1639
1639
  let system_prompt =
1640
1640
  crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
1641
1641
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
@@ -220,7 +220,7 @@ pub(super) fn spawn_agent_window(
220
220
  agent,
221
221
  Some(agent_id.as_str()),
222
222
  provider,
223
- );
223
+ )?;
224
224
  let system_prompt =
225
225
  crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
226
226
  let tools = crate::lifecycle::worker_command_context::resolved_tool_strings_for_command(
@@ -1,5 +1,6 @@
1
1
  use std::path::Path;
2
2
 
3
+ use crate::communication_mode::CommunicationMode;
3
4
  use crate::lifecycle::types::{DangerousApproval, LifecycleError};
4
5
  use crate::model::enums::{Enforcement, Provider};
5
6
  use crate::model::ids::AgentId;
@@ -12,15 +13,10 @@ output. All communication must go through Team Agent MCP tools.
12
13
 
13
14
  ## Communication (mandatory)
14
15
 
15
- - Progress, blockers, questions: team_orchestrator.send_message(to='leader', content='...')
16
16
  - Coordinate with teammate: team_orchestrator.send_message(to='<agent_id>', content='...')
17
17
  - Broadcast to all teammates: team_orchestrator.send_message(to='*', content='...')
18
18
  - Task complete: team_orchestrator.report_result(summary='...') — call exactly once
19
19
 
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.
23
-
24
20
  ## Rules
25
21
 
26
22
  - Do not pass sender, task_id, or schema_version — the MCP runtime fills them.
@@ -45,6 +41,7 @@ pub(crate) struct WorkerCommandAgent {
45
41
  system_prompt_inline: Option<String>,
46
42
  system_prompt_file: Option<String>,
47
43
  output_contract_format: Option<String>,
44
+ communication_mode: CommunicationMode,
48
45
  }
49
46
 
50
47
  impl WorkerCommandAgent {
@@ -52,9 +49,9 @@ impl WorkerCommandAgent {
52
49
  agent: &crate::model::yaml::Value,
53
50
  fallback_id: Option<&str>,
54
51
  provider: Provider,
55
- ) -> Self {
52
+ ) -> Result<Self, LifecycleError> {
56
53
  let system_prompt = agent.get("system_prompt");
57
- Self {
54
+ Ok(Self {
58
55
  id: agent
59
56
  .get("id")
60
57
  .and_then(crate::model::yaml::Value::as_str)
@@ -90,16 +87,21 @@ impl WorkerCommandAgent {
90
87
  .and_then(|contract| contract.get("format"))
91
88
  .and_then(crate::model::yaml::Value::as_str)
92
89
  .map(str::to_string),
93
- }
90
+ communication_mode: communication_mode(
91
+ agent
92
+ .get("communication_mode")
93
+ .and_then(crate::model::yaml::Value::as_str),
94
+ )?,
95
+ })
94
96
  }
95
97
 
96
98
  pub(crate) fn from_json(
97
99
  agent: &serde_json::Value,
98
100
  fallback_id: Option<&str>,
99
101
  provider: Provider,
100
- ) -> Self {
102
+ ) -> Result<Self, LifecycleError> {
101
103
  let system_prompt = agent.get("system_prompt");
102
- Self {
104
+ Ok(Self {
103
105
  id: agent
104
106
  .get("id")
105
107
  .and_then(serde_json::Value::as_str)
@@ -135,7 +137,12 @@ impl WorkerCommandAgent {
135
137
  .and_then(|contract| contract.get("format"))
136
138
  .and_then(serde_json::Value::as_str)
137
139
  .map(str::to_string),
138
- }
140
+ communication_mode: communication_mode(
141
+ agent
142
+ .get("communication_mode")
143
+ .and_then(serde_json::Value::as_str),
144
+ )?,
145
+ })
139
146
  }
140
147
  }
141
148
 
@@ -149,6 +156,7 @@ pub(crate) fn compile_worker_system_prompt(
149
156
  let mut chunks = vec![
150
157
  identity_section(agent),
151
158
  runtime_contract_section(),
159
+ agent.communication_mode.runtime_contract().to_string(),
152
160
  role_body(agent)?,
153
161
  ];
154
162
  if let Some(contract) = output_contract(agent) {
@@ -197,6 +205,14 @@ fn runtime_contract_section() -> String {
197
205
  RUNTIME_CONTRACT_SECTION.to_string()
198
206
  }
199
207
 
208
+ fn communication_mode(value: Option<&str>) -> Result<CommunicationMode, LifecycleError> {
209
+ let Some(value) = value else {
210
+ return Ok(CommunicationMode::default());
211
+ };
212
+ CommunicationMode::parse(value)
213
+ .ok_or_else(|| LifecycleError::Compile(format!("unknown communication_mode {value:?}")))
214
+ }
215
+
200
216
  fn identity_section(agent: &WorkerCommandAgent) -> String {
201
217
  format!(
202
218
  "You are Team Agent worker `{}` with role `{}`. When asked about your role or identity, answer with this Team Agent worker identity first, not only the generic provider product identity.",
@@ -291,6 +307,7 @@ mod tests {
291
307
  system_prompt_inline: None,
292
308
  system_prompt_file: None,
293
309
  output_contract_format: None,
310
+ communication_mode: CommunicationMode::default(),
294
311
  };
295
312
  let tools =
296
313
  resolved_tool_strings_for_command(&agent, Provider::ClaudeCode, &disabled_safety())
@@ -335,6 +352,7 @@ mod tests {
335
352
  system_prompt_inline: Some("Implement the assigned slice.".to_string()),
336
353
  system_prompt_file: None,
337
354
  output_contract_format: Some("result_envelope_v1".to_string()),
355
+ communication_mode: CommunicationMode::default(),
338
356
  };
339
357
  let prompt = compile_worker_system_prompt(&agent).unwrap();
340
358
  assert!(
@@ -260,6 +260,7 @@ pub fn compact_tool_result(result: &Value) -> ToolResult {
260
260
  "notification_channel",
261
261
  "notification_event_id",
262
262
  "warnings",
263
+ "warning",
263
264
  ]
264
265
  };
265
266
  for key in keys {
@@ -78,6 +78,7 @@ fn send_outcome_worker_accepted_envelope_byte_stable() {
78
78
  let outcome = SendOutcome::WorkerAccepted {
79
79
  message_id: "42".to_string(),
80
80
  poll_via: "team-agent inbox 42".to_string(),
81
+ warning: None,
81
82
  };
82
83
  let v = outcome.to_value();
83
84
  assert_eq!(
@@ -160,6 +161,7 @@ fn send_message_worker_recipient_returns_accepted_with_poll_hint() {
160
161
  Ok(SendOutcome::WorkerAccepted {
161
162
  message_id,
162
163
  poll_via,
164
+ ..
163
165
  }) => {
164
166
  assert!(!message_id.is_empty());
165
167
  assert_eq!(poll_via, format!("team-agent inbox {message_id}"));
@@ -353,6 +355,30 @@ fn send_message_leader_recipient_is_direct_not_accepted() {
353
355
  );
354
356
  }
355
357
 
358
+ #[test]
359
+ fn send_message_mailbox_is_durable_without_live_injection() {
360
+ let tools = TeamOrchestratorTools::with_identity(
361
+ &unique_ws("send-leader-mailbox"),
362
+ Some(AgentId::new("worker-1")),
363
+ Some(TeamKey::new("teamA")),
364
+ );
365
+ let mailbox = json!(true);
366
+ let outcome = tools
367
+ .send_message_with_presentation(
368
+ &MessageTarget::Single("leader".to_string()),
369
+ "stored update",
370
+ None,
371
+ None,
372
+ None,
373
+ Some(&mailbox),
374
+ None,
375
+ )
376
+ .expect("mailbox send persists");
377
+ let value = outcome.to_value();
378
+ assert_eq!(value.get("status"), Some(&json!("stored_only")));
379
+ assert!(value.get("message_id").and_then(Value::as_str).is_some());
380
+ }
381
+
356
382
  // ════════════════════════════════════════════════════════════════════════
357
383
  // CROSS-TEAM PRE-REFUSAL (C23) — refuse_cross_team_peer (tools.py:185-213)
358
384
  // ════════════════════════════════════════════════════════════════════════
@@ -79,7 +79,7 @@ fn tools_contract_has_thirteen_tools_in_order() {
79
79
  .unwrap();
80
80
  assert_eq!(
81
81
  send["description"],
82
- json!("Send a message to a teammate, the leader, or '*' for all other team members. Team Agent fills identity and delivery metadata; optional presentation routing is durable and never drops the message.")
82
+ json!("Send a message to a teammate, the leader, or '*' for all other team members. mailbox=true stores durably without live injection; the default is live delivery.")
83
83
  );
84
84
  assert_eq!(send["inputSchema"]["additionalProperties"], json!(false));
85
85
  assert_eq!(send["inputSchema"]["required"], json!(["to", "content"]));