@team-agent/installer 0.3.0 → 0.3.2

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 (39) hide show
  1. package/Cargo.lock +1 -1
  2. package/Cargo.toml +1 -1
  3. package/crates/team-agent/src/cli/adapters.rs +38 -7
  4. package/crates/team-agent/src/cli/emit.rs +182 -54
  5. package/crates/team-agent/src/cli/mod.rs +703 -35
  6. package/crates/team-agent/src/cli/status_port.rs +170 -44
  7. package/crates/team-agent/src/cli/tests/run_delegation.rs +2 -0
  8. package/crates/team-agent/src/cli/types.rs +1 -0
  9. package/crates/team-agent/src/coordinator/health.rs +130 -0
  10. package/crates/team-agent/src/leader/lease.rs +23 -2
  11. package/crates/team-agent/src/leader/rediscover/tests.rs +1 -0
  12. package/crates/team-agent/src/leader/rediscover.rs +2 -0
  13. package/crates/team-agent/src/leader/tests/byte_findings.rs +9 -6
  14. package/crates/team-agent/src/leader/tests/idle.rs +1 -0
  15. package/crates/team-agent/src/leader/tests/lease_claim.rs +157 -0
  16. package/crates/team-agent/src/leader/types.rs +2 -0
  17. package/crates/team-agent/src/lifecycle/launch.rs +554 -65
  18. package/crates/team-agent/src/lifecycle/restart/common.rs +65 -0
  19. package/crates/team-agent/src/lifecycle/restart/rebuild.rs +57 -15
  20. package/crates/team-agent/src/lifecycle/restart/remove.rs +5 -1
  21. package/crates/team-agent/src/lifecycle/restart.rs +20 -0
  22. package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +52 -0
  23. package/crates/team-agent/src/lifecycle/types.rs +25 -0
  24. package/crates/team-agent/src/mcp_server/tests/wire.rs +28 -0
  25. package/crates/team-agent/src/mcp_server/wire.rs +81 -1
  26. package/crates/team-agent/src/messaging/delivery.rs +574 -12
  27. package/crates/team-agent/src/messaging/leader_receiver.rs +26 -37
  28. package/crates/team-agent/src/messaging/mod.rs +1 -1
  29. package/crates/team-agent/src/messaging/results.rs +218 -49
  30. package/crates/team-agent/src/messaging/send.rs +15 -19
  31. package/crates/team-agent/src/provider/adapter.rs +95 -10
  32. package/crates/team-agent/src/provider/helpers.rs +10 -1
  33. package/crates/team-agent/src/state/identity.rs +3 -0
  34. package/crates/team-agent/src/state/persist.rs +113 -1
  35. package/crates/team-agent/src/state/projection.rs +127 -3
  36. package/crates/team-agent/src/tmux_backend/tests.rs +179 -0
  37. package/crates/team-agent/src/tmux_backend.rs +124 -12
  38. package/npm/install.mjs +29 -7
  39. package/package.json +4 -4
@@ -160,6 +160,71 @@ pub(super) fn agent_rollout_path(agent: &serde_json::Value) -> Option<RolloutPat
160
160
  .map(RolloutPath::new)
161
161
  }
162
162
 
163
+ pub(crate) fn refresh_missing_provider_sessions(
164
+ state: &mut serde_json::Value,
165
+ ) -> Result<bool, LifecycleError> {
166
+ let Some(agents) = state.get_mut("agents").and_then(serde_json::Value::as_object_mut) else {
167
+ return Ok(false);
168
+ };
169
+ let mut changed = false;
170
+ for (agent_id, agent) in agents {
171
+ let Some(agent_obj) = agent.as_object_mut() else {
172
+ continue;
173
+ };
174
+ if agent_obj
175
+ .get("session_id")
176
+ .and_then(serde_json::Value::as_str)
177
+ .is_some_and(|session| !session.is_empty())
178
+ {
179
+ continue;
180
+ }
181
+ let Some(spawn_cwd) = agent_obj
182
+ .get("spawn_cwd")
183
+ .and_then(serde_json::Value::as_str)
184
+ .filter(|cwd| !cwd.is_empty())
185
+ else {
186
+ continue;
187
+ };
188
+ let provider = agent_provider(&serde_json::Value::Object(agent_obj.clone()));
189
+ let adapter = crate::provider::get_adapter(provider);
190
+ let captured = adapter
191
+ .capture_session_id(agent_id, Path::new(spawn_cwd), 0)
192
+ .map_err(|e| LifecycleError::Provider(e.to_string()))?;
193
+ let Some(captured) = captured else {
194
+ continue;
195
+ };
196
+ if let Some(session_id) = captured.session_id {
197
+ agent_obj.insert(
198
+ "session_id".to_string(),
199
+ serde_json::json!(session_id.as_str()),
200
+ );
201
+ changed = true;
202
+ }
203
+ if let Some(rollout_path) = captured.rollout_path {
204
+ agent_obj.insert(
205
+ "rollout_path".to_string(),
206
+ serde_json::json!(rollout_path.as_path().to_string_lossy()),
207
+ );
208
+ changed = true;
209
+ }
210
+ agent_obj.insert(
211
+ "captured_at".to_string(),
212
+ serde_json::json!(chrono::Utc::now().to_rfc3339()),
213
+ );
214
+ agent_obj.insert(
215
+ "captured_via".to_string(),
216
+ serde_json::to_value(captured.captured_via)
217
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
218
+ );
219
+ agent_obj.insert(
220
+ "attribution_confidence".to_string(),
221
+ serde_json::to_value(captured.attribution_confidence)
222
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?,
223
+ );
224
+ }
225
+ Ok(changed)
226
+ }
227
+
163
228
  /// Tools list off an agent's runtime state entry (`tools: [...]`). Restart paths
164
229
  /// don't have the full spec object, only the runtime state — so they read tools from
165
230
  /// the state row, falling back to an empty list. Contract C requires the worker
@@ -31,6 +31,12 @@ pub fn restart_with_transport(
31
31
  team: Option<&str>,
32
32
  transport: &dyn crate::transport::Transport,
33
33
  ) -> Result<RestartReport, LifecycleError> {
34
+ if crate::lifecycle::restart::input_has_no_local_team_context(workspace) {
35
+ return Err(LifecycleError::TeamSelect(format!(
36
+ "missing spec for restart: {}",
37
+ workspace.join("team.spec.yaml").display()
38
+ )));
39
+ }
34
40
  let run_candidate = crate::model::paths::canonical_run_workspace(workspace)
35
41
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
36
42
  if !workspace.join("team.spec.yaml").exists()
@@ -47,7 +53,7 @@ pub fn restart_with_transport(
47
53
  crate::state::selector::SelectorMode::RequireSpec,
48
54
  )
49
55
  .map_err(|e| LifecycleError::TeamSelect(e.to_string()))?;
50
- let state = selected.state;
56
+ let mut state = selected.state;
51
57
  crate::lifecycle::launch::ensure_owner_allowed_for_state(&state, None)?;
52
58
  let spec_workspace = selected
53
59
  .spec_workspace
@@ -55,6 +61,10 @@ pub fn restart_with_transport(
55
61
  .ok_or_else(|| LifecycleError::TeamSelect("active team spec workspace not found".to_string()))?;
56
62
  let spec = load_team_spec(spec_workspace)?;
57
63
  let safety = crate::lifecycle::launch::effective_runtime_config(&spec)?;
64
+ if refresh_missing_provider_sessions(&mut state)? {
65
+ crate::state::projection::save_team_scoped_state(&selected.run_workspace, &state)
66
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
67
+ }
58
68
  let plan = classify_restart_plan(&state, allow_fresh)?;
59
69
  write_restart_resume_decision_events(&selected.run_workspace, &state, allow_fresh, &plan.decisions)?;
60
70
  if !plan.corrupt_entries.is_empty() {
@@ -117,7 +127,6 @@ fn write_restart_resume_decision_events(
117
127
  allow_fresh: bool,
118
128
  decisions: &[RestartedAgent],
119
129
  ) -> Result<(), LifecycleError> {
120
- let log = crate::event_log::EventLog::new(workspace);
121
130
  for decision in decisions {
122
131
  let agent = state
123
132
  .get("agents")
@@ -134,23 +143,56 @@ fn write_restart_resume_decision_events(
134
143
  ResumeDecision::FreshStart => "fresh_start",
135
144
  ResumeDecision::Refuse => "refuse",
136
145
  };
137
- log.write(
138
- crate::lifecycle::types::event_names::RESTART_RESUME_DECISION,
139
- serde_json::json!({
140
- "worker_id": decision.agent_id.as_str(),
141
- "has_first_send_at": first_send_at.is_some(),
142
- "has_session_id": session_id.is_some(),
143
- "allow_fresh": allow_fresh,
144
- "decision": decision_wire,
145
- "first_send_at": first_send_at,
146
- "session_id": session_id,
147
- }),
148
- )
149
- .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
146
+ write_restart_resume_decision_event(
147
+ workspace,
148
+ decision.agent_id.as_str(),
149
+ first_send_at,
150
+ session_id,
151
+ allow_fresh,
152
+ decision_wire,
153
+ )?;
150
154
  }
151
155
  Ok(())
152
156
  }
153
157
 
158
+ fn write_restart_resume_decision_event(
159
+ workspace: &Path,
160
+ worker_id: &str,
161
+ first_send_at: Option<String>,
162
+ session_id: Option<String>,
163
+ allow_fresh: bool,
164
+ decision: &str,
165
+ ) -> Result<(), LifecycleError> {
166
+ use std::io::Write as _;
167
+
168
+ let path = workspace.join(".team").join("logs").join("events.jsonl");
169
+ if let Some(parent) = path.parent() {
170
+ std::fs::create_dir_all(parent)
171
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
172
+ }
173
+ let event = serde_json::json!({
174
+ "ts": chrono::Utc::now().to_rfc3339(),
175
+ "event": crate::lifecycle::types::event_names::RESTART_RESUME_DECISION,
176
+ "worker_id": worker_id,
177
+ "has_first_send_at": first_send_at.is_some(),
178
+ "has_session_id": session_id.is_some(),
179
+ "allow_fresh": allow_fresh,
180
+ "decision": decision,
181
+ "first_send_at": first_send_at,
182
+ "session_id": session_id,
183
+ });
184
+ let line = serde_json::to_string(&event)
185
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
186
+ let mut file = std::fs::OpenOptions::new()
187
+ .create(true)
188
+ .append(true)
189
+ .open(&path)
190
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
191
+ file.write_all(line.as_bytes())
192
+ .and_then(|_| file.write_all(b"\n"))
193
+ .map_err(|e| LifecycleError::StatePersist(e.to_string()))
194
+ }
195
+
154
196
  /// `restart_candidates(workspace)`(`restart/selection.py:12`)。从 snapshot + active
155
197
  /// state 收集可重启 team。
156
198
  pub fn restart_candidates(workspace: &Path) -> Result<Vec<RestartCandidate>, LifecycleError> {
@@ -169,7 +169,11 @@ fn remove_agent_inner(
169
169
  // (team projection) — NOT a raw save, so other teams in a multi-team workspace are preserved.
170
170
  let mut removed_state = working_state;
171
171
  remove_agent_from_state(&mut removed_state, agent_id)?;
172
- crate::state::projection::save_team_scoped_state(paths.run_workspace, &removed_state)
172
+ crate::state::projection::save_team_scoped_state_with_deleted_agents(
173
+ paths.run_workspace,
174
+ &removed_state,
175
+ &[agent_id.as_str()],
176
+ )
173
177
  .map_err(|e| LifecycleError::StatePersist(e.to_string()))?;
174
178
  cleared_locations.push(serde_json::json!("state.json:agents"));
175
179
  write_remove_step_event(
@@ -33,6 +33,7 @@ mod team_state;
33
33
 
34
34
  pub use agent::{reset_agent, reset_agent_with_transport, start_agent, start_agent_with_transport, stop_agent, stop_agent_with_transport};
35
35
  pub(crate) use agent::start_agent_at_paths;
36
+ pub(crate) use common::refresh_missing_provider_sessions;
36
37
  pub use orchestrator::{halt_plan, plan_status};
37
38
  pub use rebuild::{restart, restart_candidates, restart_with_transport, select_restart_state};
38
39
  pub use remove::{remove_agent, remove_agent_with_transport};
@@ -45,6 +46,13 @@ pub(crate) fn lifecycle_run_workspace(workspace: &Path) -> Result<std::path::Pat
45
46
  }
46
47
 
47
48
  fn lifecycle_paths(workspace: &Path, team: Option<&str>) -> Result<LifecyclePaths, LifecycleError> {
49
+ if input_has_no_local_team_context(workspace) {
50
+ return Err(LifecycleError::TeamSelect(format!(
51
+ "active team spec not found: input_workspace={} expected_spec_path={}",
52
+ workspace.display(),
53
+ workspace.join("team.spec.yaml").display()
54
+ )));
55
+ }
48
56
  let selected = crate::state::selector::resolve_active_team(
49
57
  workspace,
50
58
  team,
@@ -60,6 +68,18 @@ fn lifecycle_paths(workspace: &Path, team: Option<&str>) -> Result<LifecyclePath
60
68
  })
61
69
  }
62
70
 
71
+ pub(crate) fn input_has_no_local_team_context(workspace: &Path) -> bool {
72
+ !workspace.join("team.spec.yaml").exists()
73
+ && !workspace.join(".team").exists()
74
+ && !crate::state::persist::runtime_state_path(workspace).exists()
75
+ && workspace.file_name().and_then(|s| s.to_str()) != Some(".team")
76
+ && workspace
77
+ .parent()
78
+ .and_then(|p| p.file_name())
79
+ .and_then(|s| s.to_str())
80
+ != Some(".team")
81
+ }
82
+
63
83
  fn selected_state_spec_workspace(state: &serde_json::Value) -> Option<std::path::PathBuf> {
64
84
  state
65
85
  .get("spec_path")
@@ -67,6 +67,58 @@ fn quick_start_compiles_real_spec_to_team_spec_yaml() {
67
67
  }
68
68
  }
69
69
 
70
+ #[test]
71
+ fn quick_start_teamdir_under_dot_team_uses_project_workspace_for_status_and_collect() {
72
+ let workspace = temp_ws();
73
+ let team = workspace.join(".team").join("current");
74
+ std::fs::create_dir_all(team.join("agents")).unwrap();
75
+ std::fs::write(team.join("TEAM.md"), QS_TEAM_MD).unwrap();
76
+ std::fs::write(team.join("agents").join("implementer.md"), QS_VALID_ROLE).unwrap();
77
+
78
+ let transport = OfflineTransport::new();
79
+ let result = quick_start_with_transport(&team, None, true, true, None, &transport);
80
+ assert!(matches!(result, Ok(QuickStartReport::Ready { .. })), "quick-start failed: {result:?}");
81
+
82
+ let state_path = crate::state::persist::runtime_state_path(&workspace);
83
+ assert!(state_path.exists(), "quick-start .team/current must persist runtime state under project root");
84
+ assert!(
85
+ !workspace.join(".team").join(".team").join("runtime").join("state.json").exists(),
86
+ "quick-start .team/current must not create nested .team/.team runtime state"
87
+ );
88
+
89
+ for input in [&workspace, &team] {
90
+ let selected = crate::state::selector::resolve_active_team(
91
+ input,
92
+ None,
93
+ crate::state::selector::SelectorMode::RuntimeOnly,
94
+ )
95
+ .expect("status/collect selector should resolve project root");
96
+ assert_eq!(selected.run_workspace, workspace, "input={}", input.display());
97
+ assert_eq!(
98
+ selected.spec_path.as_deref().map(std::fs::canonicalize).transpose().unwrap(),
99
+ Some(std::fs::canonicalize(team.join("team.spec.yaml")).unwrap()),
100
+ "input={}",
101
+ input.display()
102
+ );
103
+ }
104
+
105
+ let status = crate::cli::cmd_status(&crate::cli::StatusArgs {
106
+ agent: None,
107
+ workspace: team.clone(),
108
+ detail: false,
109
+ summary: false,
110
+ json: true,
111
+ });
112
+ assert!(status.is_ok(), "status should normalize teamdir to project-root runtime state: {status:?}");
113
+
114
+ let collect = crate::cli::cmd_collect(&crate::cli::CollectArgs {
115
+ result_file: None,
116
+ workspace: team.clone(),
117
+ json: true,
118
+ });
119
+ assert!(collect.is_ok(), "collect should normalize teamdir to the same project-root state/spec: {collect:?}");
120
+ }
121
+
70
122
  // P0 — quick_start over an INVALID role doc (missing `provider`) must surface the REAL compile
71
123
  // error, distinct from the stub's hardcoded "no role docs found". Golden: compile_team raises
72
124
  // "missing front matter field provider" (compiler.py:_validate_role_doc), before preflight.
@@ -445,6 +445,24 @@ pub struct PermissionSummary {
445
445
  pub raw: serde_json::Value,
446
446
  }
447
447
 
448
+ /// BUG-7 (0.3.1): quick-start cannot honestly report "ready" before the workers'
449
+ /// MCP tool sets have actually loaded — provider-side schema rejections (codex
450
+ /// invalid_function_parameters etc.) happen AFTER spawn and silently disable the
451
+ /// worker. The report must therefore carry a readiness verdict so the CLI surface
452
+ /// never emits bare "ready" while worker capability is unverified or already known
453
+ /// to be degraded.
454
+ #[derive(Debug, Clone, PartialEq, Eq)]
455
+ pub enum QuickStartReadiness {
456
+ /// At least one agent already failed to materialize a live tmux window (BUG-2
457
+ /// observable). The team is *not* ready; user must inspect / restart.
458
+ Degraded { unhealthy_agents: Vec<String> },
459
+ /// All spawned agents have live windows but their MCP tool set load has NOT
460
+ /// been verified yet — provider-side schema/auth failures could still leave
461
+ /// the worker unable to call team_orchestrator tools. CLI must label this
462
+ /// `pending` / `unverified`, NOT bare `ready`.
463
+ PendingToolLoad,
464
+ }
465
+
448
466
  /// `quick_start(...)` 报告(`diagnose/quick_start.py:103` typed 版)。`Refused` 区分
449
467
  /// existing-context(需 restart 或 --fresh)与 preflight 失败。
450
468
  #[derive(Debug, Clone, PartialEq, Eq)]
@@ -454,6 +472,13 @@ pub enum QuickStartReport {
454
472
  session_name: SessionName,
455
473
  launch: Box<LaunchReport>,
456
474
  next_actions: Vec<String>,
475
+ /// BUG-7: real readiness verdict. `Ready` ⇒ the wrapper completed AND the
476
+ /// caller already verified tool-set availability; the framework itself
477
+ /// never emits this without an external observable confirming worker
478
+ /// tool calls succeeded. quick_start_with_transport always defaults to
479
+ /// [`QuickStartReadiness::PendingToolLoad`] (or `Degraded` if any agent
480
+ /// failed to spawn) so the CLI surface cannot lie about availability.
481
+ worker_readiness: QuickStartReadiness,
457
482
  },
458
483
  /// 已有 runtime state,非 --fresh → 引导用 restart(`quick_start.py:42`)。
459
484
  ExistingRuntime {
@@ -68,6 +68,34 @@
68
68
  assert_eq!(send["inputSchema"]["required"], json!(["to", "content"]));
69
69
  }
70
70
 
71
+ #[test]
72
+ fn tools_contract_input_schemas_are_openai_strict_top_level_objects() {
73
+ let forbidden = ["oneOf", "anyOf", "allOf", "enum", "not"];
74
+ for tool in tools_contract() {
75
+ let schema = tool["inputSchema"].as_object().unwrap();
76
+ assert_eq!(schema.get("type"), Some(&json!("object")), "schema must be a top-level object: {tool}");
77
+ for key in forbidden {
78
+ assert!(
79
+ !schema.contains_key(key),
80
+ "OpenAI rejects top-level `{key}` in MCP tool schema: {tool}"
81
+ );
82
+ }
83
+ let properties = schema
84
+ .get("properties")
85
+ .and_then(Value::as_object)
86
+ .unwrap_or_else(|| panic!("schema properties must be an object: {tool}"));
87
+ for required in schema.get("required").and_then(Value::as_array).into_iter().flatten() {
88
+ let Some(name) = required.as_str() else {
89
+ panic!("required entries must be strings: {tool}");
90
+ };
91
+ assert!(
92
+ properties.contains_key(name),
93
+ "required property `{name}` must be declared in properties: {tool}"
94
+ );
95
+ }
96
+ }
97
+ }
98
+
71
99
  // ════════════════════════════════════════════════════════════════════════
72
100
  // handle_mcp — JSON-RPC routing (server.py:46-91)
73
101
  // ════════════════════════════════════════════════════════════════════════
@@ -298,13 +298,93 @@ fn tool_contract(tool: McpTool) -> Value {
298
298
  "description": description,
299
299
  "inputSchema": {
300
300
  "type": "object",
301
- "properties": {},
301
+ "properties": tool_properties(tool),
302
302
  "required": required,
303
303
  "additionalProperties": false
304
304
  }
305
305
  })
306
306
  }
307
307
 
308
+ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
309
+ let mut properties = serde_json::Map::new();
310
+ match tool {
311
+ McpTool::AssignTask => {
312
+ insert_property(&mut properties, "task", object_property("Task object to add or update."));
313
+ insert_property(&mut properties, "message", string_property("Optional message to deliver with the task."));
314
+ }
315
+ McpTool::SendMessage => {
316
+ insert_property(&mut properties, "to", string_property("Target agent id, 'leader', or '*' for broadcast."));
317
+ insert_property(&mut properties, "content", string_property("Message body."));
318
+ insert_property(&mut properties, "task_id", string_property("Optional task id to associate with the message."));
319
+ insert_property(&mut properties, "sender", string_property("Optional sender override."));
320
+ insert_property(&mut properties, "requires_ack", boolean_property("Whether the recipient should acknowledge delivery."));
321
+ insert_property(&mut properties, "scope", string_property("Optional delivery scope: team or workspace."));
322
+ }
323
+ McpTool::ReportResult => {
324
+ insert_property(&mut properties, "envelope", object_property("Optional full result envelope."));
325
+ insert_property(&mut properties, "summary", string_property("Short result summary."));
326
+ insert_property(&mut properties, "status", string_property("Result status."));
327
+ insert_property(&mut properties, "changes", array_property("Changed files or artifacts."));
328
+ insert_property(&mut properties, "tests", array_property("Tests or checks performed."));
329
+ insert_property(&mut properties, "risks", array_property("Risks or blockers."));
330
+ insert_property(&mut properties, "artifacts", array_property("Artifact references."));
331
+ insert_property(&mut properties, "next_actions", array_property("Suggested next actions."));
332
+ insert_property(&mut properties, "task_id", string_property("Optional task id override."));
333
+ insert_property(&mut properties, "agent_id", string_property("Optional reporting agent id override."));
334
+ }
335
+ McpTool::UpdateState => {
336
+ insert_property(&mut properties, "note", string_property("Note to append to team state."));
337
+ }
338
+ McpTool::GetTeamStatus | McpTool::StuckList => {}
339
+ McpTool::StopAgent => {
340
+ insert_property(&mut properties, "agent_id", string_property("Agent id to stop."));
341
+ }
342
+ McpTool::ResetAgent => {
343
+ insert_property(&mut properties, "agent_id", string_property("Agent id to reset."));
344
+ insert_property(&mut properties, "discard_session", boolean_property("Whether to discard the existing provider session."));
345
+ }
346
+ McpTool::AddAgent => {
347
+ insert_property(&mut properties, "new_agent_id", string_property("New agent id."));
348
+ insert_property(&mut properties, "role_file_path", string_property("Workspace-relative role file path."));
349
+ }
350
+ McpTool::ForkAgent => {
351
+ insert_property(&mut properties, "source_agent_id", string_property("Agent id to fork from."));
352
+ insert_property(&mut properties, "as_agent_id", string_property("Agent id for the forked worker."));
353
+ insert_property(&mut properties, "label", string_property("Optional display label."));
354
+ }
355
+ McpTool::RequestHuman => {
356
+ insert_property(&mut properties, "question", string_property("Question to ask the human."));
357
+ insert_property(&mut properties, "task_id", string_property("Optional related task id."));
358
+ insert_property(&mut properties, "agent_id", string_property("Optional requesting agent id."));
359
+ }
360
+ McpTool::StuckCancel => {
361
+ insert_property(&mut properties, "agent_id", string_property("Agent id whose stuck alerts should be suppressed."));
362
+ insert_property(&mut properties, "alert_type", string_property("Alert type to suppress, or all."));
363
+ }
364
+ }
365
+ properties
366
+ }
367
+
368
+ fn insert_property(properties: &mut serde_json::Map<String, Value>, name: &str, schema: Value) {
369
+ properties.insert(name.to_string(), schema);
370
+ }
371
+
372
+ fn string_property(description: &str) -> Value {
373
+ serde_json::json!({"type": "string", "description": description})
374
+ }
375
+
376
+ fn boolean_property(description: &str) -> Value {
377
+ serde_json::json!({"type": "boolean", "description": description})
378
+ }
379
+
380
+ fn object_property(description: &str) -> Value {
381
+ serde_json::json!({"type": "object", "description": description, "additionalProperties": true})
382
+ }
383
+
384
+ fn array_property(description: &str) -> Value {
385
+ serde_json::json!({"type": "array", "description": description, "items": {"type": "object", "additionalProperties": true}})
386
+ }
387
+
308
388
  pub(crate) fn dispatch_tool(tools: &TeamOrchestratorTools, tool: McpTool, args: &Value) -> ToolResult {
309
389
  match tool {
310
390
  McpTool::AssignTask => tools.assign_task(args.get("task").unwrap_or(args), args.get("message").and_then(Value::as_str)),