@team-agent/installer 0.3.14 → 0.3.15
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/emit.rs +92 -11
- package/crates/team-agent/src/cli/mod.rs +60 -10
- package/crates/team-agent/src/cli/send.rs +252 -0
- package/crates/team-agent/src/cli/tests/run_delegation.rs +15 -3
- package/crates/team-agent/src/cli/tests/shutdown_kill_plan.rs +159 -2
- package/crates/team-agent/src/cli/types.rs +30 -1
- package/crates/team-agent/src/compiler/tests.rs +2 -2
- package/crates/team-agent/src/compiler.rs +1 -1
- package/crates/team-agent/src/fake_worker.rs +32 -145
- package/crates/team-agent/src/lifecycle/display.rs +3 -3
- package/crates/team-agent/src/lifecycle/launch.rs +560 -69
- package/crates/team-agent/src/lifecycle/restart/agent.rs +137 -17
- package/crates/team-agent/src/lifecycle/restart/common.rs +38 -2
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +72 -1
- package/crates/team-agent/src/lifecycle/tests/core.rs +14 -5
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +668 -8
- package/crates/team-agent/src/lifecycle/types.rs +5 -0
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +8 -0
- package/crates/team-agent/src/mcp_server/wire.rs +153 -3
- package/crates/team-agent/src/messaging/leader_receiver.rs +276 -43
- package/crates/team-agent/src/messaging/mod.rs +6 -3
- package/crates/team-agent/src/messaging/results.rs +240 -158
- package/crates/team-agent/src/messaging/send.rs +3 -2
- package/crates/team-agent/src/messaging/tests/e23.rs +35 -0
- package/crates/team-agent/src/messaging/tests/mod.rs +6 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +9 -9
- package/crates/team-agent/src/os_probe.rs +11 -0
- package/crates/team-agent/src/state/persist.rs +6 -0
- package/crates/team-agent/src/tmux_backend.rs +85 -0
- package/crates/team-agent/src/transport/test_support.rs +46 -1
- package/crates/team-agent/src/transport.rs +27 -0
- package/package.json +4 -4
- package/skills/team-agent/references/recovery-runbook.md +277 -0
|
@@ -158,11 +158,33 @@ fn tmp_shutdown_workspace(tag: &str) -> PathBuf {
|
|
|
158
158
|
|
|
159
159
|
struct CleanShutdownTransport {
|
|
160
160
|
session_present: Mutex<bool>,
|
|
161
|
+
targets: Vec<PaneInfo>,
|
|
162
|
+
kill_server_called: Mutex<bool>,
|
|
163
|
+
probe_timeout_kind: Option<&'static str>,
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
impl CleanShutdownTransport {
|
|
164
167
|
fn new() -> Self {
|
|
165
|
-
Self {
|
|
168
|
+
Self {
|
|
169
|
+
session_present: Mutex::new(true),
|
|
170
|
+
targets: Vec::new(),
|
|
171
|
+
kill_server_called: Mutex::new(false),
|
|
172
|
+
probe_timeout_kind: None,
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn with_targets(mut self, targets: Vec<PaneInfo>) -> Self {
|
|
177
|
+
self.targets = targets;
|
|
178
|
+
self
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn kill_server_called(&self) -> bool {
|
|
182
|
+
*self.kill_server_called.lock().unwrap()
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
fn with_probe_timeout(mut self, probe: &'static str) -> Self {
|
|
186
|
+
self.probe_timeout_kind = Some(probe);
|
|
187
|
+
self
|
|
166
188
|
}
|
|
167
189
|
}
|
|
168
190
|
|
|
@@ -227,7 +249,11 @@ impl Transport for CleanShutdownTransport {
|
|
|
227
249
|
}
|
|
228
250
|
|
|
229
251
|
fn list_targets(&self) -> Result<Vec<PaneInfo>, TransportError> {
|
|
230
|
-
|
|
252
|
+
if *self.session_present.lock().unwrap() {
|
|
253
|
+
Ok(self.targets.clone())
|
|
254
|
+
} else {
|
|
255
|
+
Ok(Vec::new())
|
|
256
|
+
}
|
|
231
257
|
}
|
|
232
258
|
|
|
233
259
|
fn has_session(&self, _session: &SessionName) -> Result<bool, TransportError> {
|
|
@@ -248,10 +274,18 @@ impl Transport for CleanShutdownTransport {
|
|
|
248
274
|
}
|
|
249
275
|
|
|
250
276
|
fn kill_session(&self, _session: &SessionName) -> Result<(), TransportError> {
|
|
277
|
+
if let Some(probe) = self.probe_timeout_kind {
|
|
278
|
+
crate::os_probe::set_probe_timeout_for_test(probe, None, 900);
|
|
279
|
+
}
|
|
251
280
|
*self.session_present.lock().unwrap() = false;
|
|
252
281
|
Ok(())
|
|
253
282
|
}
|
|
254
283
|
|
|
284
|
+
fn kill_server(&self) -> Result<(), TransportError> {
|
|
285
|
+
*self.kill_server_called.lock().unwrap() = true;
|
|
286
|
+
Ok(())
|
|
287
|
+
}
|
|
288
|
+
|
|
255
289
|
fn kill_window(&self, _target: &Target) -> Result<(), TransportError> {
|
|
256
290
|
Ok(())
|
|
257
291
|
}
|
|
@@ -260,3 +294,126 @@ impl Transport for CleanShutdownTransport {
|
|
|
260
294
|
Ok(AttachOutcome::Attached)
|
|
261
295
|
}
|
|
262
296
|
}
|
|
297
|
+
|
|
298
|
+
#[test]
|
|
299
|
+
fn lsof_cwd_timeout_is_diagnostic_not_shutdown_partial() {
|
|
300
|
+
let ws = tmp_shutdown_workspace("lsof-cwd-timeout-diagnostic");
|
|
301
|
+
crate::state::persist::save_runtime_state(
|
|
302
|
+
&ws,
|
|
303
|
+
&json!({
|
|
304
|
+
"session_name": "team-lsof-cwd-timeout",
|
|
305
|
+
"agents": {
|
|
306
|
+
"fake_impl": {
|
|
307
|
+
"status": "running",
|
|
308
|
+
"provider": "fake",
|
|
309
|
+
"window": "fake_impl"
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}),
|
|
313
|
+
)
|
|
314
|
+
.unwrap();
|
|
315
|
+
|
|
316
|
+
let out = crate::cli::lifecycle_port::shutdown_with_transport(
|
|
317
|
+
&ws,
|
|
318
|
+
true,
|
|
319
|
+
None,
|
|
320
|
+
&CleanShutdownTransport::new().with_probe_timeout("lsof_cwd"),
|
|
321
|
+
)
|
|
322
|
+
.expect("shutdown should complete");
|
|
323
|
+
|
|
324
|
+
assert_eq!(out["ok"], json!(true));
|
|
325
|
+
assert_eq!(out["status"], json!("ok"));
|
|
326
|
+
assert_eq!(out["phase"], json!(null));
|
|
327
|
+
assert_eq!(out["verification_degraded"], json!(true));
|
|
328
|
+
assert_eq!(out["probe_timeout_kind"], json!("lsof_cwd"));
|
|
329
|
+
assert_eq!(out["residuals"]["sessions"], json!([]));
|
|
330
|
+
assert_eq!(out["residuals"]["processes"], json!([]));
|
|
331
|
+
|
|
332
|
+
let events = crate::event_log::EventLog::new(&ws).tail(0).expect("events");
|
|
333
|
+
let shutdown = events
|
|
334
|
+
.iter()
|
|
335
|
+
.find(|event| {
|
|
336
|
+
event.get("event").and_then(serde_json::Value::as_str) == Some("lifecycle.shutdown")
|
|
337
|
+
})
|
|
338
|
+
.unwrap_or_else(|| panic!("missing lifecycle.shutdown event: {events:?}"));
|
|
339
|
+
assert_eq!(shutdown["status"], json!("ok"));
|
|
340
|
+
assert_eq!(shutdown["phase"], json!(null));
|
|
341
|
+
assert_eq!(shutdown["verification_degraded"], json!(true));
|
|
342
|
+
assert_eq!(shutdown["probe_timeout_kind"], json!("lsof_cwd"));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
#[test]
|
|
346
|
+
fn ps_table_timeout_still_degrades_shutdown_truth() {
|
|
347
|
+
let ws = tmp_shutdown_workspace("ps-table-timeout-partial");
|
|
348
|
+
crate::state::persist::save_runtime_state(
|
|
349
|
+
&ws,
|
|
350
|
+
&json!({
|
|
351
|
+
"session_name": "team-ps-table-timeout",
|
|
352
|
+
"agents": {
|
|
353
|
+
"fake_impl": {
|
|
354
|
+
"status": "running",
|
|
355
|
+
"provider": "fake",
|
|
356
|
+
"window": "fake_impl"
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}),
|
|
360
|
+
)
|
|
361
|
+
.unwrap();
|
|
362
|
+
|
|
363
|
+
let out = crate::cli::lifecycle_port::shutdown_with_transport(
|
|
364
|
+
&ws,
|
|
365
|
+
true,
|
|
366
|
+
None,
|
|
367
|
+
&CleanShutdownTransport::new().with_probe_timeout("ps_table"),
|
|
368
|
+
)
|
|
369
|
+
.expect("shutdown should complete");
|
|
370
|
+
|
|
371
|
+
assert_eq!(out["ok"], json!(false));
|
|
372
|
+
assert_eq!(out["status"], json!("partial"));
|
|
373
|
+
assert_eq!(out["phase"], json!("os_probe"));
|
|
374
|
+
assert_eq!(out["verification_degraded"], json!(true));
|
|
375
|
+
assert_eq!(out["probe_timeout_kind"], json!("ps_table"));
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
#[test]
|
|
379
|
+
fn leader_env_tmux_socket_never_kills_server_even_when_sessions_look_exclusive() {
|
|
380
|
+
let ws = tmp_shutdown_workspace("leader-env-socket-no-kill-server");
|
|
381
|
+
crate::state::persist::save_runtime_state(
|
|
382
|
+
&ws,
|
|
383
|
+
&json!({
|
|
384
|
+
"tmux_socket_source": "leader_env",
|
|
385
|
+
"agents": {
|
|
386
|
+
"fake_impl": {
|
|
387
|
+
"status": "running",
|
|
388
|
+
"provider": "fake",
|
|
389
|
+
"window": "fake_impl"
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}),
|
|
393
|
+
)
|
|
394
|
+
.unwrap();
|
|
395
|
+
let transport = CleanShutdownTransport::new().with_targets(vec![PaneInfo {
|
|
396
|
+
pane_id: PaneId::new("%1"),
|
|
397
|
+
session: SessionName::new("team-layout"),
|
|
398
|
+
window_index: Some(0),
|
|
399
|
+
window_name: Some(WindowName::new("team-w1")),
|
|
400
|
+
pane_index: Some(0),
|
|
401
|
+
tty: None,
|
|
402
|
+
current_command: None,
|
|
403
|
+
current_path: None,
|
|
404
|
+
active: true,
|
|
405
|
+
pane_pid: None,
|
|
406
|
+
leader_env: BTreeMap::new(),
|
|
407
|
+
}]);
|
|
408
|
+
|
|
409
|
+
let out = crate::cli::lifecycle_port::shutdown_with_transport(&ws, true, None, &transport)
|
|
410
|
+
.expect("shutdown should complete");
|
|
411
|
+
|
|
412
|
+
assert_eq!(out["ok"], json!(true));
|
|
413
|
+
assert_eq!(out["killed_sessions"], json!(["team-layout"]));
|
|
414
|
+
assert_eq!(out["spared_sessions"], json!([]));
|
|
415
|
+
assert!(
|
|
416
|
+
!transport.kill_server_called(),
|
|
417
|
+
"leader-env/shared socket shutdown must kill sessions individually, never kill-server"
|
|
418
|
+
);
|
|
419
|
+
}
|
|
@@ -97,7 +97,8 @@ impl CliError {
|
|
|
97
97
|
"tmux session `{session}` already exists. It may be an active team. Do not terminate existing tmux sessions from startup; use a different team name or runtime.session_name and start again."
|
|
98
98
|
);
|
|
99
99
|
payload.next_actions = Some(vec![
|
|
100
|
-
"Use a different team name or runtime.session_name before starting again."
|
|
100
|
+
"Use a different team name or runtime.session_name before starting again."
|
|
101
|
+
.to_string(),
|
|
101
102
|
]);
|
|
102
103
|
}
|
|
103
104
|
}
|
|
@@ -262,6 +263,7 @@ pub struct QuickStartArgs {
|
|
|
262
263
|
pub team_id: Option<String>,
|
|
263
264
|
pub yes: bool,
|
|
264
265
|
pub fresh: bool,
|
|
266
|
+
pub no_display: bool,
|
|
265
267
|
pub json: bool,
|
|
266
268
|
}
|
|
267
269
|
|
|
@@ -304,6 +306,33 @@ pub struct SendArgs {
|
|
|
304
306
|
pub message_id: Option<String>,
|
|
305
307
|
}
|
|
306
308
|
|
|
309
|
+
/// E23 worker-side emergency fallback for `team_orchestrator.send_message`
|
|
310
|
+
/// transport failures. This is not a general control-plane send path.
|
|
311
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
312
|
+
pub struct FallbackSendLeaderArgs {
|
|
313
|
+
pub workspace: PathBuf,
|
|
314
|
+
pub team: Option<String>,
|
|
315
|
+
pub sender: String,
|
|
316
|
+
pub task: Option<String>,
|
|
317
|
+
pub message_id: String,
|
|
318
|
+
pub content: String,
|
|
319
|
+
pub primary_error: String,
|
|
320
|
+
pub json: bool,
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/// E23 worker-side emergency fallback for `team_orchestrator.report_result`
|
|
324
|
+
/// transport failures. It must still persist through the results DB.
|
|
325
|
+
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
326
|
+
pub struct FallbackReportResultArgs {
|
|
327
|
+
pub workspace: PathBuf,
|
|
328
|
+
pub team: Option<String>,
|
|
329
|
+
pub agent_id: String,
|
|
330
|
+
pub task_id: String,
|
|
331
|
+
pub result_json: String,
|
|
332
|
+
pub primary_error: String,
|
|
333
|
+
pub json: bool,
|
|
334
|
+
}
|
|
335
|
+
|
|
307
336
|
/// `allow-peer-talk`(`parser.py`): allow direct peer communication between two agents.
|
|
308
337
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
309
338
|
pub struct AllowPeerTalkArgs {
|
|
@@ -179,7 +179,7 @@ fn front_matter_non_object_errors() {
|
|
|
179
179
|
// separators=(",",":"))` with the workspace path templated to __WS__.
|
|
180
180
|
// (team-agent-public v0.2.11, /tmp/probe_compiler.py.)
|
|
181
181
|
|
|
182
|
-
const BASE_NOPROFILE_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks and report result_envelope_v1.","file":null},"tools":["fs_read","fs_write","execute_bash","mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"
|
|
182
|
+
const BASE_NOPROFILE_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"implementer","role":"Implementation Engineer","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Implement bounded tasks and report result_envelope_v1.","file":null},"tools":["fs_read","fs_write","execute_bash","mcp_team"],"permission_mode":"restricted","preferred_for":["implementer","Implementation Engineer"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"implementer","rules":[{"id":"route-implementer","match":{"assignee":["implementer"]},"assign_to":"implementer","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"adaptive","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":1,"startup_order":["implementer"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"implementer","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
|
|
183
183
|
|
|
184
184
|
#[test]
|
|
185
185
|
fn compile_base_noprofile_matches_python_dict_order_and_values() {
|
|
@@ -352,7 +352,7 @@ tools:
|
|
|
352
352
|
Bravo body.
|
|
353
353
|
";
|
|
354
354
|
|
|
355
|
-
const TWO_AGENTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"alpha","role":"Alpha Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Alpha body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["alpha","Alpha Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}},{"id":"bravo","role":"Bravo Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Bravo body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["bravo","Bravo Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"alpha","rules":[{"id":"route-alpha","match":{"assignee":["alpha"]},"assign_to":"alpha","priority":10},{"id":"route-bravo","match":{"assignee":["bravo"]},"assign_to":"bravo","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"
|
|
355
|
+
const TWO_AGENTS_JSON: &str = r#"{"version":1,"team":{"name":"doc-team","mode":"supervisor_worker","objective":"Compile role docs.","workspace":"__WS__"},"leader":{"id":"leader","role":"leader","provider":"codex","model":"gpt-5.5","tools":["fs_read","fs_list","mcp_team"],"context_policy":{"keep_user_thread":true,"receive_worker_outputs":"business_messages_and_short_summaries","max_worker_result_tokens":2000}},"agents":[{"id":"alpha","role":"Alpha Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Alpha body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["alpha","Alpha Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}},{"id":"bravo","role":"Bravo Worker","provider":"codex","model":"gpt-5.5","auth_mode":"subscription","working_directory":"__WS__","system_prompt":{"inline":"Bravo body.","file":null},"tools":["mcp_team"],"permission_mode":"restricted","preferred_for":["bravo","Bravo Worker"],"avoid_for":[],"output_contract":{"format":"result_envelope_v1","required_fields":["task_id","status","summary","artifacts"]}}],"routing":{"default_assignee":"alpha","rules":[{"id":"route-alpha","match":{"assignee":["alpha"]},"assign_to":"alpha","priority":10},{"id":"route-bravo","match":{"assignee":["bravo"]},"assign_to":"bravo","priority":10}]},"communication":{"protocol":"mcp_inbox","topology":"leader_centered","worker_to_worker":true,"ack_timeout_sec":60,"result_format":"result_envelope_v1","message_store":{"sqlite":".team/runtime/team.db","mirror_files":".team/messages"}},"runtime":{"backend":"tmux","display_backend":"adaptive","session_name":"team-doc-team","auto_launch":true,"require_user_approval_before_launch":true,"max_active_agents":2,"startup_order":["alpha","bravo"],"dangerous_auto_approve":false,"fast":false,"tick_interval_sec":2,"push_min_interval_sec":60,"stuck_timeout_sec":300},"context":{"state_file":"team_state.md","artifact_dir":".team/artifacts","log_dir":".team/logs","summarization":{"worker_full_logs":"retain_outside_leader_context","state_update":"after_each_result"}},"tasks":[{"id":"task_initial","title":"Initial document-driven team task","type":"implementation","assignee":"alpha","deps":[],"acceptance":["Worker reports valid result_envelope_v1"],"status":"pending","requires_tools":["mcp_team"],"files":[],"risk":"low"}]}"#;
|
|
356
356
|
|
|
357
357
|
#[test]
|
|
358
358
|
fn compile_two_agents_sorted_by_filename_with_routing_and_startup_order() {
|
|
@@ -247,7 +247,7 @@ pub fn compile_team(team_dir: &Path) -> Result<Value, ModelError> {
|
|
|
247
247
|
"display_backend",
|
|
248
248
|
Value::Str(
|
|
249
249
|
string_field(&team_meta, "display_backend")
|
|
250
|
-
.unwrap_or_else(|| "
|
|
250
|
+
.unwrap_or_else(|| "adaptive".to_string()),
|
|
251
251
|
),
|
|
252
252
|
),
|
|
253
253
|
("session_name", Value::Str(session_name(&team_meta, &team_name))),
|
|
@@ -107,7 +107,10 @@ fn parse_rendered_header(line: &str) -> Option<RenderedBlock> {
|
|
|
107
107
|
.split_once(" for ")
|
|
108
108
|
.map(|(_, task)| task.trim().to_string())
|
|
109
109
|
.filter(|task| !task.is_empty());
|
|
110
|
-
Some(RenderedBlock {
|
|
110
|
+
Some(RenderedBlock {
|
|
111
|
+
task_id,
|
|
112
|
+
lines: Vec::new(),
|
|
113
|
+
})
|
|
111
114
|
}
|
|
112
115
|
|
|
113
116
|
fn parse_token(line: &str) -> Option<String> {
|
|
@@ -135,149 +138,13 @@ fn report_fake_result(
|
|
|
135
138
|
.ok()
|
|
136
139
|
.map(|value| value.trim().to_string())
|
|
137
140
|
.filter(|value| !value.is_empty());
|
|
138
|
-
|
|
141
|
+
let _ = source_content;
|
|
139
142
|
crate::messaging::report_result_for_owner_team(workspace, &envelope, owner_team.as_deref())
|
|
140
143
|
.map_err(|e| FakeWorkerError::Report(e.to_string()))?;
|
|
141
|
-
mirror_fake_result_to_leader(workspace, owner_team.as_deref(), &envelope, source_content);
|
|
142
144
|
writeln!(output, "{}", serde_json::to_string(&envelope)?)?;
|
|
143
145
|
Ok(())
|
|
144
146
|
}
|
|
145
147
|
|
|
146
|
-
fn mirror_fake_result_to_leader(
|
|
147
|
-
workspace: &Path,
|
|
148
|
-
owner_team: Option<&str>,
|
|
149
|
-
envelope: &serde_json::Value,
|
|
150
|
-
source_content: Option<&str>,
|
|
151
|
-
) {
|
|
152
|
-
let Ok(raw_state) = crate::state::persist::load_runtime_state(workspace) else {
|
|
153
|
-
return;
|
|
154
|
-
};
|
|
155
|
-
let state = owner_team
|
|
156
|
-
.and_then(|team| crate::state::projection::resolve_owner_team_id(&raw_state, team).canonical_key().map(str::to_string))
|
|
157
|
-
.map(|team| crate::state::projection::project_top_level_view(&raw_state, &team))
|
|
158
|
-
.unwrap_or(raw_state);
|
|
159
|
-
let attached = state
|
|
160
|
-
.get("leader_receiver")
|
|
161
|
-
.and_then(|receiver| receiver.get("status"))
|
|
162
|
-
.and_then(serde_json::Value::as_str)
|
|
163
|
-
== Some("attached");
|
|
164
|
-
let pane_id = state
|
|
165
|
-
.get("leader_receiver")
|
|
166
|
-
.and_then(|receiver| receiver.get("pane_id"))
|
|
167
|
-
.and_then(serde_json::Value::as_str)
|
|
168
|
-
.filter(|pane| !pane.is_empty())
|
|
169
|
-
.map(str::to_string)
|
|
170
|
-
.or_else(|| owner_team.and_then(|team| claimed_pane_from_events(workspace, team)));
|
|
171
|
-
let Some(pane_id) = pane_id.filter(|_| attached || owner_team.is_some()) else {
|
|
172
|
-
return;
|
|
173
|
-
};
|
|
174
|
-
let summary = envelope
|
|
175
|
-
.get("summary")
|
|
176
|
-
.and_then(serde_json::Value::as_str)
|
|
177
|
-
.unwrap_or("Fake worker completed");
|
|
178
|
-
let result_id = envelope
|
|
179
|
-
.get("result_id")
|
|
180
|
-
.and_then(serde_json::Value::as_str)
|
|
181
|
-
.unwrap_or("");
|
|
182
|
-
let text = match source_content.filter(|content| !content.trim().is_empty()) {
|
|
183
|
-
Some(content) => format!(
|
|
184
|
-
"Fake worker result: {summary}\nMessage content: {}\nResult id: {result_id}",
|
|
185
|
-
content.trim()
|
|
186
|
-
),
|
|
187
|
-
None => format!("Fake worker result: {summary}\nResult id: {result_id}"),
|
|
188
|
-
};
|
|
189
|
-
let target = crate::transport::Target::Pane(crate::transport::PaneId::new(&pane_id));
|
|
190
|
-
let payload = crate::transport::InjectPayload::Text(text.clone());
|
|
191
|
-
let key = crate::transport::Key::Enter;
|
|
192
|
-
if let Some(socket) = leader_receiver_full_socket(&state) {
|
|
193
|
-
let endpoint_backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
|
|
194
|
-
if crate::transport::Transport::inject(&endpoint_backend, &target, &payload, key, true).is_ok() {
|
|
195
|
-
mirror_to_team_session_panes(&state, workspace, agent_id_from_envelope(envelope), &text);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
let workspace_backend = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
|
|
200
|
-
let _ = crate::transport::Transport::inject(&workspace_backend, &target, &payload, key, true);
|
|
201
|
-
mirror_to_team_session_panes(&state, workspace, agent_id_from_envelope(envelope), &text);
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
fn leader_receiver_full_socket(state: &serde_json::Value) -> Option<&str> {
|
|
205
|
-
state
|
|
206
|
-
.get("leader_receiver")
|
|
207
|
-
.and_then(|receiver| receiver.get("tmux_socket"))
|
|
208
|
-
.and_then(serde_json::Value::as_str)
|
|
209
|
-
.filter(|socket| !socket.is_empty() && Path::new(socket).is_absolute())
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
fn claimed_pane_from_events(workspace: &Path, owner_team: &str) -> Option<String> {
|
|
213
|
-
crate::event_log::EventLog::new(workspace)
|
|
214
|
-
.tail(50)
|
|
215
|
-
.ok()?
|
|
216
|
-
.into_iter()
|
|
217
|
-
.rev()
|
|
218
|
-
.find_map(|event| {
|
|
219
|
-
if event.get("event").and_then(serde_json::Value::as_str)
|
|
220
|
-
!= Some("leader_receiver.rebind_applied")
|
|
221
|
-
{
|
|
222
|
-
return None;
|
|
223
|
-
}
|
|
224
|
-
if event.get("team_id").and_then(serde_json::Value::as_str) != Some(owner_team) {
|
|
225
|
-
return None;
|
|
226
|
-
}
|
|
227
|
-
event
|
|
228
|
-
.get("new_pane_id")
|
|
229
|
-
.and_then(serde_json::Value::as_str)
|
|
230
|
-
.filter(|pane| !pane.is_empty())
|
|
231
|
-
.map(str::to_string)
|
|
232
|
-
})
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
fn agent_id_from_envelope(envelope: &serde_json::Value) -> Option<&str> {
|
|
236
|
-
envelope.get("agent_id").and_then(serde_json::Value::as_str)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
fn mirror_to_team_session_panes(
|
|
240
|
-
state: &serde_json::Value,
|
|
241
|
-
workspace: &Path,
|
|
242
|
-
agent_id: Option<&str>,
|
|
243
|
-
text: &str,
|
|
244
|
-
) {
|
|
245
|
-
let Some(session) = state
|
|
246
|
-
.get("session_name")
|
|
247
|
-
.and_then(serde_json::Value::as_str)
|
|
248
|
-
.filter(|session| !session.is_empty())
|
|
249
|
-
else {
|
|
250
|
-
return;
|
|
251
|
-
};
|
|
252
|
-
let worker_pane = agent_id.and_then(|agent_id| {
|
|
253
|
-
state
|
|
254
|
-
.get("agents")
|
|
255
|
-
.and_then(|agents| agents.get(agent_id))
|
|
256
|
-
.and_then(|agent| agent.get("pane_id"))
|
|
257
|
-
.and_then(serde_json::Value::as_str)
|
|
258
|
-
});
|
|
259
|
-
if let Some(socket) = leader_receiver_full_socket(state) {
|
|
260
|
-
let backend = crate::tmux_backend::TmuxBackend::for_tmux_endpoint(socket);
|
|
261
|
-
for pane in crate::transport::Transport::list_targets(&backend).unwrap_or_default() {
|
|
262
|
-
if pane.session.as_str() != session || Some(pane.pane_id.as_str()) == worker_pane {
|
|
263
|
-
continue;
|
|
264
|
-
}
|
|
265
|
-
let target = crate::transport::Target::Pane(pane.pane_id);
|
|
266
|
-
let payload = crate::transport::InjectPayload::Text(text.to_string());
|
|
267
|
-
let _ = crate::transport::Transport::inject(&backend, &target, &payload, crate::transport::Key::Enter, true);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
let backend = crate::tmux_backend::TmuxBackend::for_workspace(workspace);
|
|
271
|
-
for pane in crate::transport::Transport::list_targets(&backend).unwrap_or_default() {
|
|
272
|
-
if pane.session.as_str() != session || Some(pane.pane_id.as_str()) == worker_pane {
|
|
273
|
-
continue;
|
|
274
|
-
}
|
|
275
|
-
let target = crate::transport::Target::Pane(pane.pane_id);
|
|
276
|
-
let payload = crate::transport::InjectPayload::Text(text.to_string());
|
|
277
|
-
let _ = crate::transport::Transport::inject(&backend, &target, &payload, crate::transport::Key::Enter, true);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
148
|
fn fake_envelope(
|
|
282
149
|
workspace: &Path,
|
|
283
150
|
agent_id: &str,
|
|
@@ -331,7 +198,9 @@ mod tests {
|
|
|
331
198
|
fn stored_result(ws: &std::path::Path) -> (String, String, String, serde_json::Value) {
|
|
332
199
|
let store = MessageStore::open(ws).unwrap();
|
|
333
200
|
let conn = open_db(store.db_path()).unwrap();
|
|
334
|
-
let mut stmt = conn
|
|
201
|
+
let mut stmt = conn
|
|
202
|
+
.prepare("select task_id, agent_id, status, envelope from results")
|
|
203
|
+
.unwrap();
|
|
335
204
|
let row = stmt
|
|
336
205
|
.query_row([], |r| {
|
|
337
206
|
Ok((
|
|
@@ -354,9 +223,18 @@ mod tests {
|
|
|
354
223
|
("t1", "w1", "success"),
|
|
355
224
|
"stored result row must be task_id=t1 agent_id=w1 status=success"
|
|
356
225
|
);
|
|
357
|
-
assert_eq!(
|
|
358
|
-
|
|
359
|
-
|
|
226
|
+
assert_eq!(
|
|
227
|
+
env["schema_version"],
|
|
228
|
+
serde_json::json!("result_envelope_v1")
|
|
229
|
+
);
|
|
230
|
+
assert_eq!(
|
|
231
|
+
env["summary"],
|
|
232
|
+
serde_json::json!("Fake worker handled message m1")
|
|
233
|
+
);
|
|
234
|
+
assert_eq!(
|
|
235
|
+
env["tests"],
|
|
236
|
+
serde_json::json!([{"command": "fake-provider", "status": "passed"}])
|
|
237
|
+
);
|
|
360
238
|
let artifact_path = env["artifacts"][0]["path"].as_str().unwrap_or_default();
|
|
361
239
|
assert!(
|
|
362
240
|
artifact_path.ends_with(".team/logs/w1.scrollback"),
|
|
@@ -375,8 +253,14 @@ mod tests {
|
|
|
375
253
|
|
|
376
254
|
assert_golden_envelope(&ws);
|
|
377
255
|
let printed = String::from_utf8(out).unwrap();
|
|
378
|
-
assert!(
|
|
379
|
-
|
|
256
|
+
assert!(
|
|
257
|
+
printed.contains("TEAM_AGENT_FAKE_READY agent=w1"),
|
|
258
|
+
"must print the READY idle marker; got {printed}"
|
|
259
|
+
);
|
|
260
|
+
assert!(
|
|
261
|
+
printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"),
|
|
262
|
+
"must print the WORKING marker on a non-empty line; got {printed}"
|
|
263
|
+
);
|
|
380
264
|
}
|
|
381
265
|
|
|
382
266
|
// Rendered-message form: a `Team Agent message from leader for t1:` block ending `[team-agent-token:m1]`
|
|
@@ -391,6 +275,9 @@ mod tests {
|
|
|
391
275
|
|
|
392
276
|
assert_golden_envelope(&ws);
|
|
393
277
|
let printed = String::from_utf8(out).unwrap();
|
|
394
|
-
assert!(
|
|
278
|
+
assert!(
|
|
279
|
+
printed.contains("TEAM_AGENT_FAKE_WORKING agent=w1"),
|
|
280
|
+
"rendered form prints WORKING per line; got {printed}"
|
|
281
|
+
);
|
|
395
282
|
}
|
|
396
283
|
}
|
|
@@ -11,15 +11,15 @@ use super::*;
|
|
|
11
11
|
// ── lifecycle::display —— 能力门 / 后端解析 / 开关 / rebind 后重建 ────────────
|
|
12
12
|
|
|
13
13
|
/// `resolve_display_backend(requested, recorded, source)`(`display/backend.py`)。默认
|
|
14
|
-
/// none
|
|
14
|
+
/// adaptive;显式 none 是逃生口。
|
|
15
15
|
pub fn resolve_display_backend(
|
|
16
16
|
requested: Option<DisplayBackend>,
|
|
17
17
|
recorded: Option<DisplayBackend>,
|
|
18
18
|
) -> ResolvedBackend {
|
|
19
|
-
let backend = requested.or(recorded).unwrap_or(DisplayBackend::
|
|
19
|
+
let backend = requested.or(recorded).unwrap_or(DisplayBackend::Adaptive);
|
|
20
20
|
ResolvedBackend {
|
|
21
21
|
backend,
|
|
22
|
-
non_default: backend != DisplayBackend::
|
|
22
|
+
non_default: backend != DisplayBackend::Adaptive,
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
|