@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.
- package/Cargo.lock +1 -1
- package/Cargo.toml +1 -1
- package/crates/team-agent/src/cli/adapters.rs +44 -1
- package/crates/team-agent/src/cli/diagnose.rs +183 -0
- package/crates/team-agent/src/cli/emit.rs +63 -14
- package/crates/team-agent/src/cli/leader.rs +8 -1
- package/crates/team-agent/src/cli/mod.rs +263 -5
- package/crates/team-agent/src/cli/named_address.rs +18 -1
- package/crates/team-agent/src/cli/send/presentation.rs +2 -0
- package/crates/team-agent/src/cli/spec.rs +4 -1
- package/crates/team-agent/src/cli/status_port/store.rs +7 -3
- package/crates/team-agent/src/cli/tests/named_address.rs +65 -0
- package/crates/team-agent/src/cli/types.rs +33 -0
- package/crates/team-agent/src/communication_mode/mod.rs +53 -0
- package/crates/team-agent/src/compiler/tests.rs +5 -5
- package/crates/team-agent/src/compiler.rs +53 -1
- package/crates/team-agent/src/db/message_store.rs +45 -1
- package/crates/team-agent/src/fake_worker.rs +21 -1
- package/crates/team-agent/src/leader/start.rs +732 -94
- package/crates/team-agent/src/leader/tests/identity.rs +64 -57
- package/crates/team-agent/src/leader/tests/identity_session_names.rs +42 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/fork_agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/agent.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +29 -11
- package/crates/team-agent/src/mcp_server/normalize.rs +1 -0
- package/crates/team-agent/src/mcp_server/tests/send.rs +26 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +100 -66
- package/crates/team-agent/src/mcp_server/types.rs +5 -0
- package/crates/team-agent/src/mcp_server/wire.rs +27 -21
- package/crates/team-agent/src/messaging/delivery.rs +105 -38
- package/crates/team-agent/src/messaging/leader_channel.rs +33 -10
- package/crates/team-agent/src/messaging/leader_receiver.rs +40 -23
- package/crates/team-agent/src/messaging/presentation.rs +109 -0
- package/crates/team-agent/src/messaging/results.rs +165 -17
- package/crates/team-agent/src/messaging/send.rs +94 -81
- package/crates/team-agent/src/messaging/tests/leader_channel.rs +6 -6
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +187 -0
- package/crates/team-agent/src/messaging/tests/runtime.rs +13 -6
- package/crates/team-agent/src/messaging/types.rs +5 -0
- package/crates/team-agent/src/messaging/watchers.rs +5 -0
- package/crates/team-agent/src/model/mod.rs +1 -0
- package/crates/team-agent/src/model/pane_authority_refusal.rs +358 -0
- package/crates/team-agent/src/model/spec.rs +16 -0
- package/crates/team-agent/src/provider/session/capture.rs +40 -18
- package/crates/team-agent/src/provider/session_scan/claude.rs +92 -3
- package/crates/team-agent/src/provider/session_scan/common.rs +23 -7
- package/crates/team-agent/src/provider/session_scan.rs +5 -0
- package/crates/team-agent/src/topology.rs +3 -0
- package/package.json +4 -4
- 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 =
|
|
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
|
|
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
|
|
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!(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
226
|
-
|
|
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
|
|
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 =
|
|
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!(
|
|
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
|
-
|
|
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!(
|
|
@@ -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.
|
|
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"]));
|