@team-agent/installer 0.5.101 → 0.5.103
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/emit.rs +2 -1
- package/crates/team-agent/src/cli/models.rs +92 -226
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/tests/main_preserved.rs +0 -74
- package/crates/team-agent/src/compiler/tests.rs +20 -23
- package/crates/team-agent/src/compiler.rs +49 -65
- package/crates/team-agent/src/coordinator/tests/s3_bounded.rs +16 -48
- package/crates/team-agent/src/leader/start.rs +1 -14
- package/crates/team-agent/src/lifecycle/launch/pi_mcp.rs +37 -161
- package/crates/team-agent/src/lifecycle/launch/spawn.rs +1 -1
- package/crates/team-agent/src/lifecycle/restart/common.rs +1 -1
- package/crates/team-agent/src/lifecycle/tests/host_cotenant_death_p0_contract.rs +0 -28
- package/crates/team-agent/src/lifecycle/tests/pi_compiler_red.rs +11 -9
- package/crates/team-agent/src/lifecycle/tests/pi_dual_entry_red.rs +8 -401
- package/crates/team-agent/src/lifecycle/tests/pi_executable_mcp_red.rs +2 -2
- package/crates/team-agent/src/lifecycle/worker_command_context.rs +22 -8
- package/crates/team-agent/src/mcp_server/helpers.rs +0 -16
- package/crates/team-agent/src/mcp_server/lifecycle_tools/mod.rs +2 -7
- package/crates/team-agent/src/mcp_server/lifecycle_tools/state_status.rs +1 -127
- package/crates/team-agent/src/mcp_server/mod.rs +7 -12
- package/crates/team-agent/src/mcp_server/tests/scoped.rs +1 -132
- package/crates/team-agent/src/mcp_server/tests/send.rs +1 -93
- package/crates/team-agent/src/mcp_server/tests/tools.rs +0 -61
- package/crates/team-agent/src/mcp_server/tests/wire.rs +6 -51
- package/crates/team-agent/src/mcp_server/tests.rs +4 -200
- package/crates/team-agent/src/mcp_server/tools.rs +5 -396
- package/crates/team-agent/src/mcp_server/types.rs +3 -36
- package/crates/team-agent/src/mcp_server/wire.rs +2 -223
- package/crates/team-agent/src/messaging/leader_receiver.rs +1 -1
- package/crates/team-agent/src/messaging/mod.rs +2 -2
- package/crates/team-agent/src/messaging/results.rs +11 -42
- package/crates/team-agent/src/messaging/scheduler.rs +2 -205
- package/crates/team-agent/src/messaging/tests/basic.rs +0 -31
- package/crates/team-agent/src/messaging/tests/main_preserved.rs +0 -73
- package/crates/team-agent/src/messaging/tests/runtime.rs +0 -26
- package/crates/team-agent/src/messaging/types.rs +0 -20
- package/crates/team-agent/src/model/spec.rs +14 -18
- package/crates/team-agent/src/provider/mod.rs +1 -0
- package/crates/team-agent/src/provider/model_catalog.rs +650 -0
- package/crates/team-agent/src/state/repository/tests.rs +1 -16
- package/crates/team-agent/src/state/repository.rs +1 -33
- package/package.json +4 -4
- package/skills/team-agent/SKILL.md +5 -5
- package/skills/team-agent/command-coverage.json +7 -0
- package/skills/team-agent/references/team-agent-operator.md +24 -116
- package/skills/team-agent/references/team-in-team.md +4 -19
- package/crates/team-agent/src/mcp_server/lifecycle_tools/agent_ops.rs +0 -484
|
@@ -48,48 +48,25 @@ pub enum McpError {
|
|
|
48
48
|
// WIRE ENUMS (§19) —散字符串 → 穷尽 enum;契约字节级真相由这些派生。
|
|
49
49
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
50
50
|
|
|
51
|
-
/// The
|
|
52
|
-
///
|
|
53
|
-
/// `TOOLS` wire list returned by `tools/list`) MUST be derived from this enum so
|
|
54
|
-
/// name/inputSchema stay byte-identical to `contracts.py`.
|
|
51
|
+
/// The three MCP tool names. Unknown names map to [`ToolErrorReason::UnknownTool`].
|
|
52
|
+
/// [`tools_contract`] (the `tools/list` payload) is derived from this enum.
|
|
55
53
|
///
|
|
56
54
|
/// [`tools_contract`]: super::tools_contract
|
|
57
55
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
58
56
|
#[serde(rename_all = "snake_case")]
|
|
59
57
|
pub enum McpTool {
|
|
60
|
-
AssignTask,
|
|
61
58
|
SendMessage,
|
|
62
59
|
ReportResult,
|
|
63
|
-
UpdateState,
|
|
64
60
|
GetTeamStatus,
|
|
65
|
-
StopAgent,
|
|
66
|
-
ResetAgent,
|
|
67
|
-
AddAgent,
|
|
68
|
-
CloneAgent,
|
|
69
|
-
ForkAgent,
|
|
70
|
-
RequestHuman,
|
|
71
|
-
StuckList,
|
|
72
|
-
StuckCancel,
|
|
73
61
|
}
|
|
74
62
|
|
|
75
63
|
impl McpTool {
|
|
76
|
-
/// Wire name
|
|
77
|
-
/// `tools/list` and `tools/call`. Parse failure ⇒ [`ToolErrorReason::UnknownTool`].
|
|
64
|
+
/// Wire name used in `tools/list` and `tools/call`.
|
|
78
65
|
pub fn wire_name(self) -> &'static str {
|
|
79
66
|
match self {
|
|
80
|
-
McpTool::AssignTask => "assign_task",
|
|
81
67
|
McpTool::SendMessage => "send_message",
|
|
82
68
|
McpTool::ReportResult => "report_result",
|
|
83
|
-
McpTool::UpdateState => "update_state",
|
|
84
69
|
McpTool::GetTeamStatus => "get_team_status",
|
|
85
|
-
McpTool::StopAgent => "stop_agent",
|
|
86
|
-
McpTool::ResetAgent => "reset_agent",
|
|
87
|
-
McpTool::AddAgent => "add_agent",
|
|
88
|
-
McpTool::CloneAgent => "clone_agent",
|
|
89
|
-
McpTool::ForkAgent => "fork_agent",
|
|
90
|
-
McpTool::RequestHuman => "request_human",
|
|
91
|
-
McpTool::StuckList => "stuck_list",
|
|
92
|
-
McpTool::StuckCancel => "stuck_cancel",
|
|
93
70
|
}
|
|
94
71
|
}
|
|
95
72
|
|
|
@@ -97,19 +74,9 @@ impl McpTool {
|
|
|
97
74
|
/// to [`ToolErrorReason::UnknownTool`], matching `server.py:43`).
|
|
98
75
|
pub fn parse(name: &str) -> Option<Self> {
|
|
99
76
|
match name {
|
|
100
|
-
"assign_task" => Some(McpTool::AssignTask),
|
|
101
77
|
"send_message" => Some(McpTool::SendMessage),
|
|
102
78
|
"report_result" => Some(McpTool::ReportResult),
|
|
103
|
-
"update_state" => Some(McpTool::UpdateState),
|
|
104
79
|
"get_team_status" => Some(McpTool::GetTeamStatus),
|
|
105
|
-
"stop_agent" => Some(McpTool::StopAgent),
|
|
106
|
-
"reset_agent" => Some(McpTool::ResetAgent),
|
|
107
|
-
"add_agent" => Some(McpTool::AddAgent),
|
|
108
|
-
"clone_agent" => Some(McpTool::CloneAgent),
|
|
109
|
-
"fork_agent" => Some(McpTool::ForkAgent),
|
|
110
|
-
"request_human" => Some(McpTool::RequestHuman),
|
|
111
|
-
"stuck_list" => Some(McpTool::StuckList),
|
|
112
|
-
"stuck_cancel" => Some(McpTool::StuckCancel),
|
|
113
80
|
_ => None,
|
|
114
81
|
}
|
|
115
82
|
}
|
|
@@ -25,19 +25,9 @@ use super::types::{
|
|
|
25
25
|
/// schema cannot drift from the enum. Returned verbatim by `tools/list`.
|
|
26
26
|
pub fn tools_contract() -> Vec<Value> {
|
|
27
27
|
let tools = [
|
|
28
|
-
McpTool::AssignTask,
|
|
29
28
|
McpTool::SendMessage,
|
|
30
29
|
McpTool::ReportResult,
|
|
31
|
-
McpTool::UpdateState,
|
|
32
30
|
McpTool::GetTeamStatus,
|
|
33
|
-
McpTool::StopAgent,
|
|
34
|
-
McpTool::ResetAgent,
|
|
35
|
-
McpTool::AddAgent,
|
|
36
|
-
McpTool::CloneAgent,
|
|
37
|
-
McpTool::ForkAgent,
|
|
38
|
-
McpTool::RequestHuman,
|
|
39
|
-
McpTool::StuckList,
|
|
40
|
-
McpTool::StuckCancel,
|
|
41
31
|
];
|
|
42
32
|
tools.into_iter().map(tool_contract).collect()
|
|
43
33
|
}
|
|
@@ -397,21 +387,11 @@ fn tool_contract(tool: McpTool) -> Value {
|
|
|
397
387
|
"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.",
|
|
398
388
|
vec!["to", "content"],
|
|
399
389
|
),
|
|
400
|
-
McpTool::AssignTask => ("Assign or update a task in the team graph and deliver it to its assignee.", vec!["task"]),
|
|
401
390
|
McpTool::ReportResult => (
|
|
402
391
|
"Report task completion with a durable result envelope. Optional presentation routing controls live leader display, not persistence.",
|
|
403
392
|
Vec::new(),
|
|
404
393
|
),
|
|
405
|
-
McpTool::UpdateState => ("Append a note to team state and rewrite team_state.md.", vec!["note"]),
|
|
406
394
|
McpTool::GetTeamStatus => ("Return machine-readable team status.", Vec::new()),
|
|
407
|
-
McpTool::StopAgent => ("Stop a running worker.", vec!["agent_id"]),
|
|
408
|
-
McpTool::ResetAgent => ("Reset one worker to a fresh session.", vec!["agent_id", "discard_session"]),
|
|
409
|
-
McpTool::AddAgent => ("Add a first-class worker from a role file.", vec!["new_agent_id", "role_file_path"]),
|
|
410
|
-
McpTool::CloneAgent => ("Clone a worker role into a fresh provider session.", vec!["source_agent_id", "as_agent_id"]),
|
|
411
|
-
McpTool::ForkAgent => ("Fork a running worker.", vec!["source_agent_id", "as_agent_id"]),
|
|
412
|
-
McpTool::RequestHuman => ("Ask the leader or user for human input.", vec!["question"]),
|
|
413
|
-
McpTool::StuckList => ("List manually suppressed idle alerts.", Vec::new()),
|
|
414
|
-
McpTool::StuckCancel => ("Suppress repeated stuck or idle alerts.", vec!["agent_id"]),
|
|
415
395
|
};
|
|
416
396
|
serde_json::json!({
|
|
417
397
|
"name": tool.wire_name(),
|
|
@@ -428,14 +408,6 @@ fn tool_contract(tool: McpTool) -> Value {
|
|
|
428
408
|
fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
|
|
429
409
|
let mut properties = serde_json::Map::new();
|
|
430
410
|
match tool {
|
|
431
|
-
McpTool::AssignTask => {
|
|
432
|
-
insert_property(&mut properties, "task", task_property());
|
|
433
|
-
insert_property(
|
|
434
|
-
&mut properties,
|
|
435
|
-
"message",
|
|
436
|
-
string_property("Optional message to deliver with the task."),
|
|
437
|
-
);
|
|
438
|
-
}
|
|
439
411
|
McpTool::SendMessage => {
|
|
440
412
|
insert_property(
|
|
441
413
|
&mut properties,
|
|
@@ -506,108 +478,7 @@ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
|
|
|
506
478
|
presentation_property("Optional durable presentation routing."),
|
|
507
479
|
);
|
|
508
480
|
}
|
|
509
|
-
McpTool::
|
|
510
|
-
insert_property(
|
|
511
|
-
&mut properties,
|
|
512
|
-
"note",
|
|
513
|
-
string_property("Note to append to team state."),
|
|
514
|
-
);
|
|
515
|
-
}
|
|
516
|
-
McpTool::GetTeamStatus | McpTool::StuckList => {}
|
|
517
|
-
McpTool::StopAgent => {
|
|
518
|
-
insert_property(
|
|
519
|
-
&mut properties,
|
|
520
|
-
"agent_id",
|
|
521
|
-
string_property("Agent id to stop."),
|
|
522
|
-
);
|
|
523
|
-
}
|
|
524
|
-
McpTool::ResetAgent => {
|
|
525
|
-
insert_property(
|
|
526
|
-
&mut properties,
|
|
527
|
-
"agent_id",
|
|
528
|
-
string_property("Agent id to reset."),
|
|
529
|
-
);
|
|
530
|
-
insert_property(
|
|
531
|
-
&mut properties,
|
|
532
|
-
"discard_session",
|
|
533
|
-
boolean_property("Whether to discard the existing provider session."),
|
|
534
|
-
);
|
|
535
|
-
}
|
|
536
|
-
McpTool::AddAgent => {
|
|
537
|
-
insert_property(
|
|
538
|
-
&mut properties,
|
|
539
|
-
"new_agent_id",
|
|
540
|
-
string_property("New agent id."),
|
|
541
|
-
);
|
|
542
|
-
insert_property(
|
|
543
|
-
&mut properties,
|
|
544
|
-
"role_file_path",
|
|
545
|
-
string_property("Workspace-relative role file path."),
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
McpTool::ForkAgent => {
|
|
549
|
-
insert_property(
|
|
550
|
-
&mut properties,
|
|
551
|
-
"source_agent_id",
|
|
552
|
-
string_property("Agent id to fork from."),
|
|
553
|
-
);
|
|
554
|
-
insert_property(
|
|
555
|
-
&mut properties,
|
|
556
|
-
"as_agent_id",
|
|
557
|
-
string_property("Agent id for the forked worker."),
|
|
558
|
-
);
|
|
559
|
-
insert_property(
|
|
560
|
-
&mut properties,
|
|
561
|
-
"label",
|
|
562
|
-
string_property("Optional display label."),
|
|
563
|
-
);
|
|
564
|
-
}
|
|
565
|
-
McpTool::CloneAgent => {
|
|
566
|
-
insert_property(
|
|
567
|
-
&mut properties,
|
|
568
|
-
"source_agent_id",
|
|
569
|
-
string_property("Agent id to clone from."),
|
|
570
|
-
);
|
|
571
|
-
insert_property(
|
|
572
|
-
&mut properties,
|
|
573
|
-
"as_agent_id",
|
|
574
|
-
string_property("Agent id for the cloned worker."),
|
|
575
|
-
);
|
|
576
|
-
insert_property(
|
|
577
|
-
&mut properties,
|
|
578
|
-
"label",
|
|
579
|
-
string_property("Optional display label."),
|
|
580
|
-
);
|
|
581
|
-
}
|
|
582
|
-
McpTool::RequestHuman => {
|
|
583
|
-
insert_property(
|
|
584
|
-
&mut properties,
|
|
585
|
-
"question",
|
|
586
|
-
string_property("Question to ask the human."),
|
|
587
|
-
);
|
|
588
|
-
insert_property(
|
|
589
|
-
&mut properties,
|
|
590
|
-
"task_id",
|
|
591
|
-
string_property("Optional related task id."),
|
|
592
|
-
);
|
|
593
|
-
insert_property(
|
|
594
|
-
&mut properties,
|
|
595
|
-
"agent_id",
|
|
596
|
-
string_property("Optional requesting agent id."),
|
|
597
|
-
);
|
|
598
|
-
}
|
|
599
|
-
McpTool::StuckCancel => {
|
|
600
|
-
insert_property(
|
|
601
|
-
&mut properties,
|
|
602
|
-
"agent_id",
|
|
603
|
-
string_property("Agent id whose stuck alerts should be suppressed."),
|
|
604
|
-
);
|
|
605
|
-
insert_property(
|
|
606
|
-
&mut properties,
|
|
607
|
-
"alert_type",
|
|
608
|
-
string_property("Alert type to suppress, or all."),
|
|
609
|
-
);
|
|
610
|
-
}
|
|
481
|
+
McpTool::GetTeamStatus => {}
|
|
611
482
|
}
|
|
612
483
|
properties
|
|
613
484
|
}
|
|
@@ -628,25 +499,6 @@ fn object_property(description: &str) -> Value {
|
|
|
628
499
|
serde_json::json!({"type": "object", "description": description, "additionalProperties": true})
|
|
629
500
|
}
|
|
630
501
|
|
|
631
|
-
fn task_property() -> Value {
|
|
632
|
-
serde_json::json!({
|
|
633
|
-
"type": "object",
|
|
634
|
-
"description": "Task object to add or update.",
|
|
635
|
-
"properties": {
|
|
636
|
-
"result_route": {
|
|
637
|
-
"type": "string",
|
|
638
|
-
"enum": crate::messaging::results::ResultRoute::ALL
|
|
639
|
-
.iter()
|
|
640
|
-
.map(|route| route.as_str())
|
|
641
|
-
.collect::<Vec<_>>(),
|
|
642
|
-
"default": crate::messaging::results::ResultRoute::default().as_str(),
|
|
643
|
-
"description": "Result destination chosen by the task assigner."
|
|
644
|
-
}
|
|
645
|
-
},
|
|
646
|
-
"additionalProperties": true
|
|
647
|
-
})
|
|
648
|
-
}
|
|
649
|
-
|
|
650
502
|
fn presentation_property(description: &str) -> Value {
|
|
651
503
|
serde_json::json!({
|
|
652
504
|
"type": "object",
|
|
@@ -690,14 +542,8 @@ pub(crate) fn dispatch_tool(
|
|
|
690
542
|
}
|
|
691
543
|
_ => {}
|
|
692
544
|
}
|
|
693
|
-
|
|
694
|
-
tools.validate_rpc_scope_args(tool.wire_name(), args)?;
|
|
695
|
-
}
|
|
545
|
+
tools.validate_rpc_scope_args(tool.wire_name(), args)?;
|
|
696
546
|
match tool {
|
|
697
|
-
McpTool::AssignTask => tools.assign_task(
|
|
698
|
-
args.get("task").unwrap_or(args),
|
|
699
|
-
args.get("message").and_then(Value::as_str),
|
|
700
|
-
),
|
|
701
547
|
McpTool::SendMessage => {
|
|
702
548
|
let target = message_target_from_value(args.get("to"));
|
|
703
549
|
let content = args.get("content").and_then(Value::as_str).unwrap_or("");
|
|
@@ -740,77 +586,10 @@ pub(crate) fn dispatch_tool(
|
|
|
740
586
|
args.get("agent_id").and_then(Value::as_str),
|
|
741
587
|
args.get("presentation"),
|
|
742
588
|
),
|
|
743
|
-
McpTool::UpdateState => {
|
|
744
|
-
tools.update_state(args.get("note").and_then(Value::as_str).unwrap_or(""))
|
|
745
|
-
}
|
|
746
589
|
McpTool::GetTeamStatus => tools.get_team_status(),
|
|
747
|
-
McpTool::StopAgent => {
|
|
748
|
-
tools.stop_agent(args.get("agent_id").and_then(Value::as_str).unwrap_or(""))
|
|
749
|
-
}
|
|
750
|
-
McpTool::ResetAgent => tools.reset_agent(
|
|
751
|
-
args.get("agent_id").and_then(Value::as_str).unwrap_or(""),
|
|
752
|
-
args.get("discard_session")
|
|
753
|
-
.and_then(Value::as_bool)
|
|
754
|
-
.unwrap_or(false),
|
|
755
|
-
),
|
|
756
|
-
McpTool::AddAgent => tools.add_agent(
|
|
757
|
-
args.get("new_agent_id")
|
|
758
|
-
.and_then(Value::as_str)
|
|
759
|
-
.unwrap_or(""),
|
|
760
|
-
args.get("role_file_path")
|
|
761
|
-
.and_then(Value::as_str)
|
|
762
|
-
.unwrap_or(""),
|
|
763
|
-
),
|
|
764
|
-
McpTool::ForkAgent => tools.fork_agent(
|
|
765
|
-
args.get("source_agent_id")
|
|
766
|
-
.and_then(Value::as_str)
|
|
767
|
-
.unwrap_or(""),
|
|
768
|
-
args.get("as_agent_id")
|
|
769
|
-
.and_then(Value::as_str)
|
|
770
|
-
.unwrap_or(""),
|
|
771
|
-
args.get("label").and_then(Value::as_str),
|
|
772
|
-
),
|
|
773
|
-
McpTool::CloneAgent => tools.clone_agent(
|
|
774
|
-
args.get("source_agent_id")
|
|
775
|
-
.and_then(Value::as_str)
|
|
776
|
-
.unwrap_or(""),
|
|
777
|
-
args.get("as_agent_id")
|
|
778
|
-
.and_then(Value::as_str)
|
|
779
|
-
.unwrap_or(""),
|
|
780
|
-
args.get("label").and_then(Value::as_str),
|
|
781
|
-
),
|
|
782
|
-
McpTool::RequestHuman => tools.request_human(
|
|
783
|
-
args.get("question").and_then(Value::as_str).unwrap_or(""),
|
|
784
|
-
args.get("task_id").and_then(Value::as_str),
|
|
785
|
-
args.get("agent_id").and_then(Value::as_str),
|
|
786
|
-
),
|
|
787
|
-
McpTool::StuckList => tools.stuck_list(),
|
|
788
|
-
McpTool::StuckCancel => tools.stuck_cancel(
|
|
789
|
-
args.get("agent_id").and_then(Value::as_str).unwrap_or(""),
|
|
790
|
-
// tools.py:351 — the MCP default alert_type is "stuck", not "all".
|
|
791
|
-
args.get("alert_type")
|
|
792
|
-
.and_then(Value::as_str)
|
|
793
|
-
.unwrap_or("stuck"),
|
|
794
|
-
),
|
|
795
590
|
}
|
|
796
591
|
}
|
|
797
592
|
|
|
798
|
-
fn scope_ceiling_tool(tool: McpTool) -> bool {
|
|
799
|
-
matches!(
|
|
800
|
-
tool,
|
|
801
|
-
McpTool::SendMessage
|
|
802
|
-
| McpTool::ReportResult
|
|
803
|
-
| McpTool::RequestHuman
|
|
804
|
-
| McpTool::AssignTask
|
|
805
|
-
| McpTool::UpdateState
|
|
806
|
-
| McpTool::GetTeamStatus
|
|
807
|
-
| McpTool::StopAgent
|
|
808
|
-
| McpTool::ResetAgent
|
|
809
|
-
| McpTool::CloneAgent
|
|
810
|
-
| McpTool::ForkAgent
|
|
811
|
-
)
|
|
812
|
-
}
|
|
813
|
-
|
|
814
593
|
fn message_target_from_value(value: Option<&Value>) -> MessageTarget {
|
|
815
594
|
match value {
|
|
816
595
|
Some(Value::String(s)) if s == "*" => MessageTarget::Broadcast,
|
|
@@ -22,7 +22,7 @@ use super::{
|
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
/// `_send_to_leader_receiver` (`leader.py:69`) — **N31/N32 funnel primitive**:所有 leader-bound
|
|
25
|
-
/// caller(send_message(to=leader) / report_result /
|
|
25
|
+
/// caller(send_message(to=leader) / report_result / idle reminder /
|
|
26
26
|
/// broadcast-to-leader / peer-mirror / worker.abnormal_exit)统一经过这里。
|
|
27
27
|
///
|
|
28
28
|
/// 职责 = create_message + leader_notification_log dedup(result_id 时)+ audit + emit
|
|
@@ -105,14 +105,14 @@ pub use results::{
|
|
|
105
105
|
collect, collect_for_team, report_result, report_result_for_owner_team,
|
|
106
106
|
report_result_for_owner_team_with_primary_error,
|
|
107
107
|
};
|
|
108
|
-
pub use scheduler::{detect_stuck_agents, fire_due_scheduled_events
|
|
108
|
+
pub use scheduler::{detect_stuck_agents, fire_due_scheduled_events};
|
|
109
109
|
pub use selftest::{run_comms_selftest, CommsSelftestDriver};
|
|
110
110
|
pub use send::{
|
|
111
111
|
apply_worker_sender_bypass, send_message, MessageTarget, SendOptions, TrustedSender,
|
|
112
112
|
};
|
|
113
113
|
pub use trust::{attempt_trust_auto_answer, TrustAnswerOutcome};
|
|
114
114
|
pub use types::{
|
|
115
|
-
ActivityStatus, AgentActivity,
|
|
115
|
+
ActivityStatus, AgentActivity, CheckEvidence, CheckKind, CheckStatus,
|
|
116
116
|
ContractSuiteCheck, DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus,
|
|
117
117
|
IdleEvaluation, LeaderNotificationKey, LeaderReceiver, PaneWidthQuery,
|
|
118
118
|
ProviderSdkCalls, ReceiverMode, ScheduledKind, SelftestCheck, SelftestReport, SendEventPayload,
|
|
@@ -1046,25 +1046,6 @@ fn leader_result_presentation(task_id: &str) -> super::presentation::Presentatio
|
|
|
1046
1046
|
)
|
|
1047
1047
|
}
|
|
1048
1048
|
|
|
1049
|
-
const LEADER_NOTIFICATION_LIMIT_BYTES: usize = 160;
|
|
1050
|
-
|
|
1051
|
-
fn truncate_utf8(text: &str, max_bytes: usize) -> String {
|
|
1052
|
-
if text.len() <= max_bytes {
|
|
1053
|
-
return text.to_string();
|
|
1054
|
-
}
|
|
1055
|
-
let suffix = "...";
|
|
1056
|
-
let budget = max_bytes.saturating_sub(suffix.len());
|
|
1057
|
-
let mut end = 0;
|
|
1058
|
-
for (index, ch) in text.char_indices() {
|
|
1059
|
-
let next = index + ch.len_utf8();
|
|
1060
|
-
if next > budget {
|
|
1061
|
-
break;
|
|
1062
|
-
}
|
|
1063
|
-
end = next;
|
|
1064
|
-
}
|
|
1065
|
-
format!("{}{}", &text[..end], suffix)
|
|
1066
|
-
}
|
|
1067
|
-
|
|
1068
1049
|
fn format_report_result_leader_summary(
|
|
1069
1050
|
result_id: &str,
|
|
1070
1051
|
task_id: &str,
|
|
@@ -1075,19 +1056,8 @@ fn format_report_result_leader_summary(
|
|
|
1075
1056
|
let summary = envelope
|
|
1076
1057
|
.get("summary")
|
|
1077
1058
|
.and_then(serde_json::Value::as_str)
|
|
1078
|
-
.unwrap_or("")
|
|
1079
|
-
|
|
1080
|
-
.next()
|
|
1081
|
-
.unwrap_or("")
|
|
1082
|
-
.chars()
|
|
1083
|
-
.filter(|ch| !ch.is_control())
|
|
1084
|
-
.collect::<String>();
|
|
1085
|
-
truncate_utf8(
|
|
1086
|
-
&format!(
|
|
1087
|
-
"Task {task_id} reported {status} from {agent_id}: {summary}; Result id: {result_id}"
|
|
1088
|
-
),
|
|
1089
|
-
LEADER_NOTIFICATION_LIMIT_BYTES,
|
|
1090
|
-
)
|
|
1059
|
+
.unwrap_or("");
|
|
1060
|
+
format!("Task {task_id} reported {status} from {agent_id}: {summary}; Result id: {result_id}")
|
|
1091
1061
|
}
|
|
1092
1062
|
|
|
1093
1063
|
fn auto_finalize_result(
|
|
@@ -1441,18 +1411,17 @@ mod tests {
|
|
|
1441
1411
|
}
|
|
1442
1412
|
|
|
1443
1413
|
#[test]
|
|
1444
|
-
fn
|
|
1445
|
-
let
|
|
1414
|
+
fn leader_result_summary_preserves_long_multiline_text_verbatim() {
|
|
1415
|
+
let source = format!("首行\nbranch: fix/keyboard-receive-and-compose\ncommit: {}", "a1b2c3d4e5f67890".repeat(24));
|
|
1416
|
+
let envelope = serde_json::json!({"summary": source});
|
|
1446
1417
|
let summary = super::format_report_result_leader_summary(
|
|
1447
|
-
"res-1",
|
|
1448
|
-
"task-1",
|
|
1449
|
-
"worker",
|
|
1450
|
-
"success",
|
|
1451
|
-
&envelope,
|
|
1418
|
+
"res-1", "task-1", "worker", "success", &envelope,
|
|
1452
1419
|
);
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
assert!(summary.
|
|
1420
|
+
let source = envelope["summary"].as_str().unwrap();
|
|
1421
|
+
assert_eq!(summary, format!("Task task-1 reported success from worker: {source}; Result id: res-1"));
|
|
1422
|
+
assert!(summary.len() > 160);
|
|
1423
|
+
assert!(summary.contains("\nbranch: fix/keyboard-receive-and-compose\ncommit: "));
|
|
1424
|
+
assert!(summary.contains("a1b2c3d4e5f67890".repeat(24).as_str()));
|
|
1456
1425
|
}
|
|
1457
1426
|
|
|
1458
1427
|
#[test]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//!
|
|
2
|
-
//!
|
|
2
|
+
//! Scheduled-event dispatch and coordinator stuck detection (card §18/§68/§73).
|
|
3
3
|
|
|
4
4
|
use std::path::Path;
|
|
5
5
|
|
|
@@ -11,9 +11,7 @@ use crate::transport::{PaneId, Transport};
|
|
|
11
11
|
|
|
12
12
|
use super::delivery::{deliver_stored_message, handle_trust_retry_needed};
|
|
13
13
|
use super::helpers::{parse_scheduled_kind, status_wire};
|
|
14
|
-
use super::{
|
|
15
|
-
AlertType, MessagingError, ScheduledKind, TrustRetryPayload, TRUST_RETRY_MAX_ATTEMPTS,
|
|
16
|
-
};
|
|
14
|
+
use super::{MessagingError, ScheduledKind, TrustRetryPayload, TRUST_RETRY_MAX_ATTEMPTS};
|
|
17
15
|
|
|
18
16
|
/// `_fire_due_scheduled_events` (`scheduler.py:41`):coordinator tick 的调度器心脏 —— 分派到期
|
|
19
17
|
/// `send`/`health_ping`/`trust_retry` ([`ScheduledKind`] 穷尽 match),send 失败有界重试。
|
|
@@ -209,204 +207,3 @@ fn output_is_stale(last_output_at: &str, timeout_seconds: i64) -> bool {
|
|
|
209
207
|
.num_seconds()
|
|
210
208
|
>= timeout_seconds
|
|
211
209
|
}
|
|
212
|
-
|
|
213
|
-
/// stuck/idle 告警列表 (`scheduler.py:222`)。team-scoped 抑制查询。CLI 调。
|
|
214
|
-
pub fn stuck_list(workspace: &Path) -> Result<serde_json::Value, MessagingError> {
|
|
215
|
-
let state = crate::state::persist::load_runtime_state(workspace)?;
|
|
216
|
-
let team = active_team_key(workspace, &state);
|
|
217
|
-
let suppressed = state
|
|
218
|
-
.get("coordinator")
|
|
219
|
-
.and_then(|v| v.get("suppressed_idle_alerts"))
|
|
220
|
-
.and_then(|v| v.get(&team))
|
|
221
|
-
.cloned()
|
|
222
|
-
.unwrap_or_else(|| serde_json::json!({}));
|
|
223
|
-
Ok(serde_json::json!({"ok": true, "suppressed_idle_alerts": suppressed}))
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
/// 取消 idle 告警抑制 (`scheduler.py:262`)。`alert_type` 为 [`AlertType`] 或 `all` (展开全集);
|
|
227
|
-
/// owner-gate 校验 (check_team_owner)。CLI 调。
|
|
228
|
-
pub fn stuck_cancel(
|
|
229
|
-
workspace: &Path,
|
|
230
|
-
agent_id: &str,
|
|
231
|
-
alert_type: Option<AlertType>,
|
|
232
|
-
suppressed_by: &str,
|
|
233
|
-
) -> Result<serde_json::Value, MessagingError> {
|
|
234
|
-
let alert_types: Vec<&str> = match alert_type {
|
|
235
|
-
Some(AlertType::Stuck) => vec!["stuck"],
|
|
236
|
-
Some(AlertType::IdleFallback) => vec!["idle_fallback"],
|
|
237
|
-
Some(AlertType::CrossWorkerDeadlock) => vec!["cross_worker_deadlock"],
|
|
238
|
-
None => vec!["cross_worker_deadlock", "idle_fallback", "stuck"],
|
|
239
|
-
};
|
|
240
|
-
let mut state = crate::state::persist::load_runtime_state(workspace)?;
|
|
241
|
-
let team = active_team_key(workspace, &state);
|
|
242
|
-
let now = chrono::Utc::now().to_rfc3339();
|
|
243
|
-
let assigned = assigned_task_ids(&state, agent_id);
|
|
244
|
-
let store = MessageStore::open(workspace)?;
|
|
245
|
-
let delivered = delivered_message_ids(&store, &team, agent_id)?;
|
|
246
|
-
for kind in &alert_types {
|
|
247
|
-
upsert_suppression(
|
|
248
|
-
&mut state,
|
|
249
|
-
SuppressionRecord {
|
|
250
|
-
team: &team,
|
|
251
|
-
agent_id,
|
|
252
|
-
alert_type: kind,
|
|
253
|
-
suppressed_by,
|
|
254
|
-
suppressed_at: &now,
|
|
255
|
-
assigned_task_ids: assigned.clone(),
|
|
256
|
-
delivered_message_ids: delivered.clone(),
|
|
257
|
-
},
|
|
258
|
-
);
|
|
259
|
-
}
|
|
260
|
-
crate::state::repository::StateRepository::new(workspace).save_reapplying(
|
|
261
|
-
crate::state::repository::StateWriteIntent::SchedulerSuppression {
|
|
262
|
-
owner_team_id: Some(&team),
|
|
263
|
-
},
|
|
264
|
-
&state,
|
|
265
|
-
|latest| {
|
|
266
|
-
for kind in &alert_types {
|
|
267
|
-
upsert_suppression(
|
|
268
|
-
latest,
|
|
269
|
-
SuppressionRecord {
|
|
270
|
-
team: &team,
|
|
271
|
-
agent_id,
|
|
272
|
-
alert_type: kind,
|
|
273
|
-
suppressed_by,
|
|
274
|
-
suppressed_at: &now,
|
|
275
|
-
assigned_task_ids: assigned.clone(),
|
|
276
|
-
delivered_message_ids: delivered.clone(),
|
|
277
|
-
},
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
},
|
|
281
|
-
)?;
|
|
282
|
-
crate::event_log::EventLog::new(workspace).write(
|
|
283
|
-
"coordinator.idle_alert_suppressed",
|
|
284
|
-
serde_json::json!({
|
|
285
|
-
"agent_id": agent_id,
|
|
286
|
-
"team": team,
|
|
287
|
-
"alert_types": alert_types,
|
|
288
|
-
"suppressed_by": suppressed_by,
|
|
289
|
-
}),
|
|
290
|
-
)?;
|
|
291
|
-
let suppressed = state
|
|
292
|
-
.get("coordinator")
|
|
293
|
-
.and_then(|v| v.get("suppressed_idle_alerts"))
|
|
294
|
-
.and_then(|v| v.get(&team))
|
|
295
|
-
.and_then(|v| v.get(agent_id))
|
|
296
|
-
.cloned()
|
|
297
|
-
.unwrap_or_else(|| serde_json::json!({}));
|
|
298
|
-
Ok(serde_json::json!({
|
|
299
|
-
"ok": true,
|
|
300
|
-
"agent_id": agent_id,
|
|
301
|
-
"alert_types": alert_types,
|
|
302
|
-
"suppressed": suppressed
|
|
303
|
-
}))
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
fn active_team_key(workspace: &Path, state: &serde_json::Value) -> String {
|
|
307
|
-
state
|
|
308
|
-
.get("active_team_key")
|
|
309
|
-
.and_then(serde_json::Value::as_str)
|
|
310
|
-
.filter(|team| !team.is_empty())
|
|
311
|
-
.map(ToString::to_string)
|
|
312
|
-
.or_else(|| {
|
|
313
|
-
workspace
|
|
314
|
-
.file_name()
|
|
315
|
-
.map(|name| name.to_string_lossy().to_string())
|
|
316
|
-
})
|
|
317
|
-
.unwrap_or_else(|| "current".to_string())
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
fn assigned_task_ids(state: &serde_json::Value, agent_id: &str) -> Vec<String> {
|
|
321
|
-
let mut ids: Vec<String> = state
|
|
322
|
-
.get("tasks")
|
|
323
|
-
.and_then(serde_json::Value::as_array)
|
|
324
|
-
.map(|tasks| {
|
|
325
|
-
tasks
|
|
326
|
-
.iter()
|
|
327
|
-
.filter(|task| {
|
|
328
|
-
task.get("assignee").and_then(serde_json::Value::as_str) == Some(agent_id)
|
|
329
|
-
})
|
|
330
|
-
.filter_map(|task| {
|
|
331
|
-
task.get("id")
|
|
332
|
-
.and_then(serde_json::Value::as_str)
|
|
333
|
-
.map(ToString::to_string)
|
|
334
|
-
})
|
|
335
|
-
.collect()
|
|
336
|
-
})
|
|
337
|
-
.unwrap_or_default();
|
|
338
|
-
ids.sort();
|
|
339
|
-
ids
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
fn delivered_message_ids(
|
|
343
|
-
store: &MessageStore,
|
|
344
|
-
team: &str,
|
|
345
|
-
agent_id: &str,
|
|
346
|
-
) -> Result<Vec<String>, MessagingError> {
|
|
347
|
-
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
348
|
-
let mut stmt = conn.prepare(
|
|
349
|
-
"select message_id from messages
|
|
350
|
-
where (owner_team_id = ?1 or owner_team_id is null)
|
|
351
|
-
and recipient = ?2
|
|
352
|
-
and status in ('visible', 'submitted', 'delivered', 'acknowledged')
|
|
353
|
-
order by message_id",
|
|
354
|
-
)?;
|
|
355
|
-
let rows = stmt.query_map(params![team, agent_id], |row| row.get::<_, String>(0))?;
|
|
356
|
-
Ok(rows.collect::<Result<Vec<_>, _>>()?)
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
struct SuppressionRecord<'a> {
|
|
360
|
-
team: &'a str,
|
|
361
|
-
agent_id: &'a str,
|
|
362
|
-
alert_type: &'a str,
|
|
363
|
-
suppressed_by: &'a str,
|
|
364
|
-
suppressed_at: &'a str,
|
|
365
|
-
assigned_task_ids: Vec<String>,
|
|
366
|
-
delivered_message_ids: Vec<String>,
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
fn upsert_suppression(state: &mut serde_json::Value, record: SuppressionRecord<'_>) {
|
|
370
|
-
let Some(root) = state.as_object_mut() else {
|
|
371
|
-
return;
|
|
372
|
-
};
|
|
373
|
-
let Some(coordinator) = root
|
|
374
|
-
.entry("coordinator")
|
|
375
|
-
.or_insert_with(|| serde_json::json!({}))
|
|
376
|
-
.as_object_mut()
|
|
377
|
-
else {
|
|
378
|
-
return;
|
|
379
|
-
};
|
|
380
|
-
let Some(all) = coordinator
|
|
381
|
-
.entry("suppressed_idle_alerts")
|
|
382
|
-
.or_insert_with(|| serde_json::json!({}))
|
|
383
|
-
.as_object_mut()
|
|
384
|
-
else {
|
|
385
|
-
return;
|
|
386
|
-
};
|
|
387
|
-
let Some(team_map) = all
|
|
388
|
-
.entry(record.team.to_string())
|
|
389
|
-
.or_insert_with(|| serde_json::json!({}))
|
|
390
|
-
.as_object_mut()
|
|
391
|
-
else {
|
|
392
|
-
return;
|
|
393
|
-
};
|
|
394
|
-
let Some(agent_map) = team_map
|
|
395
|
-
.entry(record.agent_id.to_string())
|
|
396
|
-
.or_insert_with(|| serde_json::json!({}))
|
|
397
|
-
.as_object_mut()
|
|
398
|
-
else {
|
|
399
|
-
return;
|
|
400
|
-
};
|
|
401
|
-
agent_map.insert(
|
|
402
|
-
record.alert_type.to_string(),
|
|
403
|
-
serde_json::json!({
|
|
404
|
-
"suppressed_at": record.suppressed_at,
|
|
405
|
-
"suppressed_by": record.suppressed_by,
|
|
406
|
-
"snapshot": {
|
|
407
|
-
"assigned_task_ids": record.assigned_task_ids,
|
|
408
|
-
"delivered_message_ids": record.delivered_message_ids
|
|
409
|
-
},
|
|
410
|
-
}),
|
|
411
|
-
);
|
|
412
|
-
}
|