@team-agent/installer 0.5.53 → 0.5.55
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 +94 -3
- package/crates/team-agent/src/cli/send/persist.rs +1 -0
- package/crates/team-agent/src/cli/send/presentation.rs +1 -0
- package/crates/team-agent/src/cli/send.rs +1 -0
- package/crates/team-agent/src/cli/spec.rs +1 -1
- package/crates/team-agent/src/cli/tests/lane_c.rs +3 -3
- package/crates/team-agent/src/cli/tests/leader_watch.rs +1 -0
- package/crates/team-agent/src/cli/tests/named_address.rs +1 -0
- package/crates/team-agent/src/cli/tests/status_send.rs +1 -0
- package/crates/team-agent/src/cli/types.rs +1 -0
- package/crates/team-agent/src/coordinator/tests/basics.rs +4 -4
- package/crates/team-agent/src/db/message_store.rs +31 -2
- package/crates/team-agent/src/db/migration.rs +7 -6
- package/crates/team-agent/src/db/schema.rs +18 -5
- package/crates/team-agent/src/lifecycle/tests/phase_golden.rs +1 -0
- package/crates/team-agent/src/mcp_server/normalize.rs +8 -0
- package/crates/team-agent/src/mcp_server/tests/wire.rs +1 -1
- package/crates/team-agent/src/mcp_server/tools.rs +71 -0
- package/crates/team-agent/src/mcp_server/types.rs +4 -0
- package/crates/team-agent/src/mcp_server/wire.rs +42 -4
- package/crates/team-agent/src/messaging/delivery.rs +2 -0
- package/crates/team-agent/src/messaging/helpers.rs +1 -0
- package/crates/team-agent/src/messaging/leader_receiver.rs +32 -0
- package/crates/team-agent/src/messaging/mod.rs +2 -1
- package/crates/team-agent/src/messaging/persist.rs +68 -2
- package/crates/team-agent/src/messaging/presentation.rs +307 -0
- package/crates/team-agent/src/messaging/results.rs +130 -3
- package/crates/team-agent/src/messaging/selftest.rs +1 -0
- package/crates/team-agent/src/messaging/send.rs +54 -2
- package/crates/team-agent/src/messaging/tests/runtime.rs +117 -1
- package/crates/team-agent/src/messaging/types.rs +2 -0
- package/crates/team-agent/src/messaging/watchers.rs +13 -7
- package/crates/team-agent/src/provider/session/capture.rs +74 -0
- package/crates/team-agent/src/provider/session_scan/common/tests.rs +112 -0
- package/crates/team-agent/src/provider/session_scan/common.rs +14 -86
- package/package.json +4 -4
- package/schemas/result-envelope.schema.json +10 -0
- package/skills/team-agent/SKILL.md +3 -0
|
@@ -178,6 +178,34 @@ impl TeamOrchestratorTools {
|
|
|
178
178
|
requires_ack: Option<bool>,
|
|
179
179
|
scope_override: Option<Scope>,
|
|
180
180
|
) -> Result<SendOutcome, ToolError> {
|
|
181
|
+
self.send_message_with_presentation(
|
|
182
|
+
to,
|
|
183
|
+
content,
|
|
184
|
+
task_id,
|
|
185
|
+
requires_ack,
|
|
186
|
+
scope_override,
|
|
187
|
+
None,
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
pub fn send_message_with_presentation(
|
|
192
|
+
&self,
|
|
193
|
+
to: &MessageTarget,
|
|
194
|
+
content: &str,
|
|
195
|
+
task_id: Option<&str>,
|
|
196
|
+
requires_ack: Option<bool>,
|
|
197
|
+
scope_override: Option<Scope>,
|
|
198
|
+
presentation: Option<&Value>,
|
|
199
|
+
) -> Result<SendOutcome, ToolError> {
|
|
200
|
+
let (presentation, presentation_error) =
|
|
201
|
+
crate::messaging::presentation::normalize_presentation(presentation);
|
|
202
|
+
if let Some(error) = presentation_error {
|
|
203
|
+
return Err(ToolError::new(
|
|
204
|
+
ToolErrorReason::InvalidToolArguments,
|
|
205
|
+
format!("invalid presentation: {error}"),
|
|
206
|
+
"PresentationError",
|
|
207
|
+
));
|
|
208
|
+
}
|
|
181
209
|
let canonical_owner_team = self.canonical_owner_team_key()?;
|
|
182
210
|
if matches!(scope_override, Some(Scope::Workspace)) {
|
|
183
211
|
return Err(self.rpc_scope_refused(
|
|
@@ -227,6 +255,7 @@ impl TeamOrchestratorTools {
|
|
|
227
255
|
sender,
|
|
228
256
|
requires_ack: ack,
|
|
229
257
|
team: canonical_owner_team,
|
|
258
|
+
presentation,
|
|
230
259
|
..SendOptions::default()
|
|
231
260
|
};
|
|
232
261
|
if is_worker_recipient(to) {
|
|
@@ -322,6 +351,36 @@ impl TeamOrchestratorTools {
|
|
|
322
351
|
next_actions: Option<&[Value]>,
|
|
323
352
|
task_id: Option<&str>,
|
|
324
353
|
agent_id: Option<&str>,
|
|
354
|
+
) -> ToolResult {
|
|
355
|
+
self.report_result_with_presentation(
|
|
356
|
+
envelope,
|
|
357
|
+
summary,
|
|
358
|
+
status,
|
|
359
|
+
changes,
|
|
360
|
+
tests,
|
|
361
|
+
risks,
|
|
362
|
+
artifacts,
|
|
363
|
+
next_actions,
|
|
364
|
+
task_id,
|
|
365
|
+
agent_id,
|
|
366
|
+
None,
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
#[allow(clippy::too_many_arguments)]
|
|
371
|
+
pub fn report_result_with_presentation(
|
|
372
|
+
&self,
|
|
373
|
+
envelope: Option<&Value>,
|
|
374
|
+
summary: Option<&str>,
|
|
375
|
+
status: ResultStatus,
|
|
376
|
+
changes: Option<&[Value]>,
|
|
377
|
+
tests: Option<&[Value]>,
|
|
378
|
+
risks: Option<&[Value]>,
|
|
379
|
+
artifacts: Option<&[Value]>,
|
|
380
|
+
next_actions: Option<&[Value]>,
|
|
381
|
+
task_id: Option<&str>,
|
|
382
|
+
agent_id: Option<&str>,
|
|
383
|
+
presentation: Option<&Value>,
|
|
325
384
|
) -> ToolResult {
|
|
326
385
|
if let Some(envelope) = envelope {
|
|
327
386
|
self.validate_rpc_scope_args("report_result", envelope)?;
|
|
@@ -331,6 +390,11 @@ impl TeamOrchestratorTools {
|
|
|
331
390
|
.unwrap_or_else(|| Value::Object(serde_json::Map::new()));
|
|
332
391
|
ensure_object(&mut base);
|
|
333
392
|
if let Some(obj) = base.as_object_mut() {
|
|
393
|
+
if !obj.contains_key("presentation") {
|
|
394
|
+
if let Some(presentation) = presentation {
|
|
395
|
+
obj.insert("presentation".to_string(), presentation.clone());
|
|
396
|
+
}
|
|
397
|
+
}
|
|
334
398
|
if !obj.contains_key("summary") {
|
|
335
399
|
obj.insert(
|
|
336
400
|
"summary".to_string(),
|
|
@@ -459,6 +523,13 @@ impl TeamOrchestratorTools {
|
|
|
459
523
|
self.note_unknown_result_status(&raw);
|
|
460
524
|
}
|
|
461
525
|
let normalized = normalize_report_envelope(&base);
|
|
526
|
+
if let Some(error) = normalized.presentation_error.as_deref() {
|
|
527
|
+
return Err(ToolError::new(
|
|
528
|
+
ToolErrorReason::InvalidToolArguments,
|
|
529
|
+
format!("invalid presentation: {error}"),
|
|
530
|
+
"PresentationError",
|
|
531
|
+
));
|
|
532
|
+
}
|
|
462
533
|
let warnings = report_result_integrity_warnings(&base, &normalized);
|
|
463
534
|
let mut env_value = normalized_envelope_value(&normalized);
|
|
464
535
|
copy_report_attribution_fields(&base, &mut env_value);
|
|
@@ -5,6 +5,7 @@ use serde_json::Value;
|
|
|
5
5
|
use thiserror::Error;
|
|
6
6
|
|
|
7
7
|
// ── REUSE: step 2 model (ids + normalized-envelope value enums) ─────────────
|
|
8
|
+
use crate::messaging::presentation::PresentationDecision;
|
|
8
9
|
use crate::model::enums::{ChangeKind, ResultStatus, RiskSeverity, TestStatus};
|
|
9
10
|
use crate::model::ids::{AgentId, TaskId, TeamKey};
|
|
10
11
|
|
|
@@ -367,6 +368,9 @@ pub struct NormalizedReportEnvelope {
|
|
|
367
368
|
pub risks: Vec<NormalizedRisk>,
|
|
368
369
|
pub artifacts: Vec<NormalizedArtifact>,
|
|
369
370
|
pub next_actions: Vec<NormalizedNextAction>,
|
|
371
|
+
pub presentation: PresentationDecision,
|
|
372
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
373
|
+
pub presentation_error: Option<String>,
|
|
370
374
|
}
|
|
371
375
|
|
|
372
376
|
/// `changes[]` (`normalize.py:126-142`): path + regularized [`ChangeKind`] +
|
|
@@ -369,11 +369,14 @@ fn python_repr(value: &str) -> String {
|
|
|
369
369
|
fn tool_contract(tool: McpTool) -> Value {
|
|
370
370
|
let (description, required) = match tool {
|
|
371
371
|
McpTool::SendMessage => (
|
|
372
|
-
"Send a message to a teammate, the leader, or '*' for all other team members.
|
|
372
|
+
"Send a message to a teammate, the leader, or '*' for all other team members. Team Agent fills identity and delivery metadata; optional presentation routing is durable and never drops the message.",
|
|
373
373
|
vec!["to", "content"],
|
|
374
374
|
),
|
|
375
375
|
McpTool::AssignTask => ("Assign or update a task in the team graph and deliver it to its assignee.", vec!["task"]),
|
|
376
|
-
McpTool::ReportResult => (
|
|
376
|
+
McpTool::ReportResult => (
|
|
377
|
+
"Report task completion with a durable result envelope. Optional presentation routing controls live leader display, not persistence.",
|
|
378
|
+
Vec::new(),
|
|
379
|
+
),
|
|
377
380
|
McpTool::UpdateState => ("Append a note to team state and rewrite team_state.md.", vec!["note"]),
|
|
378
381
|
McpTool::GetTeamStatus => ("Return machine-readable team status.", Vec::new()),
|
|
379
382
|
McpTool::StopAgent => ("Stop a running worker.", vec!["agent_id"]),
|
|
@@ -419,6 +422,11 @@ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
|
|
|
419
422
|
string_property("Target agent id, 'leader', or '*' for broadcast."),
|
|
420
423
|
);
|
|
421
424
|
insert_property(&mut properties, "content", string_property("Message body."));
|
|
425
|
+
insert_property(
|
|
426
|
+
&mut properties,
|
|
427
|
+
"presentation",
|
|
428
|
+
presentation_property("Optional durable presentation routing."),
|
|
429
|
+
);
|
|
422
430
|
}
|
|
423
431
|
McpTool::ReportResult => {
|
|
424
432
|
insert_property(
|
|
@@ -467,6 +475,11 @@ fn tool_properties(tool: McpTool) -> serde_json::Map<String, Value> {
|
|
|
467
475
|
"agent_id",
|
|
468
476
|
string_property("Optional reporting agent id override."),
|
|
469
477
|
);
|
|
478
|
+
insert_property(
|
|
479
|
+
&mut properties,
|
|
480
|
+
"presentation",
|
|
481
|
+
presentation_property("Optional durable presentation routing."),
|
|
482
|
+
);
|
|
470
483
|
}
|
|
471
484
|
McpTool::UpdateState => {
|
|
472
485
|
insert_property(
|
|
@@ -590,6 +603,23 @@ fn object_property(description: &str) -> Value {
|
|
|
590
603
|
serde_json::json!({"type": "object", "description": description, "additionalProperties": true})
|
|
591
604
|
}
|
|
592
605
|
|
|
606
|
+
fn presentation_property(description: &str) -> Value {
|
|
607
|
+
serde_json::json!({
|
|
608
|
+
"type": "object",
|
|
609
|
+
"description": description,
|
|
610
|
+
"properties": {
|
|
611
|
+
"sink": {"type": "string", "enum": ["leader", "casefile", "silent"]},
|
|
612
|
+
"class": {"type": "string", "enum": [
|
|
613
|
+
"message", "progress", "stage_result", "stage_pass", "bounce",
|
|
614
|
+
"blocking", "final_review", "timeout"
|
|
615
|
+
]},
|
|
616
|
+
"case_id": {"type": "string"}
|
|
617
|
+
},
|
|
618
|
+
"required": ["sink", "class"],
|
|
619
|
+
"additionalProperties": false
|
|
620
|
+
})
|
|
621
|
+
}
|
|
622
|
+
|
|
593
623
|
fn array_property(description: &str) -> Value {
|
|
594
624
|
serde_json::json!({"type": "array", "description": description, "items": {"type": "object", "additionalProperties": true}})
|
|
595
625
|
}
|
|
@@ -610,7 +640,14 @@ pub(crate) fn dispatch_tool(
|
|
|
610
640
|
McpTool::SendMessage => {
|
|
611
641
|
let target = message_target_from_value(args.get("to"));
|
|
612
642
|
let content = args.get("content").and_then(Value::as_str).unwrap_or("");
|
|
613
|
-
let outcome = tools.
|
|
643
|
+
let outcome = tools.send_message_with_presentation(
|
|
644
|
+
&target,
|
|
645
|
+
content,
|
|
646
|
+
None,
|
|
647
|
+
None,
|
|
648
|
+
None,
|
|
649
|
+
args.get("presentation"),
|
|
650
|
+
)?;
|
|
614
651
|
match outcome {
|
|
615
652
|
SendOutcome::WorkerAccepted { .. } => Ok(ToolOk {
|
|
616
653
|
fields: object_fields(outcome.to_value()),
|
|
@@ -618,7 +655,7 @@ pub(crate) fn dispatch_tool(
|
|
|
618
655
|
SendOutcome::Direct(ok) => Ok(ok),
|
|
619
656
|
}
|
|
620
657
|
}
|
|
621
|
-
McpTool::ReportResult => tools.
|
|
658
|
+
McpTool::ReportResult => tools.report_result_with_presentation(
|
|
622
659
|
args.get("envelope"),
|
|
623
660
|
args.get("summary").and_then(Value::as_str),
|
|
624
661
|
// cr verdict (T3-1 refined): an unknown status literal normalizes to
|
|
@@ -650,6 +687,7 @@ pub(crate) fn dispatch_tool(
|
|
|
650
687
|
.map(Vec::as_slice),
|
|
651
688
|
args.get("task_id").and_then(Value::as_str),
|
|
652
689
|
args.get("agent_id").and_then(Value::as_str),
|
|
690
|
+
args.get("presentation"),
|
|
653
691
|
),
|
|
654
692
|
McpTool::UpdateState => {
|
|
655
693
|
tools.update_state(args.get("note").and_then(Value::as_str).unwrap_or(""))
|
|
@@ -57,6 +57,7 @@ pub fn deliver_stored_message(
|
|
|
57
57
|
requires_ack,
|
|
58
58
|
None,
|
|
59
59
|
super::InitialDisposition::Accepted,
|
|
60
|
+
None,
|
|
60
61
|
)?
|
|
61
62
|
else {
|
|
62
63
|
unreachable!("internal delivery does not accept caller-supplied ids")
|
|
@@ -1828,6 +1829,7 @@ pub fn deliver_pending_messages(
|
|
|
1828
1829
|
let store = MessageStore::open(workspace)?;
|
|
1829
1830
|
let message_ids = {
|
|
1830
1831
|
let conn = crate::db::schema::open_db(store.db_path())?;
|
|
1832
|
+
// `stored_only` is intentionally absent: C6 chose durable presentation without C5.
|
|
1831
1833
|
let mut stmt = conn.prepare(
|
|
1832
1834
|
"select message_id from messages
|
|
1833
1835
|
where status in (
|
|
@@ -25,6 +25,7 @@ static RESULT_COUNTER: AtomicU64 = AtomicU64::new(0);
|
|
|
25
25
|
pub(crate) fn status_wire(status: DeliveryStatus) -> &'static str {
|
|
26
26
|
match status {
|
|
27
27
|
DeliveryStatus::Delivered => "delivered",
|
|
28
|
+
DeliveryStatus::StoredOnly => "stored_only",
|
|
28
29
|
DeliveryStatus::Failed => "failed",
|
|
29
30
|
DeliveryStatus::Queued => "queued",
|
|
30
31
|
DeliveryStatus::Blocked => "blocked",
|
|
@@ -14,6 +14,7 @@ use crate::transport::{
|
|
|
14
14
|
|
|
15
15
|
use super::helpers::MessageStatusShadow;
|
|
16
16
|
use super::persist::persist_internal_send;
|
|
17
|
+
use super::presentation::PresentationDecision;
|
|
17
18
|
use super::{
|
|
18
19
|
DeliveryOutcome, DeliveryRefusal, DeliveryStage, DeliveryStatus, InitialDisposition,
|
|
19
20
|
InternalSendKind, MessagingError, PersistResolution,
|
|
@@ -69,6 +70,35 @@ pub fn send_to_leader_receiver_with_message_id(
|
|
|
69
70
|
result_id: Option<&str>,
|
|
70
71
|
requested_message_id: Option<&str>,
|
|
71
72
|
event_log: &EventLog,
|
|
73
|
+
) -> Result<DeliveryOutcome, MessagingError> {
|
|
74
|
+
send_to_leader_receiver_with_presentation(
|
|
75
|
+
workspace,
|
|
76
|
+
state,
|
|
77
|
+
leader_id,
|
|
78
|
+
content,
|
|
79
|
+
task_id,
|
|
80
|
+
sender,
|
|
81
|
+
requires_ack,
|
|
82
|
+
result_id,
|
|
83
|
+
requested_message_id,
|
|
84
|
+
&PresentationDecision::default(),
|
|
85
|
+
event_log,
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[allow(clippy::too_many_arguments)]
|
|
90
|
+
pub fn send_to_leader_receiver_with_presentation(
|
|
91
|
+
workspace: &Path,
|
|
92
|
+
state: &serde_json::Value,
|
|
93
|
+
leader_id: &str,
|
|
94
|
+
content: &str,
|
|
95
|
+
task_id: Option<&TaskId>,
|
|
96
|
+
sender: &str,
|
|
97
|
+
requires_ack: bool,
|
|
98
|
+
result_id: Option<&str>,
|
|
99
|
+
requested_message_id: Option<&str>,
|
|
100
|
+
presentation: &PresentationDecision,
|
|
101
|
+
event_log: &EventLog,
|
|
72
102
|
) -> Result<DeliveryOutcome, MessagingError> {
|
|
73
103
|
let store = MessageStore::open(workspace)?;
|
|
74
104
|
let owner_team = active_team_key(workspace, state);
|
|
@@ -90,6 +120,7 @@ pub fn send_to_leader_receiver_with_message_id(
|
|
|
90
120
|
false,
|
|
91
121
|
requested_message_id,
|
|
92
122
|
InitialDisposition::Accepted,
|
|
123
|
+
Some(presentation),
|
|
93
124
|
)? {
|
|
94
125
|
PersistResolution::Duplicate(requested) => {
|
|
95
126
|
return Ok(DeliveryOutcome {
|
|
@@ -643,6 +674,7 @@ pub fn enqueue_leader_mailbox_until_attach(
|
|
|
643
674
|
false,
|
|
644
675
|
None,
|
|
645
676
|
InitialDisposition::QueuedUntilLeaderAttach,
|
|
677
|
+
None,
|
|
646
678
|
)?
|
|
647
679
|
else {
|
|
648
680
|
unreachable!("offline mailbox does not accept caller-supplied ids")
|
|
@@ -68,6 +68,7 @@ pub mod leader_channel;
|
|
|
68
68
|
pub mod leader_receiver;
|
|
69
69
|
pub mod peers;
|
|
70
70
|
pub mod persist;
|
|
71
|
+
pub mod presentation;
|
|
71
72
|
pub mod results;
|
|
72
73
|
pub mod scheduler;
|
|
73
74
|
pub mod selftest;
|
|
@@ -94,7 +95,7 @@ pub use leader_channel::{
|
|
|
94
95
|
pub use leader_receiver::{
|
|
95
96
|
deliver_to_leader_fallback_pane, enqueue_leader_mailbox_until_attach,
|
|
96
97
|
mirror_peer_message_to_leader, send_to_leader_receiver,
|
|
97
|
-
send_to_leader_receiver_with_message_id,
|
|
98
|
+
send_to_leader_receiver_with_message_id, send_to_leader_receiver_with_presentation,
|
|
98
99
|
};
|
|
99
100
|
pub use peers::allow_peer_talk;
|
|
100
101
|
pub use persist::{
|
|
@@ -5,6 +5,7 @@ use std::path::{Path, PathBuf};
|
|
|
5
5
|
use crate::db::message_store::{MessageRowStatus, MessageStore, PersistMessageInput};
|
|
6
6
|
use crate::model::ids::{AgentId, TaskId, TeamKey};
|
|
7
7
|
|
|
8
|
+
use super::presentation::PresentationDecision;
|
|
8
9
|
use super::send::TrustedSender;
|
|
9
10
|
use super::MessagingError;
|
|
10
11
|
|
|
@@ -64,6 +65,7 @@ impl DeliveryBlocker {
|
|
|
64
65
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
65
66
|
pub enum InitialDisposition {
|
|
66
67
|
Accepted,
|
|
68
|
+
StoredOnly,
|
|
67
69
|
QueuedUntilLeaderAttach,
|
|
68
70
|
Blocked(DeliveryBlocker),
|
|
69
71
|
}
|
|
@@ -72,6 +74,7 @@ impl InitialDisposition {
|
|
|
72
74
|
fn row_status(self) -> MessageRowStatus {
|
|
73
75
|
match self {
|
|
74
76
|
Self::Accepted => MessageRowStatus::Accepted,
|
|
77
|
+
Self::StoredOnly => MessageRowStatus::StoredOnly,
|
|
75
78
|
Self::QueuedUntilLeaderAttach => MessageRowStatus::QueuedUntilLeaderAttach,
|
|
76
79
|
Self::Blocked(DeliveryBlocker::CoordinatorUnavailable) => {
|
|
77
80
|
MessageRowStatus::QueuedCoordinatorUnavailable
|
|
@@ -82,7 +85,7 @@ impl InitialDisposition {
|
|
|
82
85
|
fn error(self) -> Option<&'static str> {
|
|
83
86
|
match self {
|
|
84
87
|
Self::Blocked(blocker) => Some(blocker.as_str()),
|
|
85
|
-
Self::Accepted | Self::QueuedUntilLeaderAttach => None,
|
|
88
|
+
Self::Accepted | Self::StoredOnly | Self::QueuedUntilLeaderAttach => None,
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
}
|
|
@@ -100,6 +103,7 @@ pub struct ResolvedSendIntent {
|
|
|
100
103
|
pub requires_ack: bool,
|
|
101
104
|
pub requested_message_id: Option<String>,
|
|
102
105
|
pub initial_disposition: InitialDisposition,
|
|
106
|
+
pub presentation: PresentationDecision,
|
|
103
107
|
}
|
|
104
108
|
|
|
105
109
|
impl ResolvedSendIntent {
|
|
@@ -128,6 +132,7 @@ impl ResolvedSendIntent {
|
|
|
128
132
|
requires_ack,
|
|
129
133
|
requested_message_id,
|
|
130
134
|
initial_disposition: InitialDisposition::Accepted,
|
|
135
|
+
presentation: PresentationDecision::default(),
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
138
|
}
|
|
@@ -158,6 +163,7 @@ pub fn persist_resolved_send(
|
|
|
158
163
|
}
|
|
159
164
|
}
|
|
160
165
|
let status = intent.initial_disposition.row_status();
|
|
166
|
+
let presentation = serde_json::to_string(&intent.presentation)?;
|
|
161
167
|
let message_id = store.persist_message(PersistMessageInput {
|
|
162
168
|
message_id: intent.requested_message_id.as_deref(),
|
|
163
169
|
owner_team_id: intent.owner_team_id.as_ref().map(TeamKey::as_str),
|
|
@@ -168,6 +174,7 @@ pub fn persist_resolved_send(
|
|
|
168
174
|
requires_ack: intent.requires_ack,
|
|
169
175
|
status,
|
|
170
176
|
content: &intent.content,
|
|
177
|
+
presentation: &presentation,
|
|
171
178
|
error: intent.initial_disposition.error(),
|
|
172
179
|
})?;
|
|
173
180
|
Ok(PersistResolution::Persisted(PersistedSend {
|
|
@@ -177,7 +184,9 @@ pub fn persist_resolved_send(
|
|
|
177
184
|
row_status: status,
|
|
178
185
|
blocker: match intent.initial_disposition {
|
|
179
186
|
InitialDisposition::Blocked(blocker) => Some(blocker),
|
|
180
|
-
InitialDisposition::Accepted
|
|
187
|
+
InitialDisposition::Accepted
|
|
188
|
+
| InitialDisposition::StoredOnly
|
|
189
|
+
| InitialDisposition::QueuedUntilLeaderAttach => None,
|
|
181
190
|
},
|
|
182
191
|
}))
|
|
183
192
|
}
|
|
@@ -195,6 +204,7 @@ pub(crate) fn persist_internal_send(
|
|
|
195
204
|
requires_ack: bool,
|
|
196
205
|
requested_message_id: Option<&str>,
|
|
197
206
|
initial_disposition: InitialDisposition,
|
|
207
|
+
presentation: Option<&PresentationDecision>,
|
|
198
208
|
) -> Result<PersistResolution, MessagingError> {
|
|
199
209
|
let mut intent = ResolvedSendIntent::accepted(
|
|
200
210
|
SendOrigin::Internal(kind),
|
|
@@ -209,6 +219,9 @@ pub(crate) fn persist_internal_send(
|
|
|
209
219
|
requested_message_id.map(ToOwned::to_owned),
|
|
210
220
|
);
|
|
211
221
|
intent.initial_disposition = initial_disposition;
|
|
222
|
+
if let Some(presentation) = presentation {
|
|
223
|
+
intent.presentation = presentation.clone();
|
|
224
|
+
}
|
|
212
225
|
persist_resolved_send(&intent)
|
|
213
226
|
}
|
|
214
227
|
|
|
@@ -266,6 +279,59 @@ mod tests {
|
|
|
266
279
|
assert_eq!(error.as_deref(), Some("coordinator_unavailable"));
|
|
267
280
|
}
|
|
268
281
|
|
|
282
|
+
#[test]
|
|
283
|
+
fn stored_only_persists_requested_presentation_without_delivery_eligibility() {
|
|
284
|
+
let workspace = workspace("stored-only");
|
|
285
|
+
let mut intent = ResolvedSendIntent::accepted(
|
|
286
|
+
SendOrigin::Mcp,
|
|
287
|
+
&workspace,
|
|
288
|
+
Some(TeamKey::new("team-a")),
|
|
289
|
+
LogicalRecipient::Leader,
|
|
290
|
+
TrustedSender::from_runtime_identity(AgentId::new("worker_a")),
|
|
291
|
+
None,
|
|
292
|
+
"stage evidence",
|
|
293
|
+
None,
|
|
294
|
+
false,
|
|
295
|
+
None,
|
|
296
|
+
);
|
|
297
|
+
intent.initial_disposition = InitialDisposition::StoredOnly;
|
|
298
|
+
intent.presentation = super::super::presentation::decide_presentation(
|
|
299
|
+
&super::super::presentation::PresentationRequest {
|
|
300
|
+
sink: super::super::presentation::PresentationSink::Casefile,
|
|
301
|
+
class: super::super::presentation::PresentationClass::StageResult,
|
|
302
|
+
case_id: Some("case-7".to_string()),
|
|
303
|
+
},
|
|
304
|
+
super::super::presentation::PresentationSource::Send,
|
|
305
|
+
);
|
|
306
|
+
let PersistResolution::Persisted(persisted) = persist_resolved_send(&intent).unwrap()
|
|
307
|
+
else {
|
|
308
|
+
panic!("fresh intent must persist")
|
|
309
|
+
};
|
|
310
|
+
assert_eq!(persisted.row_status, MessageRowStatus::StoredOnly);
|
|
311
|
+
let store = MessageStore::open(&workspace).unwrap();
|
|
312
|
+
let connection = crate::db::schema::open_db(store.db_path()).unwrap();
|
|
313
|
+
let (status, presentation): (String, String) = connection
|
|
314
|
+
.query_row(
|
|
315
|
+
"select status, presentation from messages where message_id = ?1",
|
|
316
|
+
[&persisted.message_id],
|
|
317
|
+
|row| Ok((row.get(0)?, row.get(1)?)),
|
|
318
|
+
)
|
|
319
|
+
.unwrap();
|
|
320
|
+
assert_eq!(status, "stored_only");
|
|
321
|
+
assert_eq!(
|
|
322
|
+
serde_json::from_str::<serde_json::Value>(&presentation).unwrap(),
|
|
323
|
+
serde_json::json!({
|
|
324
|
+
"sink": "casefile",
|
|
325
|
+
"class": "stage_result",
|
|
326
|
+
"case_id": "case-7",
|
|
327
|
+
"requested_sink": "casefile",
|
|
328
|
+
"effective_sink": "casefile",
|
|
329
|
+
"policy_reason": "requested_sink:casefile",
|
|
330
|
+
"policy_version": "team-presentation-v1"
|
|
331
|
+
})
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
269
335
|
#[test]
|
|
270
336
|
fn production_row_creation_is_owned_by_persisted_primitive() {
|
|
271
337
|
for (name, source) in [
|