@team-agent/installer 0.5.113 → 0.5.114
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 +252 -84
- package/Cargo.toml +3 -1
- package/crates/team-agent/Cargo.toml +3 -0
- package/crates/team-agent/src/cli/adapters.rs +28 -15
- package/crates/team-agent/src/cli/diagnose.rs +36 -13
- package/crates/team-agent/src/cli/emit.rs +55 -20
- package/crates/team-agent/src/cli/leader.rs +16 -0
- package/crates/team-agent/src/cli/leader_prompt.rs +261 -0
- package/crates/team-agent/src/cli/mod.rs +152 -8
- package/crates/team-agent/src/cli/models.rs +68 -18
- package/crates/team-agent/src/cli/spec.rs +7 -1
- package/crates/team-agent/src/cli/status_port/brief.rs +268 -101
- package/crates/team-agent/src/cli/tests/base.rs +88 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +2 -0
- package/crates/team-agent/src/compiler.rs +66 -18
- package/crates/team-agent/src/contract_runtime/backend.rs +646 -0
- package/crates/team-agent/src/contract_runtime/config.rs +309 -0
- package/crates/team-agent/src/contract_runtime/discovery.rs +237 -0
- package/crates/team-agent/src/contract_runtime/forward.rs +229 -0
- package/crates/team-agent/src/contract_runtime/framework.rs +261 -0
- package/crates/team-agent/src/contract_runtime/mcp.rs +396 -0
- package/crates/team-agent/src/contract_runtime/mod.rs +14 -0
- package/crates/team-agent/src/contract_runtime/prompt.rs +216 -0
- package/crates/team-agent/src/contract_runtime/pump.rs +207 -0
- package/crates/team-agent/src/contract_runtime/registry.rs +30 -0
- package/crates/team-agent/src/coordinator/tick.rs +26 -8
- package/crates/team-agent/src/leader/mod.rs +1 -0
- package/crates/team-agent/src/leader/prompt.rs +906 -0
- package/crates/team-agent/src/leader/start.rs +260 -16
- package/crates/team-agent/src/leader/tests/mod.rs +2 -0
- package/crates/team-agent/src/leader/tests/prompt_contract.rs +268 -0
- package/crates/team-agent/src/leader/types.rs +6 -0
- package/crates/team-agent/src/lib.rs +4 -0
- package/crates/team-agent/src/lifecycle/launch/quick_start.rs +472 -388
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +38 -9
- package/crates/team-agent/src/lifecycle/launch/state_projection.rs +17 -0
- package/crates/team-agent/src/lifecycle/launch.rs +39 -28
- package/crates/team-agent/src/lifecycle/types.rs +4 -1
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +37 -27
- package/crates/team-agent/src/main.rs +6 -0
- package/crates/team-agent/src/messaging/delivery.rs +50 -1
- package/crates/team-agent/src/messaging/send.rs +10 -0
- package/crates/team-agent/src/messaging/tests/leader_inject_acceptance.rs +131 -0
- package/crates/team-agent/src/model/spec.rs +24 -12
- package/crates/team-agent/src/state/persist.rs +73 -0
- package/package.json +4 -4
|
@@ -45,7 +45,7 @@ use super::*;
|
|
|
45
45
|
/// params:
|
|
46
46
|
/// spec_path: spec 路径,team 目录优先取 state 里的 team_dir
|
|
47
47
|
/// session_name: 目标 tmux session
|
|
48
|
-
/// returns:
|
|
48
|
+
/// returns: legacy 启动收据与独立 contract 已提交席位 ID;不为私有 socket 伪造 legacy pane
|
|
49
49
|
/// errors: 命令拼装或 transport spawn 失败时返回 LifecycleError
|
|
50
50
|
/// ---
|
|
51
51
|
pub(super) fn spawn_agents(
|
|
@@ -54,7 +54,7 @@ pub(super) fn spawn_agents(
|
|
|
54
54
|
spec: &Value,
|
|
55
55
|
session_name: &SessionName,
|
|
56
56
|
transport: &dyn Transport,
|
|
57
|
-
) -> Result<Vec<StartedAgent>, LifecycleError> {
|
|
57
|
+
) -> Result<(Vec<StartedAgent>, Vec<AgentId>), LifecycleError> {
|
|
58
58
|
// E5 解耦:team_dir(角色定义 + profiles 所在)≠ spec_path.parent()(spec 已迁出到 .team/runtime)。
|
|
59
59
|
// 优先取 state.team_dir(角色目录),回落 spec_path.parent()(legacy 同目录布局)。
|
|
60
60
|
let team_dir_buf = crate::state::persist::load_runtime_state(workspace)
|
|
@@ -74,6 +74,10 @@ pub(super) fn spawn_agents(
|
|
|
74
74
|
Some(Value::Bool(true))
|
|
75
75
|
);
|
|
76
76
|
let mut started = Vec::new();
|
|
77
|
+
#[cfg(unix)]
|
|
78
|
+
let mut contract_started = Vec::new();
|
|
79
|
+
#[cfg(not(unix))]
|
|
80
|
+
let contract_started = Vec::new();
|
|
77
81
|
for agent in spec_agent_values(spec) {
|
|
78
82
|
let Some(agent_id_raw) = agent.get("id").and_then(Value::as_str) else {
|
|
79
83
|
continue;
|
|
@@ -81,6 +85,26 @@ pub(super) fn spawn_agents(
|
|
|
81
85
|
if agent_is_paused(agent) {
|
|
82
86
|
continue;
|
|
83
87
|
}
|
|
88
|
+
#[cfg(unix)]
|
|
89
|
+
if agent
|
|
90
|
+
.get("provider")
|
|
91
|
+
.and_then(Value::as_str)
|
|
92
|
+
.is_some_and(|provider| {
|
|
93
|
+
crate::contract_runtime::registry::descriptor(provider).is_some()
|
|
94
|
+
})
|
|
95
|
+
{
|
|
96
|
+
let team = runtime_team_key_for_spec(spec_path, spec, session_name);
|
|
97
|
+
crate::contract_runtime::framework::start_role(
|
|
98
|
+
workspace,
|
|
99
|
+
team_dir,
|
|
100
|
+
&team,
|
|
101
|
+
agent_id_raw,
|
|
102
|
+
)?;
|
|
103
|
+
contract_started.push(AgentId::new(agent_id_raw));
|
|
104
|
+
// No legacy target is manufactured for the private native socket.
|
|
105
|
+
// persist_spawn_agent_state projects the actual K3 seat separately.
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
84
108
|
let agent_id = AgentId::new(agent_id_raw);
|
|
85
109
|
let provider = agent
|
|
86
110
|
.get("provider")
|
|
@@ -99,11 +123,12 @@ pub(super) fn spawn_agents(
|
|
|
99
123
|
// has both the role instruction AND the callable Team Agent MCP capability.
|
|
100
124
|
// probe5 RED proved that `build_command(.., None, None, ..)` left the worker
|
|
101
125
|
// without `report_result`; placeholders are substituted at spawn time.
|
|
102
|
-
let command_agent =
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
126
|
+
let command_agent =
|
|
127
|
+
crate::lifecycle::worker_command_context::WorkerCommandAgent::from_yaml(
|
|
128
|
+
agent,
|
|
129
|
+
Some(agent_id_raw),
|
|
130
|
+
provider,
|
|
131
|
+
)?;
|
|
107
132
|
let system_prompt =
|
|
108
133
|
crate::lifecycle::worker_command_context::compile_worker_system_prompt(&command_agent)?;
|
|
109
134
|
// 0.5.66 bypass 单源:per-agent safety 只服务 env 注入与审计,不参与 argv 决策。
|
|
@@ -431,7 +456,7 @@ pub(super) fn spawn_agents(
|
|
|
431
456
|
managed_mcp_config: plan.managed_mcp_config || profile_launch.managed_mcp_config,
|
|
432
457
|
});
|
|
433
458
|
}
|
|
434
|
-
Ok(started)
|
|
459
|
+
Ok((started, contract_started))
|
|
435
460
|
}
|
|
436
461
|
|
|
437
462
|
/// ---
|
|
@@ -451,7 +476,11 @@ pub(crate) fn agent_id_spec_source_path(workspace: &Path, agent_id: &str) -> Str
|
|
|
451
476
|
});
|
|
452
477
|
team_dir
|
|
453
478
|
.map(|dir| dir.join("agents").join(format!("{agent_id}.md")))
|
|
454
|
-
.unwrap_or_else(||
|
|
479
|
+
.unwrap_or_else(|| {
|
|
480
|
+
workspace
|
|
481
|
+
.join(".team/current/agents")
|
|
482
|
+
.join(format!("{agent_id}.md"))
|
|
483
|
+
})
|
|
455
484
|
.display()
|
|
456
485
|
.to_string()
|
|
457
486
|
}
|
|
@@ -119,6 +119,23 @@ pub(super) fn persist_spawn_agent_state(
|
|
|
119
119
|
let Some(id) = agent.get("id").and_then(Value::as_str) else {
|
|
120
120
|
continue;
|
|
121
121
|
};
|
|
122
|
+
#[cfg(unix)]
|
|
123
|
+
if agent
|
|
124
|
+
.get("provider")
|
|
125
|
+
.and_then(Value::as_str)
|
|
126
|
+
.is_some_and(|provider| {
|
|
127
|
+
crate::contract_runtime::registry::descriptor(provider).is_some()
|
|
128
|
+
})
|
|
129
|
+
{
|
|
130
|
+
let projected = if agent_is_paused(agent) {
|
|
131
|
+
serde_json::json!({"agent_id":id,"provider":"kiro","runtime_family":"contract","status":"paused","pane_id":null})
|
|
132
|
+
} else {
|
|
133
|
+
crate::contract_runtime::framework::project_seat(workspace, &team_id, id)
|
|
134
|
+
.map_err(|error| LifecycleError::StatePersist(error.to_string()))?
|
|
135
|
+
};
|
|
136
|
+
agents.insert(id.to_string(), projected);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
122
139
|
let provider = agent
|
|
123
140
|
.get("provider")
|
|
124
141
|
.and_then(Value::as_str)
|
|
@@ -133,7 +133,11 @@ pub fn launch_with_transport_in_workspace(
|
|
|
133
133
|
// 所有角色都未声明 dangerously_skip_permissions=true → 写 warning 事件不阻塞。
|
|
134
134
|
// TODO(0.6.0): remove(0.6.0 彻底删源头 A 后此警示无意义)。
|
|
135
135
|
emit_dangerous_flag_no_role_declared_warning(workspace, &spec);
|
|
136
|
-
phase_timer.emit(
|
|
136
|
+
phase_timer.emit(
|
|
137
|
+
workspace,
|
|
138
|
+
"launch.phase",
|
|
139
|
+
super::restart::LifecyclePhase::CompileSpec,
|
|
140
|
+
);
|
|
137
141
|
let session_name = spec_session_name(&spec);
|
|
138
142
|
if !dry_run && transport_has_session(transport, &session_name) {
|
|
139
143
|
return Err(LifecycleError::SessionConflict(format!(
|
|
@@ -142,11 +146,16 @@ pub fn launch_with_transport_in_workspace(
|
|
|
142
146
|
)));
|
|
143
147
|
}
|
|
144
148
|
let routes = spec_routes(&spec);
|
|
145
|
-
let started = if dry_run {
|
|
146
|
-
Vec::new()
|
|
149
|
+
let (started, contract_started) = if dry_run {
|
|
150
|
+
(Vec::new(), Vec::new())
|
|
147
151
|
} else {
|
|
148
|
-
phase_timer.emit(
|
|
149
|
-
|
|
152
|
+
phase_timer.emit(
|
|
153
|
+
workspace,
|
|
154
|
+
"launch.phase",
|
|
155
|
+
super::restart::LifecyclePhase::SpawnAll,
|
|
156
|
+
);
|
|
157
|
+
let (started, contract_started) =
|
|
158
|
+
spawn_agents(workspace, spec_path, &spec, &session_name, transport)?;
|
|
150
159
|
persist_spawn_agent_state(
|
|
151
160
|
workspace,
|
|
152
161
|
spec_path,
|
|
@@ -164,14 +173,16 @@ pub fn launch_with_transport_in_workspace(
|
|
|
164
173
|
.first()
|
|
165
174
|
.map(|agent| agent.spawned_at.as_str())
|
|
166
175
|
.unwrap_or(session_name.as_str());
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
176
|
+
if !started.is_empty() {
|
|
177
|
+
transport
|
|
178
|
+
.set_session_owner_with_generation(
|
|
179
|
+
&session_name,
|
|
180
|
+
workspace,
|
|
181
|
+
&owner_team,
|
|
182
|
+
generation,
|
|
183
|
+
)
|
|
184
|
+
.map_err(|error| LifecycleError::Transport(error.to_string()))?;
|
|
185
|
+
}
|
|
175
186
|
// 0.5.38: per-worker timing tags (source="launch") so operators can
|
|
176
187
|
// trace which worker's spawn dominates wall time. Zeros for now on
|
|
177
188
|
// the sub-timings — Step 1 first enables shape assertion; a later
|
|
@@ -208,7 +219,7 @@ pub fn launch_with_transport_in_workspace(
|
|
|
208
219
|
"launch",
|
|
209
220
|
);
|
|
210
221
|
}
|
|
211
|
-
started
|
|
222
|
+
(started, contract_started)
|
|
212
223
|
};
|
|
213
224
|
// 0.3.28 Step 1: topology invariant guard (warn-only during migration).
|
|
214
225
|
// Logs each violation to stderr; never panics. Promoted to hard error at
|
|
@@ -223,6 +234,7 @@ pub fn launch_with_transport_in_workspace(
|
|
|
223
234
|
Ok(LaunchReport {
|
|
224
235
|
session_name,
|
|
225
236
|
started,
|
|
237
|
+
contract_started,
|
|
226
238
|
dry_run,
|
|
227
239
|
tmux_endpoint: transport.tmux_endpoint(),
|
|
228
240
|
routes,
|
|
@@ -348,13 +360,13 @@ pub use cursor_mcp_iso::{
|
|
|
348
360
|
materialize_cursor_mcp_project,
|
|
349
361
|
};
|
|
350
362
|
mod cursor_mcp;
|
|
363
|
+
pub(crate) use cursor_mcp::apply_cursor_mcp_enable_profile_env;
|
|
351
364
|
pub use cursor_mcp::{
|
|
352
365
|
apply_cursor_mcp_overlay, apply_cursor_spawn_workspace_pointers,
|
|
353
366
|
apply_cursor_workspace_physical_path, cursor_mcp_enable_argv, cursor_mcp_enable_working_dir,
|
|
354
367
|
enable_cursor_workspace_mcp, enable_cursor_workspace_mcp_with_profile, physical_workspace_path,
|
|
355
368
|
prepare_cursor_seat_mcp, refuse_second_cursor_occupant,
|
|
356
369
|
};
|
|
357
|
-
pub(crate) use cursor_mcp::apply_cursor_mcp_enable_profile_env;
|
|
358
370
|
|
|
359
371
|
mod cursor_create_chat;
|
|
360
372
|
|
|
@@ -363,9 +375,9 @@ pub(super) use worker_env::*;
|
|
|
363
375
|
pub(crate) use worker_env::{
|
|
364
376
|
apply_copilot_instructions_overlay, apply_cursor_agent_rules_overlay,
|
|
365
377
|
apply_cursor_subscription_proxy_env_with_profile, apply_mcp_auto_approval_env,
|
|
366
|
-
apply_profile_launch_env, auth_mode_env_value,
|
|
367
|
-
|
|
368
|
-
|
|
378
|
+
apply_profile_launch_env, auth_mode_env_value, fill_spawn_placeholders,
|
|
379
|
+
fill_spawn_placeholders_full, inherited_env_with_team_overrides, persist_command_plan_state,
|
|
380
|
+
spawn_timestamp,
|
|
369
381
|
};
|
|
370
382
|
pub use worker_env::{apply_cursor_subscription_proxy_env, CursorProxyPresence};
|
|
371
383
|
|
|
@@ -393,18 +405,18 @@ pub(crate) use quick_start_transport::{
|
|
|
393
405
|
};
|
|
394
406
|
|
|
395
407
|
pub mod readiness;
|
|
408
|
+
pub(super) use readiness::*;
|
|
396
409
|
pub use readiness::{
|
|
397
410
|
classify_leader_binding, launched_team_receiver_is_attached, selected_team_leader_receiver,
|
|
398
411
|
LeaderBindingClass,
|
|
399
412
|
};
|
|
400
|
-
pub(super) use readiness::*;
|
|
401
413
|
|
|
402
414
|
mod add_agent;
|
|
403
415
|
pub(super) use add_agent::*;
|
|
404
416
|
pub use add_agent::{add_agent, add_agent_force};
|
|
405
417
|
pub(crate) use add_agent::{
|
|
406
|
-
add_agent_with_transport,
|
|
407
|
-
|
|
418
|
+
add_agent_with_transport, add_agent_with_transport_at_paths_locked,
|
|
419
|
+
add_agent_with_transport_force, force_recreate_with_transport_locked,
|
|
408
420
|
};
|
|
409
421
|
|
|
410
422
|
pub(crate) mod add_agent_state;
|
|
@@ -428,21 +440,20 @@ pub(super) use ownership::*;
|
|
|
428
440
|
pub(crate) use ownership::{ensure_owner_allowed, ensure_owner_allowed_for_state, state_path};
|
|
429
441
|
|
|
430
442
|
pub mod spec_state;
|
|
431
|
-
pub use spec_state::{
|
|
432
|
-
seed_launched_owner_from_caller_with_provider_lookup, worker_session_name_pub,
|
|
433
|
-
};
|
|
434
443
|
pub(crate) use spec_state::{
|
|
435
444
|
effective_runtime_config_for_worker_spawn, effective_runtime_config_for_worker_spawn_json,
|
|
436
445
|
};
|
|
437
446
|
use spec_state::{
|
|
438
|
-
env_nonempty, has_positive_caller_leader_env, initial_runtime_state,
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
spec_tasks_json, team_workspace, yaml_value_to_json,
|
|
447
|
+
env_nonempty, has_positive_caller_leader_env, initial_runtime_state, override_spec_runtime_str,
|
|
448
|
+
seed_launched_owner_from_env, spec_agent_values, spec_agents, spec_default_assignee,
|
|
449
|
+
spec_routes, spec_session_name, spec_tasks_json, team_workspace, yaml_value_to_json,
|
|
442
450
|
};
|
|
443
451
|
pub(crate) use spec_state::{
|
|
444
452
|
override_spec_session_name, override_spec_workspace, spec_agent_id_set, write_spec_atomic,
|
|
445
453
|
};
|
|
454
|
+
pub use spec_state::{
|
|
455
|
+
seed_launched_owner_from_caller_with_provider_lookup, worker_session_name_pub,
|
|
456
|
+
};
|
|
446
457
|
|
|
447
458
|
#[cfg(test)]
|
|
448
459
|
mod tests {
|
|
@@ -433,8 +433,11 @@ pub enum LifecycleError {
|
|
|
433
433
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
434
434
|
pub struct LaunchReport {
|
|
435
435
|
pub session_name: SessionName,
|
|
436
|
-
///
|
|
436
|
+
/// Legacy transport starts; their targets belong to the team transport.
|
|
437
437
|
pub started: Vec<StartedAgent>,
|
|
438
|
+
/// Committed contract starts, without fabricating legacy pane/window targets.
|
|
439
|
+
/// Empty on dry-run; populated only after the contract startup returns success.
|
|
440
|
+
pub contract_started: Vec<AgentId>,
|
|
438
441
|
/// 是否 dry-run(只解析路由/权限,不起进程)。
|
|
439
442
|
pub dry_run: bool,
|
|
440
443
|
/// Selected tmux endpoint for the team transport when known.
|
|
@@ -63,16 +63,16 @@ pub(crate) struct WorkerCommandAgent {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
impl WorkerCommandAgent {
|
|
66
|
-
/// ---
|
|
67
|
-
/// purpose: 从 spec 的 YAML agent 节点读出命令上下文
|
|
68
|
-
/// params:
|
|
69
|
-
/// agent: 单个 agent 的 YAML 节点
|
|
70
|
-
/// fallback_id: agent 节点没写 id 时的兜底 id
|
|
71
|
-
/// provider: 已解析的 provider
|
|
72
|
-
/// returns: 填好的 WorkerCommandAgent
|
|
73
|
-
/// errors: communication_mode 取值非法时返回 LifecycleError
|
|
74
|
-
/// contract_id: lifecycle.worker_command_agent.from_source
|
|
75
|
-
/// ---
|
|
66
|
+
/// ---
|
|
67
|
+
/// purpose: 从 spec 的 YAML agent 节点读出命令上下文
|
|
68
|
+
/// params:
|
|
69
|
+
/// agent: 单个 agent 的 YAML 节点
|
|
70
|
+
/// fallback_id: agent 节点没写 id 时的兜底 id
|
|
71
|
+
/// provider: 已解析的 provider
|
|
72
|
+
/// returns: 填好的 WorkerCommandAgent
|
|
73
|
+
/// errors: communication_mode 取值非法时返回 LifecycleError
|
|
74
|
+
/// contract_id: lifecycle.worker_command_agent.from_source
|
|
75
|
+
/// ---
|
|
76
76
|
pub(crate) fn from_yaml(
|
|
77
77
|
agent: &crate::model::yaml::Value,
|
|
78
78
|
fallback_id: Option<&str>,
|
|
@@ -117,16 +117,16 @@ impl WorkerCommandAgent {
|
|
|
117
117
|
})
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
-
/// ---
|
|
121
|
-
/// purpose: 从 runtime state 的 JSON agent 节点读出命令上下文
|
|
122
|
-
/// params:
|
|
123
|
-
/// agent: 单个 agent 的 JSON 节点
|
|
124
|
-
/// fallback_id: agent 节点没写 id 时的兜底 id
|
|
125
|
-
/// provider: 已解析的 provider
|
|
126
|
-
/// returns: 填好的 WorkerCommandAgent
|
|
127
|
-
/// errors: communication_mode 取值非法时返回 LifecycleError
|
|
128
|
-
/// contract_id: lifecycle.worker_command_agent.from_source
|
|
129
|
-
/// ---
|
|
120
|
+
/// ---
|
|
121
|
+
/// purpose: 从 runtime state 的 JSON agent 节点读出命令上下文
|
|
122
|
+
/// params:
|
|
123
|
+
/// agent: 单个 agent 的 JSON 节点
|
|
124
|
+
/// fallback_id: agent 节点没写 id 时的兜底 id
|
|
125
|
+
/// provider: 已解析的 provider
|
|
126
|
+
/// returns: 填好的 WorkerCommandAgent
|
|
127
|
+
/// errors: communication_mode 取值非法时返回 LifecycleError
|
|
128
|
+
/// contract_id: lifecycle.worker_command_agent.from_source
|
|
129
|
+
/// ---
|
|
130
130
|
pub(crate) fn from_json(
|
|
131
131
|
agent: &serde_json::Value,
|
|
132
132
|
fallback_id: Option<&str>,
|
|
@@ -190,7 +190,8 @@ pub(crate) fn compile_worker_system_prompt(
|
|
|
190
190
|
mcp_tool_name(agent.provider, "team_orchestrator", "get_team_status")
|
|
191
191
|
);
|
|
192
192
|
let report_result = mcp_tool_name(agent.provider, "team_orchestrator", "report_result");
|
|
193
|
-
let runtime_contract =
|
|
193
|
+
let runtime_contract =
|
|
194
|
+
runtime_contract_section(&send_message, &report_result, &get_team_status);
|
|
194
195
|
let communication_contract = agent.communication_mode.runtime_contract(&send_message);
|
|
195
196
|
let mut chunks = vec![
|
|
196
197
|
identity_section(agent),
|
|
@@ -235,7 +236,7 @@ fn mcp_tool_name(provider: Provider, server: &str, tool: &str) -> String {
|
|
|
235
236
|
}
|
|
236
237
|
}
|
|
237
238
|
|
|
238
|
-
fn runtime_contract_section(
|
|
239
|
+
pub(crate) fn runtime_contract_section(
|
|
239
240
|
send_message: &str,
|
|
240
241
|
report_result: &str,
|
|
241
242
|
get_team_status: &str,
|
|
@@ -255,10 +256,15 @@ fn communication_mode(value: Option<&str>) -> Result<CommunicationMode, Lifecycl
|
|
|
255
256
|
}
|
|
256
257
|
|
|
257
258
|
fn identity_section(agent: &WorkerCommandAgent) -> String {
|
|
258
|
-
|
|
259
|
-
"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.",
|
|
259
|
+
worker_identity_section(
|
|
260
260
|
agent.id.as_deref().unwrap_or("unknown"),
|
|
261
|
-
agent.role.as_deref().unwrap_or("developer")
|
|
261
|
+
agent.role.as_deref().unwrap_or("developer"),
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
pub(crate) fn worker_identity_section(id: &str, role: &str) -> String {
|
|
266
|
+
format!(
|
|
267
|
+
"You are Team Agent worker `{id}` with role `{role}`. When asked about your role or identity, answer with this Team Agent worker identity first, not only the generic provider product identity."
|
|
262
268
|
)
|
|
263
269
|
}
|
|
264
270
|
|
|
@@ -344,7 +350,9 @@ mod tests {
|
|
|
344
350
|
"Do not reply to pure ACKs, greetings, or unchanged status notices (such as \"paused\" or \"waiting\"); after reporting a blocker once or completing a task, remain silent until a new actionable instruction arrives."
|
|
345
351
|
));
|
|
346
352
|
assert!(!prompt.contains("Silence and resumption (mandatory)"));
|
|
347
|
-
assert!(!prompt.contains(
|
|
353
|
+
assert!(!prompt.contains(
|
|
354
|
+
"When you receive a message from the leader or a teammate, you MUST respond"
|
|
355
|
+
));
|
|
348
356
|
match mode {
|
|
349
357
|
CommunicationMode::LeaderCentric => {
|
|
350
358
|
assert!(prompt.contains(
|
|
@@ -352,7 +360,9 @@ mod tests {
|
|
|
352
360
|
));
|
|
353
361
|
}
|
|
354
362
|
CommunicationMode::Orchestrated => {
|
|
355
|
-
assert!(prompt.contains(
|
|
363
|
+
assert!(prompt.contains(
|
|
364
|
+
"Respond to task-related messages through Team Agent MCP tools."
|
|
365
|
+
));
|
|
356
366
|
assert!(prompt.contains("A pure ACK, unrelated status, or non-task message does not require a response."));
|
|
357
367
|
}
|
|
358
368
|
}
|
|
@@ -5,6 +5,12 @@ fn main() -> anyhow::Result<()> {
|
|
|
5
5
|
.skip(1)
|
|
6
6
|
.map(|arg| arg.to_string_lossy().into_owned());
|
|
7
7
|
let command = args.next();
|
|
8
|
+
#[cfg(unix)]
|
|
9
|
+
if matches!(command.as_deref(), Some("contract-mcp")) {
|
|
10
|
+
team_agent::contract_runtime::mcp::run(&args.collect::<Vec<_>>())
|
|
11
|
+
.map_err(|error| anyhow::anyhow!(error.to_string()))?;
|
|
12
|
+
return Ok(());
|
|
13
|
+
}
|
|
8
14
|
if matches!(command.as_deref(), Some("fake-worker")) {
|
|
9
15
|
let mut workspace = None;
|
|
10
16
|
let mut agent_id = None;
|
|
@@ -1926,7 +1926,11 @@ fn delivery_transport_for_recipient<'a>(
|
|
|
1926
1926
|
}
|
|
1927
1927
|
return DeliveryTransport::Borrowed(product_transport);
|
|
1928
1928
|
};
|
|
1929
|
-
|
|
1929
|
+
// An exact endpoint match can reuse the supplied channel; do not select
|
|
1930
|
+
// another server merely because it has a pane with the same numeric id.
|
|
1931
|
+
if product_transport.tmux_endpoint().as_deref() == Some(socket)
|
|
1932
|
+
|| socket == crate::tmux_backend::socket_name_for_workspace(workspace)
|
|
1933
|
+
{
|
|
1930
1934
|
DeliveryTransport::Borrowed(product_transport)
|
|
1931
1935
|
} else {
|
|
1932
1936
|
// 0.5.4 gate054 cross-team leak fix: when `leader_receiver.tmux_socket`
|
|
@@ -2523,6 +2527,51 @@ pub(crate) fn recipient_is_busy(state: &serde_json::Value, recipient: &str) -> b
|
|
|
2523
2527
|
}
|
|
2524
2528
|
|
|
2525
2529
|
fn leader_rollout_path(state: &serde_json::Value) -> Option<std::path::PathBuf> {
|
|
2530
|
+
if let Some(receiver) = leader_receiver_value(state).filter(|receiver| {
|
|
2531
|
+
receiver.get("provider").and_then(serde_json::Value::as_str) == Some("pi")
|
|
2532
|
+
&& receiver.get("native_session").is_some()
|
|
2533
|
+
}) {
|
|
2534
|
+
let native = receiver.get("native_session")?;
|
|
2535
|
+
for key in ["pane_id", "tmux_socket", "leader_session_uuid", "owner_epoch"] {
|
|
2536
|
+
let bound = native.get(key)?;
|
|
2537
|
+
if bound.is_null() || receiver.get(key) != Some(bound) {
|
|
2538
|
+
return None;
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
// Managed Pi's native ID is distinct from leader_session_uuid.
|
|
2542
|
+
// The JSONL appears asynchronously. Resolve only inside the recorded
|
|
2543
|
+
// root, by exact id/cwd/spawn time; ambiguity or an unknown
|
|
2544
|
+
// NativeDefault root stays unavailable (no HOME scan).
|
|
2545
|
+
let context = crate::provider::CaptureSessionContext {
|
|
2546
|
+
agent_id: "leader".into(),
|
|
2547
|
+
spawn_cwd: native.get("spawn_cwd")?.as_str()?.into(),
|
|
2548
|
+
pane_id: None,
|
|
2549
|
+
pane_pid: None,
|
|
2550
|
+
spawned_at: Some(native.get("spawned_at")?.as_str()?.into()),
|
|
2551
|
+
expected_session_id: Some(crate::provider::SessionId::new(
|
|
2552
|
+
native.get("expected_session_id")?.as_str()?,
|
|
2553
|
+
)),
|
|
2554
|
+
provider_projects_root: Some(native.get("provider_projects_root")?.as_str()?.into()),
|
|
2555
|
+
};
|
|
2556
|
+
if !context.spawn_cwd.is_absolute()
|
|
2557
|
+
|| !context.provider_projects_root.as_ref()?.is_absolute()
|
|
2558
|
+
{
|
|
2559
|
+
return None;
|
|
2560
|
+
}
|
|
2561
|
+
let candidates = crate::provider::session_scan::scan_session_candidates_once(
|
|
2562
|
+
Provider::Pi,
|
|
2563
|
+
&context,
|
|
2564
|
+
)
|
|
2565
|
+
.ok()?;
|
|
2566
|
+
return match candidates.as_slice() {
|
|
2567
|
+
[candidate] => candidate
|
|
2568
|
+
.captured
|
|
2569
|
+
.rollout_path
|
|
2570
|
+
.as_ref()
|
|
2571
|
+
.map(|path| path.as_path().to_path_buf()),
|
|
2572
|
+
_ => None,
|
|
2573
|
+
};
|
|
2574
|
+
}
|
|
2526
2575
|
state
|
|
2527
2576
|
.get("leader")
|
|
2528
2577
|
.and_then(|leader| leader.get("rollout_path"))
|
|
@@ -265,6 +265,16 @@ pub fn send_message(
|
|
|
265
265
|
}
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
|
+
#[cfg(unix)]
|
|
269
|
+
if state
|
|
270
|
+
.get("agents")
|
|
271
|
+
.and_then(|agents| agents.get(recipient))
|
|
272
|
+
.is_some_and(crate::contract_runtime::framework::is_contract)
|
|
273
|
+
{
|
|
274
|
+
return crate::contract_runtime::framework::enqueue(
|
|
275
|
+
workspace, &state, recipient, content, opts,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
268
278
|
let stale_worker_target_missing =
|
|
269
279
|
stale_worker_target_missing_preflight(workspace, &state, recipient)?;
|
|
270
280
|
let coordinator_unavailable = if stale_worker_target_missing {
|
|
@@ -77,6 +77,131 @@ fn tmux_leader_submit_without_provider_receipt_stays_pending_then_accepts_same_r
|
|
|
77
77
|
assert!(case.delivery_consumed_at(&message_id).is_some());
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
fn pi_receipt_case(tag: &str) -> (Case, serde_json::Value) {
|
|
81
|
+
let mut case = Case::new_without_rollout(tag);
|
|
82
|
+
let root = case.workspace.join("pi-sessions");
|
|
83
|
+
std::fs::create_dir_all(&root).unwrap();
|
|
84
|
+
case.rollout = root.join("not-derived-from-the-session-id.jsonl");
|
|
85
|
+
let receiver = &mut case.state["leader_receiver"];
|
|
86
|
+
receiver["provider"] = serde_json::json!("pi");
|
|
87
|
+
receiver["tmux_socket"] = serde_json::json!(case.workspace.join("fixture-tmux.sock"));
|
|
88
|
+
receiver["leader_session_uuid"] = serde_json::json!("team-owner-not-native-id");
|
|
89
|
+
receiver["owner_epoch"] = serde_json::json!(1);
|
|
90
|
+
receiver["native_session"] = serde_json::json!({
|
|
91
|
+
"expected_session_id": "native-pi-session",
|
|
92
|
+
"provider_projects_root": root,
|
|
93
|
+
"spawn_cwd": case.workspace,
|
|
94
|
+
"spawned_at": "2026-10-10T03:59:07Z",
|
|
95
|
+
"pane_id": "%leader",
|
|
96
|
+
"tmux_socket": receiver["tmux_socket"],
|
|
97
|
+
"leader_session_uuid": "team-owner-not-native-id",
|
|
98
|
+
"owner_epoch": 1,
|
|
99
|
+
});
|
|
100
|
+
crate::state::persist::save_runtime_state(&case.workspace, &case.state).unwrap();
|
|
101
|
+
let header = serde_json::json!({
|
|
102
|
+
"type": "session", "version": 3, "id": "native-pi-session",
|
|
103
|
+
"cwd": case.workspace, "timestamp": "2026-10-10T03:59:08Z",
|
|
104
|
+
});
|
|
105
|
+
(case, header)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[test]
|
|
109
|
+
fn pi_leader_receipt_binds_exact_header_and_consumes_without_reinjection() {
|
|
110
|
+
let (case, header) = pi_receipt_case("pi-native-receipt");
|
|
111
|
+
let endpoint = case.state["leader_receiver"]["tmux_socket"].as_str().unwrap();
|
|
112
|
+
assert!(Path::new(endpoint).is_absolute());
|
|
113
|
+
let unbound = crate::messaging::resolve_live_leader_channel(
|
|
114
|
+
&case.workspace,
|
|
115
|
+
&case.state["leader_receiver"],
|
|
116
|
+
&RecordingTransport::new(""),
|
|
117
|
+
);
|
|
118
|
+
assert!(
|
|
119
|
+
matches!(
|
|
120
|
+
unbound,
|
|
121
|
+
crate::messaging::LeaderChannelResolution::Unbound(
|
|
122
|
+
crate::messaging::LeaderChannelUnbound::EndpointMismatch
|
|
123
|
+
)
|
|
124
|
+
),
|
|
125
|
+
"{unbound:?}"
|
|
126
|
+
);
|
|
127
|
+
let transport = RecordingTransport {
|
|
128
|
+
endpoint: Some(endpoint.to_string()),
|
|
129
|
+
..RecordingTransport::new("")
|
|
130
|
+
};
|
|
131
|
+
let message_id = case.seed_message("Pi receipt canary");
|
|
132
|
+
std::fs::write(&case.rollout, format!("{header}\n")).unwrap();
|
|
133
|
+
let first = deliver_pending_message(
|
|
134
|
+
&case.workspace,
|
|
135
|
+
&case.store,
|
|
136
|
+
&transport,
|
|
137
|
+
&message_id,
|
|
138
|
+
&case.event_log,
|
|
139
|
+
&case.state,
|
|
140
|
+
)
|
|
141
|
+
.unwrap();
|
|
142
|
+
assert!(first.ok, "{first:?}");
|
|
143
|
+
assert_eq!(
|
|
144
|
+
case.message_status(&message_id),
|
|
145
|
+
"submitted_pending_acceptance"
|
|
146
|
+
);
|
|
147
|
+
assert_eq!(transport.inject_count(), 1);
|
|
148
|
+
|
|
149
|
+
let received = serde_json::json!({
|
|
150
|
+
"type": "message", "message": {"role": "user", "content": [
|
|
151
|
+
{"type": "text", "text": format!("5535 [team-agent-token:{message_id}]")}
|
|
152
|
+
]}
|
|
153
|
+
});
|
|
154
|
+
std::fs::write(&case.rollout, format!("{header}\n{received}\n")).unwrap();
|
|
155
|
+
let second = deliver_pending_message(
|
|
156
|
+
&case.workspace,
|
|
157
|
+
&case.store,
|
|
158
|
+
&transport,
|
|
159
|
+
&message_id,
|
|
160
|
+
&case.event_log,
|
|
161
|
+
&case.state,
|
|
162
|
+
)
|
|
163
|
+
.unwrap();
|
|
164
|
+
assert!(second.ok, "{second:?}");
|
|
165
|
+
assert_eq!(case.message_status(&message_id), "delivered");
|
|
166
|
+
assert!(case.delivery_consumed_at(&message_id).is_some());
|
|
167
|
+
assert_eq!(transport.inject_count(), 1);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[test]
|
|
171
|
+
fn pi_leader_receipt_refuses_missing_ambiguous_stale_and_other_owner_backing() {
|
|
172
|
+
use crate::messaging::delivery::{observe_leader_receipt, LeaderReceiptObservation::*};
|
|
173
|
+
let (mut case, header) = pi_receipt_case("pi-native-receipt-fences");
|
|
174
|
+
let old = case.workspace.join("old-leader.jsonl");
|
|
175
|
+
std::fs::write(&old, "[team-agent-token:canary]").unwrap();
|
|
176
|
+
case.state["leader"] = serde_json::json!({"rollout_path": old});
|
|
177
|
+
// An old root leader receipt cannot substitute for this native session.
|
|
178
|
+
assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
|
|
179
|
+
for (field, value) in [
|
|
180
|
+
("id", serde_json::json!("another-session")),
|
|
181
|
+
("cwd", serde_json::json!(case.workspace.join("other"))),
|
|
182
|
+
("timestamp", serde_json::json!("2026-10-10T03:59:06Z")),
|
|
183
|
+
] {
|
|
184
|
+
let mut wrong = header.clone();
|
|
185
|
+
wrong[field] = value;
|
|
186
|
+
std::fs::write(&case.rollout, format!("{wrong}\n[team-agent-token:canary]\n")).unwrap();
|
|
187
|
+
assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
|
|
188
|
+
}
|
|
189
|
+
std::fs::write(&case.rollout, format!("{header}\n")).unwrap();
|
|
190
|
+
assert_eq!(observe_leader_receipt(&case.state, "canary"), TokenAbsent);
|
|
191
|
+
let duplicate = case.rollout.with_file_name("duplicate.jsonl");
|
|
192
|
+
std::fs::write(&duplicate, format!("{header}\n[team-agent-token:canary]\n")).unwrap();
|
|
193
|
+
assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
|
|
194
|
+
std::fs::remove_file(duplicate).unwrap();
|
|
195
|
+
for key in ["pane_id", "tmux_socket", "leader_session_uuid", "owner_epoch"] {
|
|
196
|
+
let mut moved = case.state.clone();
|
|
197
|
+
moved["leader_receiver"][key] = serde_json::json!("changed");
|
|
198
|
+
assert_eq!(observe_leader_receipt(&moved, "canary"), SourceUnavailable);
|
|
199
|
+
}
|
|
200
|
+
case.state["leader_receiver"]["native_session"]["provider_projects_root"] =
|
|
201
|
+
serde_json::Value::Null;
|
|
202
|
+
assert_eq!(observe_leader_receipt(&case.state, "canary"), SourceUnavailable);
|
|
203
|
+
}
|
|
204
|
+
|
|
80
205
|
#[test]
|
|
81
206
|
fn attached_leader_mailbox_is_rechecked_by_the_normal_delivery_tick() {
|
|
82
207
|
let case = Case::new("mailbox");
|
|
@@ -428,6 +553,7 @@ impl Case {
|
|
|
428
553
|
struct RecordingTransport {
|
|
429
554
|
injects: Arc<AtomicUsize>,
|
|
430
555
|
capture: Arc<Mutex<String>>,
|
|
556
|
+
endpoint: Option<String>,
|
|
431
557
|
}
|
|
432
558
|
|
|
433
559
|
impl RecordingTransport {
|
|
@@ -435,6 +561,7 @@ impl RecordingTransport {
|
|
|
435
561
|
Self {
|
|
436
562
|
injects: Arc::new(AtomicUsize::new(0)),
|
|
437
563
|
capture: Arc::new(Mutex::new(capture.to_string())),
|
|
564
|
+
endpoint: None,
|
|
438
565
|
}
|
|
439
566
|
}
|
|
440
567
|
|
|
@@ -444,6 +571,10 @@ impl RecordingTransport {
|
|
|
444
571
|
}
|
|
445
572
|
|
|
446
573
|
impl Transport for RecordingTransport {
|
|
574
|
+
fn tmux_endpoint(&self) -> Option<String> {
|
|
575
|
+
self.endpoint.clone()
|
|
576
|
+
}
|
|
577
|
+
|
|
447
578
|
fn kind(&self) -> BackendKind {
|
|
448
579
|
BackendKind::Tmux
|
|
449
580
|
}
|