@team-agent/installer 0.3.9 → 0.3.11
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/send.rs +9 -2
- package/crates/team-agent/src/coordinator/backoff.rs +83 -2
- package/crates/team-agent/src/coordinator/tests/spine.rs +6 -0
- package/crates/team-agent/src/coordinator/tick.rs +410 -168
- package/crates/team-agent/src/leader/lease.rs +19 -0
- package/crates/team-agent/src/leader/rediscover/tests.rs +12 -0
- package/crates/team-agent/src/leader/rediscover.rs +2 -0
- package/crates/team-agent/src/lifecycle/launch.rs +35 -0
- package/crates/team-agent/src/lifecycle/restart/agent.rs +17 -3
- package/crates/team-agent/src/lifecycle/restart/common.rs +75 -0
- package/crates/team-agent/src/lifecycle/restart/rebuild.rs +201 -3
- package/crates/team-agent/src/lifecycle/restart/selection.rs +51 -14
- package/crates/team-agent/src/lifecycle/restart.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/core.rs +89 -15
- package/crates/team-agent/src/lifecycle/tests/launch_spawn.rs +68 -3
- package/crates/team-agent/src/lifecycle/tests/main_preserved.rs +3 -1
- package/crates/team-agent/src/mcp_server/helpers.rs +24 -5
- package/crates/team-agent/src/mcp_server/normalize.rs +13 -6
- package/crates/team-agent/src/mcp_server/tests/send.rs +310 -212
- package/crates/team-agent/src/messaging/delivery.rs +83 -2
- package/crates/team-agent/src/messaging/helpers.rs +30 -10
- package/crates/team-agent/src/messaging/send.rs +71 -14
- package/crates/team-agent/src/messaging/tests/basic.rs +25 -7
- package/crates/team-agent/src/messaging/tests/runtime.rs +565 -111
- package/crates/team-agent/src/messaging/types.rs +19 -4
- package/crates/team-agent/src/provider/approvals/parsing.rs +43 -14
- package/crates/team-agent/src/provider/approvals/runtime_prompts.rs +12 -9
- package/crates/team-agent/src/transport/test_support.rs +12 -1
- package/package.json +4 -4
|
@@ -25,6 +25,7 @@ pub enum DeliveryStatus {
|
|
|
25
25
|
Queued,
|
|
26
26
|
Blocked,
|
|
27
27
|
Refused,
|
|
28
|
+
Degraded,
|
|
28
29
|
RetryScheduled,
|
|
29
30
|
TrustAutoAnswerExhausted,
|
|
30
31
|
AlreadyDelivered,
|
|
@@ -65,6 +66,9 @@ pub enum DeliveryRefusal {
|
|
|
65
66
|
/// swallow batch 3: an explicit empty `--to` target list (a failed send always
|
|
66
67
|
/// carries its reason; an unexplained `failed` is a swallowed error).
|
|
67
68
|
EmptyTargetList,
|
|
69
|
+
/// Coordinator is known unhealthy, so accepting a worker-bound message would
|
|
70
|
+
/// strand it in the store with no delivery tick.
|
|
71
|
+
CoordinatorUnavailable,
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
/// Debug 输出 = wire snake_case 字面(单一真相源 = serde rename),与事件/JSON 面一致,
|
|
@@ -134,7 +138,11 @@ pub enum AlertType {
|
|
|
134
138
|
impl AlertType {
|
|
135
139
|
/// `stuck_cancel(alert_type="all")` → `sorted(_ALERT_TYPES)` 全集 (`scheduler.py:269`)。
|
|
136
140
|
pub fn all() -> [AlertType; 3] {
|
|
137
|
-
[
|
|
141
|
+
[
|
|
142
|
+
AlertType::CrossWorkerDeadlock,
|
|
143
|
+
AlertType::IdleFallback,
|
|
144
|
+
AlertType::Stuck,
|
|
145
|
+
]
|
|
138
146
|
}
|
|
139
147
|
}
|
|
140
148
|
|
|
@@ -175,10 +183,14 @@ pub enum ReceiverMode {
|
|
|
175
183
|
/// 自动应答。故 `Failed` 携带 reason 但**不**携带任何 fallback 宽度。
|
|
176
184
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
177
185
|
pub enum PaneWidthQuery {
|
|
178
|
-
Ok {
|
|
186
|
+
Ok {
|
|
187
|
+
pane_width: u32,
|
|
188
|
+
},
|
|
179
189
|
/// 失败原因 (`tmux_query_failed:<exc>`/`tmux_query_nonzero`/`empty_output`/
|
|
180
190
|
/// `unparseable_output`/`non_positive_width`)。**无默认宽度** (fail-safe)。
|
|
181
|
-
Failed {
|
|
191
|
+
Failed {
|
|
192
|
+
error: String,
|
|
193
|
+
},
|
|
182
194
|
}
|
|
183
195
|
|
|
184
196
|
// ===========================================================================
|
|
@@ -243,7 +255,10 @@ pub enum CheckEvidence {
|
|
|
243
255
|
/// `no_provider_sdk_calls` 的机械证据 (§84):三 SDK 调用计数。
|
|
244
256
|
ProviderSdkCalls(ProviderSdkCalls),
|
|
245
257
|
/// binding 一致性比对结果 (mismatch 列表)。
|
|
246
|
-
Binding {
|
|
258
|
+
Binding {
|
|
259
|
+
mismatches: Vec<String>,
|
|
260
|
+
details: serde_json::Value,
|
|
261
|
+
},
|
|
247
262
|
/// executable zero-token comms contract suite evidence.
|
|
248
263
|
ContractSuite { checks: Vec<ContractSuiteCheck> },
|
|
249
264
|
}
|
|
@@ -10,6 +10,8 @@ pub struct ApprovalPrompt {
|
|
|
10
10
|
pub prompt: String,
|
|
11
11
|
pub choices: Vec<String>,
|
|
12
12
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
13
|
+
pub server: Option<String>,
|
|
14
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
13
15
|
pub tool: Option<String>,
|
|
14
16
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
15
17
|
pub command: Option<String>,
|
|
@@ -21,6 +23,9 @@ impl ApprovalPrompt {
|
|
|
21
23
|
map.insert("agent_id".to_string(), serde_json::json!(self.agent_id));
|
|
22
24
|
map.insert("state".to_string(), serde_json::json!(self.state));
|
|
23
25
|
map.insert("kind".to_string(), serde_json::json!(self.kind));
|
|
26
|
+
if let Some(server) = &self.server {
|
|
27
|
+
map.insert("server".to_string(), serde_json::json!(server));
|
|
28
|
+
}
|
|
24
29
|
if let Some(tool) = &self.tool {
|
|
25
30
|
map.insert("tool".to_string(), serde_json::json!(tool));
|
|
26
31
|
}
|
|
@@ -54,17 +59,17 @@ pub fn extract_approval_prompt(agent_id: &str, capture: &str) -> Option<Approval
|
|
|
54
59
|
let lines: Vec<&str> = capture.lines().collect();
|
|
55
60
|
let control_idx = active_approval_control_index(&lines)?;
|
|
56
61
|
|
|
57
|
-
if let Some((matched_idx, prompt, tool)) = explicit_mcp_prompt(&lines, control_idx) {
|
|
62
|
+
if let Some((matched_idx, prompt, server, tool)) = explicit_mcp_prompt(&lines, control_idx) {
|
|
58
63
|
let choices = approval_choices_between(&lines, matched_idx, control_idx);
|
|
59
|
-
return Some(approval(agent_id, ApprovalKind::McpTool, prompt, choices, Some(tool), None));
|
|
64
|
+
return Some(approval(agent_id, ApprovalKind::McpTool, prompt, choices, Some(server), Some(tool), None));
|
|
60
65
|
}
|
|
61
|
-
if let Some((matched_idx, prompt, tool)) = short_mcp_prompt(&lines, control_idx) {
|
|
66
|
+
if let Some((matched_idx, prompt, server, tool)) = short_mcp_prompt(&lines, control_idx) {
|
|
62
67
|
let choices = approval_choices_between(&lines, matched_idx, control_idx);
|
|
63
|
-
return Some(approval(agent_id, ApprovalKind::McpTool, prompt, choices, Some(tool), None));
|
|
68
|
+
return Some(approval(agent_id, ApprovalKind::McpTool, prompt, choices, Some(server), Some(tool), None));
|
|
64
69
|
}
|
|
65
70
|
if let Some((matched_idx, prompt, command)) = command_prompt(&lines, control_idx) {
|
|
66
71
|
let choices = approval_choices_between(&lines, matched_idx, control_idx);
|
|
67
|
-
return Some(approval(agent_id, ApprovalKind::Command, prompt, choices, None, Some(command)));
|
|
72
|
+
return Some(approval(agent_id, ApprovalKind::Command, prompt, choices, None, None, Some(command)));
|
|
68
73
|
}
|
|
69
74
|
let choices = approval_choices_between(&lines, 0, control_idx);
|
|
70
75
|
Some(approval(
|
|
@@ -74,6 +79,7 @@ pub fn extract_approval_prompt(agent_id: &str, capture: &str) -> Option<Approval
|
|
|
74
79
|
choices,
|
|
75
80
|
None,
|
|
76
81
|
None,
|
|
82
|
+
None,
|
|
77
83
|
))
|
|
78
84
|
}
|
|
79
85
|
|
|
@@ -117,6 +123,7 @@ fn approval(
|
|
|
117
123
|
kind: ApprovalKind,
|
|
118
124
|
prompt: String,
|
|
119
125
|
choices: Vec<String>,
|
|
126
|
+
server: Option<String>,
|
|
120
127
|
tool: Option<String>,
|
|
121
128
|
command: Option<String>,
|
|
122
129
|
) -> ApprovalPrompt {
|
|
@@ -126,6 +133,7 @@ fn approval(
|
|
|
126
133
|
kind,
|
|
127
134
|
prompt,
|
|
128
135
|
choices,
|
|
136
|
+
server,
|
|
129
137
|
tool,
|
|
130
138
|
command,
|
|
131
139
|
}
|
|
@@ -137,15 +145,18 @@ fn is_control_line(line: &str) -> bool {
|
|
|
137
145
|
|| (lower.contains("esc to cancel") && lower.contains("tab to amend"))
|
|
138
146
|
}
|
|
139
147
|
|
|
140
|
-
fn explicit_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, String, String)> {
|
|
148
|
+
fn explicit_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, String, String, String)> {
|
|
141
149
|
for (idx, raw) in lines.iter().enumerate().take(control_idx).rev() {
|
|
142
150
|
let line = clean_line(raw);
|
|
143
|
-
let prefix = "Allow the
|
|
151
|
+
let prefix = "Allow the ";
|
|
144
152
|
if let Some(rest) = line.strip_prefix(prefix) {
|
|
145
|
-
if let Some((
|
|
146
|
-
if
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
if let Some((server, rest)) = rest.split_once(" MCP server to run tool \"") {
|
|
154
|
+
if let Some((tool, _)) = rest.split_once('"') {
|
|
155
|
+
if !server.is_empty() && !tool.is_empty() {
|
|
156
|
+
let server = server.to_string();
|
|
157
|
+
let tool = tool.to_string();
|
|
158
|
+
return Some((idx, line, server, tool));
|
|
159
|
+
}
|
|
149
160
|
}
|
|
150
161
|
}
|
|
151
162
|
}
|
|
@@ -153,17 +164,17 @@ fn explicit_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, Str
|
|
|
153
164
|
None
|
|
154
165
|
}
|
|
155
166
|
|
|
156
|
-
fn short_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, String, String)> {
|
|
167
|
+
fn short_mcp_prompt(lines: &[&str], control_idx: usize) -> Option<(usize, String, String, String)> {
|
|
157
168
|
for (idx, raw) in lines.iter().enumerate().take(control_idx).rev() {
|
|
158
169
|
let line = clean_line(raw);
|
|
159
170
|
if parse_choice_line(&line).is_some() {
|
|
160
171
|
continue;
|
|
161
172
|
}
|
|
162
173
|
if let Some(tool) = team_orchestrator_tool(&line, '-') {
|
|
163
|
-
return Some((idx, format!("team_orchestrator - {tool}"), tool));
|
|
174
|
+
return Some((idx, format!("team_orchestrator - {tool}"), "team_orchestrator".to_string(), tool));
|
|
164
175
|
}
|
|
165
176
|
if let Some(tool) = team_orchestrator_tool(&line, '.') {
|
|
166
|
-
return Some((idx, format!("team_orchestrator - {tool}"), tool));
|
|
177
|
+
return Some((idx, format!("team_orchestrator - {tool}"), "team_orchestrator".to_string(), tool));
|
|
167
178
|
}
|
|
168
179
|
}
|
|
169
180
|
None
|
|
@@ -287,6 +298,7 @@ mod tests {
|
|
|
287
298
|
assert_eq!(prompt.agent_id, "agent-x");
|
|
288
299
|
assert_eq!(prompt.state, "waiting_approval");
|
|
289
300
|
assert_eq!(prompt.kind, ApprovalKind::McpTool);
|
|
301
|
+
assert_eq!(prompt.server.as_deref(), Some("team_orchestrator"));
|
|
290
302
|
assert_eq!(prompt.tool.as_deref(), Some("send_message"));
|
|
291
303
|
assert_eq!(prompt.prompt, "Allow the team_orchestrator MCP server to run tool \"send_message\"?");
|
|
292
304
|
assert_eq!(prompt.choices, vec!["Allow for this session", "Deny"]);
|
|
@@ -300,10 +312,23 @@ mod tests {
|
|
|
300
312
|
)
|
|
301
313
|
.unwrap();
|
|
302
314
|
assert_eq!(prompt.kind, ApprovalKind::McpTool);
|
|
315
|
+
assert_eq!(prompt.server.as_deref(), Some("team_orchestrator"));
|
|
303
316
|
assert_eq!(prompt.tool.as_deref(), Some("assign_task"));
|
|
304
317
|
assert_eq!(prompt.prompt, "team_orchestrator - assign_task");
|
|
305
318
|
}
|
|
306
319
|
|
|
320
|
+
#[test]
|
|
321
|
+
fn explicit_mcp_prompt_preserves_custom_server_name() {
|
|
322
|
+
let prompt = extract_approval_prompt(
|
|
323
|
+
"agent-x",
|
|
324
|
+
"Allow the custom_server MCP server to run tool \"write_file\"?\n 1. Allow\nEnter to submit | Esc to cancel\n",
|
|
325
|
+
)
|
|
326
|
+
.unwrap();
|
|
327
|
+
assert_eq!(prompt.kind, ApprovalKind::McpTool);
|
|
328
|
+
assert_eq!(prompt.server.as_deref(), Some("custom_server"));
|
|
329
|
+
assert_eq!(prompt.tool.as_deref(), Some("write_file"));
|
|
330
|
+
}
|
|
331
|
+
|
|
307
332
|
#[test]
|
|
308
333
|
fn command_prompt_extracts_command_before_prompt() {
|
|
309
334
|
let prompt = extract_approval_prompt(
|
|
@@ -354,6 +379,7 @@ mod tests {
|
|
|
354
379
|
],
|
|
355
380
|
None,
|
|
356
381
|
None,
|
|
382
|
+
None,
|
|
357
383
|
);
|
|
358
384
|
assert_eq!(choose_internal_mcp_approval_choice(&prompt), "Allow for this session");
|
|
359
385
|
prompt.choices = vec!["Maybe".to_string()];
|
|
@@ -374,6 +400,7 @@ mod tests {
|
|
|
374
400
|
vec!["Allow".to_string(), "Deny".to_string()],
|
|
375
401
|
None,
|
|
376
402
|
None,
|
|
403
|
+
None,
|
|
377
404
|
);
|
|
378
405
|
assert_eq!(approval_choice_keys(&prompt, "❯ 1. Allow\n", "Missing"), vec!["Down", "Enter"]);
|
|
379
406
|
assert_eq!(approval_choice_keys(&prompt, " 1. Allow\n 2. Deny\n", "Deny"), vec!["2", "Enter"]);
|
|
@@ -413,6 +440,7 @@ mod tests {
|
|
|
413
440
|
ApprovalKind::McpTool,
|
|
414
441
|
"p".to_string(),
|
|
415
442
|
vec!["Allow".to_string()],
|
|
443
|
+
None,
|
|
416
444
|
Some("send_message".to_string()),
|
|
417
445
|
None,
|
|
418
446
|
)
|
|
@@ -426,6 +454,7 @@ mod tests {
|
|
|
426
454
|
"p".to_string(),
|
|
427
455
|
vec!["Yes".to_string()],
|
|
428
456
|
None,
|
|
457
|
+
None,
|
|
429
458
|
Some("Bash(echo hi)".to_string()),
|
|
430
459
|
)
|
|
431
460
|
.to_ordered_value();
|
|
@@ -5,15 +5,15 @@ use sha2::{Digest, Sha256};
|
|
|
5
5
|
|
|
6
6
|
use crate::provider::{ApprovalFingerprint, ApprovalKind, ApprovalPrompt};
|
|
7
7
|
|
|
8
|
-
pub const
|
|
9
|
-
"send_message",
|
|
10
|
-
"report_result",
|
|
11
|
-
"get_team_status",
|
|
12
|
-
"request_human",
|
|
13
|
-
];
|
|
8
|
+
pub const RUNTIME_MCP_APPROVAL_SERVER: &str = "team_orchestrator";
|
|
14
9
|
|
|
15
10
|
pub fn runtime_mcp_tool_allowlisted(tool: &str) -> bool {
|
|
16
|
-
|
|
11
|
+
!tool.trim().is_empty()
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
pub fn runtime_mcp_prompt_allowlisted(prompt: &ApprovalPrompt) -> bool {
|
|
15
|
+
prompt.server.as_deref() == Some(RUNTIME_MCP_APPROVAL_SERVER)
|
|
16
|
+
&& prompt.tool.as_deref().is_some_and(runtime_mcp_tool_allowlisted)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
@@ -79,8 +79,7 @@ pub fn awaiting_human_confirm_reason(
|
|
|
79
79
|
) -> Option<&'static str> {
|
|
80
80
|
match prompt.kind {
|
|
81
81
|
ApprovalKind::McpTool => {
|
|
82
|
-
|
|
83
|
-
if !runtime_mcp_tool_allowlisted(tool) {
|
|
82
|
+
if !runtime_mcp_prompt_allowlisted(prompt) {
|
|
84
83
|
Some("tool_not_allowlisted")
|
|
85
84
|
} else if !leader_auto_approval_allowed {
|
|
86
85
|
Some("leader_restricted")
|
|
@@ -105,6 +104,10 @@ pub fn approval_prompt_fingerprint(team: &str, agent_id: &str, prompt: &Approval
|
|
|
105
104
|
hasher.update(tool.as_bytes());
|
|
106
105
|
}
|
|
107
106
|
hasher.update([0]);
|
|
107
|
+
if let Some(server) = prompt.server.as_deref() {
|
|
108
|
+
hasher.update(server.as_bytes());
|
|
109
|
+
}
|
|
110
|
+
hasher.update([0]);
|
|
108
111
|
if let Some(command) = prompt.command.as_deref() {
|
|
109
112
|
hasher.update(command.as_bytes());
|
|
110
113
|
}
|
|
@@ -25,6 +25,7 @@ struct OfflineState {
|
|
|
25
25
|
targets: Vec<PaneInfo>,
|
|
26
26
|
windows: Vec<WindowName>,
|
|
27
27
|
pane_presence: BTreeMap<String, bool>,
|
|
28
|
+
spawned_panes_addressable: bool,
|
|
28
29
|
liveness: BTreeMap<String, PaneLiveness>,
|
|
29
30
|
default_liveness: PaneLiveness,
|
|
30
31
|
calls: Vec<&'static str>,
|
|
@@ -41,6 +42,7 @@ impl Default for OfflineState {
|
|
|
41
42
|
targets: Vec::new(),
|
|
42
43
|
windows: Vec::new(),
|
|
43
44
|
pane_presence: BTreeMap::new(),
|
|
45
|
+
spawned_panes_addressable: true,
|
|
44
46
|
liveness: BTreeMap::new(),
|
|
45
47
|
default_liveness: PaneLiveness::Unknown,
|
|
46
48
|
calls: Vec::new(),
|
|
@@ -100,6 +102,11 @@ impl OfflineTransport {
|
|
|
100
102
|
self
|
|
101
103
|
}
|
|
102
104
|
|
|
105
|
+
pub fn with_spawned_panes_addressable(self, present: bool) -> Self {
|
|
106
|
+
self.with_state(|state| state.spawned_panes_addressable = present);
|
|
107
|
+
self
|
|
108
|
+
}
|
|
109
|
+
|
|
103
110
|
pub fn calls(&self) -> Vec<&'static str> {
|
|
104
111
|
self.with_state(|state| state.calls.clone())
|
|
105
112
|
}
|
|
@@ -149,7 +156,11 @@ impl OfflineTransport {
|
|
|
149
156
|
if kind == "spawn_first" && !state.session_absent_after_spawn_first {
|
|
150
157
|
state.session_present = true;
|
|
151
158
|
}
|
|
152
|
-
state.spawns.len().saturating_sub(1)
|
|
159
|
+
let pane_index = state.spawns.len().saturating_sub(1);
|
|
160
|
+
state
|
|
161
|
+
.pane_presence
|
|
162
|
+
.insert(format!("%{pane_index}"), state.spawned_panes_addressable);
|
|
163
|
+
pane_index
|
|
153
164
|
});
|
|
154
165
|
SpawnResult {
|
|
155
166
|
pane_id: PaneId::new(format!("%{pane_index}")),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@team-agent/installer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
4
4
|
"description": "npx installer for Team Agent",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"codex",
|
|
@@ -20,9 +20,9 @@
|
|
|
20
20
|
"team-agent-installer": "npm/install.mjs"
|
|
21
21
|
},
|
|
22
22
|
"optionalDependencies": {
|
|
23
|
-
"@team-agent/cli-darwin-arm64": "0.3.
|
|
24
|
-
"@team-agent/cli-darwin-x64": "0.3.
|
|
25
|
-
"@team-agent/cli-linux-x64": "0.3.
|
|
23
|
+
"@team-agent/cli-darwin-arm64": "0.3.11",
|
|
24
|
+
"@team-agent/cli-darwin-x64": "0.3.11",
|
|
25
|
+
"@team-agent/cli-linux-x64": "0.3.11"
|
|
26
26
|
},
|
|
27
27
|
"scripts": {
|
|
28
28
|
"postinstall": "node npm/bincheck.mjs",
|