- 53
/// when the Agent explicitly marks it as additional information. The complete - 54
/// model text remains in the append-only session for inspection and replay. - 55
pub fn supplemental_card_note(narration: &str) -> Option<&str> { - 56
let text = narration.trim(); - 57
let note = text - 58
.strip_prefix("Note:") - 59
.or_else(|| text.strip_prefix("Additional note:"))? - 60
.trim(); - 61
(!note.is_empty()).then_some(note) - 62
} - 63
- 64
/// The complete answer and its conservative structural projection. - 65
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 66
pub struct AnswerDraft { - 67
pub schema_version: u16, - 68
pub source_markdown: String, - 69
pub blocks: Vec<Block>, - 70
#[serde(default)] - 71
pub document: PresentationDocument, - 72
#[serde(default)] - 73
pub metadata: BTreeMap<String, String>, - 74
/// Independently presentable results. Empty means this draft is a single - 75
/// answer result represented by `source_markdown`. - 76
#[serde(default)] - 77
pub results: Vec<AnswerResult>, - 78
} - 79
- 80
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 81
pub struct AnswerResult { - 82
pub id: String, - 83
pub source_markdown: String, - 84
#[serde(default, skip_serializing_if = "Option::is_none")] - 85
pub outcome: Option<ResultOutcome>, - 86
} - 87
- 88
impl AnswerDraft { - 89
/// Parse a useful initial block projection while retaining the exact - 90
/// source for export and fallback delivery. - 91
pub fn from_markdown(source_markdown: impl Into<String>) -> Self { - 92
let source_markdown = source_markdown.into(); - 93
let blocks = parse_blocks(&source_markdown); - 94
let document = compile_markdown(source_markdown.clone()); - 95
Self { - 96
schema_version: DELIVERY_SCHEMA_VERSION, - 97
source_markdown, - 98
blocks, - 99
document, - 100
metadata: BTreeMap::new(), - 101
results: Vec::new(), - 102
} - 103
} - 104
- 105
fn channel_projection(&self) -> Self { - 106
if self.results.is_empty() { - 107
return self.clone(); - 108
} - 109
let source_markdown = self - 110
.results - 111
.iter() - 112
.map(|result| result.source_markdown.as_str()) - 113
.collect::<Vec<_>>() - 114
.join("\n\n"); - 115
let mut projected = Self::from_markdown(source_markdown); - 116
projected.metadata = self.metadata.clone(); - 117
projected.results = self.results.clone(); - 118
projected - 119
} - 120
} - 121
- 122
/// Stable, presentation-neutral blocks. New block types must retain a - 123
/// textual fallback and receive a new schema version when their contract - 124
/// changes incompatibly. - 125
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 126
#[serde(tag = "type", rename_all = "snake_case")] - 127
pub enum Block { - 128
Heading { - 129
id: String, - 130
level: u8, - 131
text: String, - 132
}, - 133
Paragraph { - 134
id: String, - 135
text: String, - 136
}, - 137
List { - 138
id: String, - 139
ordered: bool, - 140
items: Vec<String>, - 141
}, - 142
Quote { - 143
id: String, - 144
text: String, - 145
}, - 146
Code { - 147
id: String, - 148
language: Option<String>, - 149
content: String, - 150
}, - 151
/// Unknown or not-yet-typed Markdown is intentionally opaque, not - 152
/// discarded. It can be rendered verbatim until a richer block exists. - 153
RawMarkdown { - 154
id: String, - 155
markdown: String, - 156
reason: String, - 157
}, - 158
} - 159
- 160
impl Block { - 161
fn id(&self) -> &str { - 162
match self { - 163
Self::Heading { id, .. } - 164
| Self::Paragraph { id, .. } - 165
| Self::List { id, .. } - 166
| Self::Quote { id, .. } - 167
| Self::Code { id, .. } - 168
| Self::RawMarkdown { id, .. } => id, - 169
} - 170
} - 171
} - 172
- 173
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 174
#[serde(rename_all = "snake_case")] - 175
pub enum Markup { - 176
Plain, - 177
Markdown, - 178
TelegramHtml, - 179
SlackMrkdwn, - 180
DiscordMarkdown, - 181
Json, - 182
} - 183
- 184
/// Message classes are intentionally explicit. Only outward-facing content - 185
/// may use a presentation template; system and control-plane messages keep - 186
/// their own protocol shape. - 187
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 188
#[serde(rename_all = "snake_case")] - 189
pub enum DeliveryKind { - 190
Assistant, - 191
TaskSummary, - 192
Alert, - 193
User, - 194
Approval, - 195
Progress, - 196
ToolResult, - 197
System, - 198
Developer, - 199
ToolCall, - 200
Steering, - 201
Internal, - 202
} - 203
- 204
impl DeliveryKind { - 205
fn allows_template(self) -> bool { - 206
matches!( - 207
self, - 208
Self::Assistant | Self::TaskSummary | Self::Alert | Self::User - 209
) - 210
} - 211
- 212
fn is_external(self) -> bool { - 213
!matches!(self, Self::System | Self::Developer | Self::Internal) - 214
} - 215
} - 216
- 217
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 218
#[serde(tag = "type", rename_all = "snake_case")] - 219
pub enum DeliveryContent { - 220
Answer(AnswerDraft), - 221
Approval(ApprovalPayload), - 222
Progress(ProgressPayload), - 223
ToolResult(ToolResultPayload), - 224
Text { markdown: String }, - 225
} - 226
- 227
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 228
pub struct ApprovalPayload { - 229
pub request_id: String, - 230
pub title: String, - 231
pub detail: String, - 232
pub expires_at: Option<String>, - 233
pub actions: Vec<DeliveryAction>, - 234
} - 235
- 236
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 237
pub struct ProgressPayload { - 238
pub label: String, - 239
pub state: String, - 240
pub percent: Option<u8>, - 241
} - 242
- 243
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 244
pub struct ToolResultPayload { - 245
pub tool: String, - 246
pub output: String, - 247
pub is_error: bool, - 248
} - 249
- 250
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 251
pub struct DeliveryAction { - 252
pub id: String, - 253
pub label: String, - 254
pub verb: String, - 255
pub data: BTreeMap<String, String>, - 256
} - 257
- 258
/// What a target can safely accept. Profiles are input to rendering, not - 259
/// claims inferred from a target name. - 260
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 261
pub struct DeliveryProfile { - 262
pub surface: String, - 263
pub markup: Markup, - 264
pub max_chars: Option<usize>, - 265
pub supports_tables: bool, - 266
pub supports_code_blocks: bool, - 267
pub supports_links: bool, - 268
pub supports_actions: bool, - 269
/// Optional declarative layout. `None` means the channel default. - 270
#[serde(default)] - 271
pub template: Option<TemplateSpec>, - 272
/// When this packet should reach its reader and how hard it may push. - 273
/// The posture is set by the caller (session/task config); it decides - 274
/// whether a packet goes out now, waits for completion, or rolls into - 275
/// a digest. It never changes the content or the renderer. - 276
#[serde(default)] - 277
pub posture: DeliveryPosture, - 278
} - 279
- 280
impl DeliveryProfile { - 281
pub fn plain(surface: impl Into<String>) -> Self { - 282
Self { - 283
surface: surface.into(), - 284
markup: Markup::Plain, - 285
max_chars: None, - 286
supports_tables: false, - 287
supports_code_blocks: true, - 288
supports_links: true, - 289
supports_actions: false, - 290
template: None, - 291
posture: DeliveryPosture::default(), - 292
} - 293
} - 294
- 295
fn validate(&self) -> Result<(), DeliveryError> { - 296
if self.surface.trim().is_empty() { - 297
return Err(DeliveryError::InvalidProfile("surface is empty".into())); - 298
} - 299
if self.max_chars == Some(0) { - 300
return Err(DeliveryError::InvalidProfile( - 301
"max_chars must be greater than zero".into(), - 302
)); - 303
} - 304
Ok(()) - 305
} - 306
- 307
/// Semantic capabilities consumed by native and constrained projectors. - 308
/// Existing profile fields remain the compatibility wire contract. - 309
pub fn capabilities(&self) -> SurfaceCapabilities { - 310
let native = matches!(self.surface.as_str(), "desktop" | "tui" | "admin"); - 311
SurfaceCapabilities { - 312
structured_blocks: matches!(self.markup, Markup::Json) || native, - 313
tables: self.supports_tables, - 314
code: self.supports_code_blocks, - 315
links: self.supports_links, - 316
media: native || matches!(self.markup, Markup::Json), - 317
file_references: native || matches!(self.markup, Markup::Json), - 318
actions: self.supports_actions, - 319
color: native, - 320
interactive: self.supports_actions || native, - 321
accessible_plain: matches!(self.markup, Markup::Plain), - 322
max_chars: self.max_chars, - 323
} - 324
} - 325
} - 326
- 327
/// User-replaceable presentation layout. Templates contain no executable - 328
/// code, network access, filesystem access, or arbitrary markup injection. - 329
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 330
pub struct TemplateSpec { - 331
pub id: String, - 332
pub revision: u32, - 333
pub origin: TemplateOrigin, - 334
#[serde(default)] - 335
pub activation: TemplateActivation, - 336
#[serde(default)] - 337
pub nodes: Vec<TemplateNode>, - 338
} - 339
- 340
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 341
#[serde(rename_all = "snake_case")] - 342
pub enum TemplateOrigin { - 343
BuiltIn, - 344
User, - 345
Project, - 346
AgentProposal, - 347
} - 348
- 349
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] - 350
#[serde(rename_all = "snake_case")] - 351
pub enum TemplateActivation { - 352
#[default] - 353
Active, - 354
Proposed, - 355
} - 356
- 357
/// Resolves replaceable templates without allowing an agent proposal to - 358
/// silently become active. User/project templates override built-ins by - 359
/// origin priority, then by revision. - 360
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] - 361
pub struct TemplateRegistry { - 362
pub templates: Vec<TemplateSpec>, - 363
} - 364
- 365
impl TemplateRegistry { - 366
pub fn upsert(&mut self, template: TemplateSpec) -> Result<(), DeliveryError> { - 367
template.validate()?; - 368
if let Some(existing) = self - 369
.templates - 370
.iter_mut() - 371
.find(|candidate| candidate.id == template.id && candidate.origin == template.origin) - 372
{ - 373
if template.revision < existing.revision { - 374
return Err(DeliveryError::InvalidTemplate(format!( - 375
"template {} revision {} is older than stored revision {}", - 376
template.id, template.revision, existing.revision - 377
))); - 378
} - 379
*existing = template; - 380
} else { - 381
self.templates.push(template); - 382
} - 383
Ok(()) - 384
} - 385
- 386
pub fn resolve(&self, id: &str) -> Option<&TemplateSpec> { - 387
self.templates - 388
.iter() - 389
.filter(|template| { - 390
template.id == id && template.activation == TemplateActivation::Active - 391
}) - 392
.max_by_key(|template| (origin_priority(template.origin), template.revision)) - 393
} - 394
- 395
pub fn activate_proposal(&mut self, id: &str, revision: u32) -> bool { - 396
self.templates.iter_mut().any(|template| { - 397
if template.id == id - 398
&& template.revision == revision - 399
&& template.origin == TemplateOrigin::AgentProposal - 400
{ - 401
template.activation = TemplateActivation::Active; - 402
true - 403
} else { - 404
false - 405
} - 406
}) - 407
} - 408
} - 409
- 410
fn origin_priority(origin: TemplateOrigin) -> u8 { - 411
match origin { - 412
TemplateOrigin::BuiltIn => 1, - 413
TemplateOrigin::AgentProposal => 2, - 414
TemplateOrigin::Project => 3, - 415
TemplateOrigin::User => 4, - 416
} - 417
} - 418
- 419
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 420
#[serde(tag = "type", rename_all = "snake_case")] - 421
pub enum TemplateNode { - 422
Literal { - 423
text: String, - 424
}, - 425
Slot { - 426
slot: TemplateSlot, - 427
}, - 428
IfPresent { - 429
slot: TemplateSlot, - 430
then_nodes: Vec<TemplateNode>, - 431
else_nodes: Vec<TemplateNode>, - 432
}, - 433
} - 434
- 435
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 436
#[serde(rename_all = "snake_case")] - 437
pub enum TemplateSlot { - 438
Title, - 439
Body, - 440
SourceMarkdown, - 441
BlockCount, - 442
Metadata { key: String }, - 443
} - 444
- 445
impl TemplateSpec { - 446
pub fn validate(&self) -> Result<(), DeliveryError> { - 447
if self.id.trim().is_empty() { - 448
return Err(DeliveryError::InvalidTemplate("id is empty".into())); - 449
} - 450
if self.nodes.len() > 128 { - 451
return Err(DeliveryError::InvalidTemplate( - 452
"template has more than 128 top-level nodes".into(), - 453
)); - 454
} - 455
validate_nodes(&self.nodes, 0) - 456
} - 457
- 458
fn render( - 459
&self, - 460
answer: &AnswerDraft, - 461
project_structured: bool, - 462
skills: &SkillRegistry, - 463
) -> Result<String, DeliveryError> { - 464
self.validate()?; - 465
render_nodes(&self.nodes, answer, 0, project_structured, skills) - 466
} - 467
} - 468
- 469
fn validate_nodes(nodes: &[TemplateNode], depth: usize) -> Result<(), DeliveryError> { - 470
if depth > 8 { - 471
return Err(DeliveryError::InvalidTemplate( - 472
"template nesting exceeds 8 levels".into(), - 473
)); - 474
} - 475
for node in nodes { - 476
match node { - 477
TemplateNode::Literal { text } if text.chars().count() > 16_384 => { - 478
return Err(DeliveryError::InvalidTemplate( - 479
"template literal exceeds 16384 characters".into(), - 480
)); - 481
} - 482
TemplateNode::Literal { .. } | TemplateNode::Slot { .. } => {} - 483
TemplateNode::IfPresent { - 484
then_nodes, - 485
else_nodes, - 486
.. - 487
} => { - 488
validate_nodes(then_nodes, depth + 1)?; - 489
validate_nodes(else_nodes, depth + 1)?; - 490
} - 491
} - 492
} - 493
Ok(()) - 494
} - 495
- 496
fn render_nodes( - 497
nodes: &[TemplateNode], - 498
answer: &AnswerDraft, - 499
depth: usize, - 500
project_structured: bool, - 501
skills: &SkillRegistry, - 502
) -> Result<String, DeliveryError> { - 503
if depth > 8 { - 504
return Err(DeliveryError::InvalidTemplate( - 505
"template nesting exceeds 8 levels".into(), - 506
)); - 507
} - 508
let mut out = String::new(); - 509
for node in nodes { - 510
match node { - 511
TemplateNode::Literal { text } => out.push_str(text), - 512
TemplateNode::Slot { slot } => { - 513
out.push_str(&slot_value(slot, answer, project_structured, skills)) - 514
} - 515
TemplateNode::IfPresent { - 516
slot, - 517
then_nodes, - 518
else_nodes, - 519
} => { - 520
let selected = if slot_value(slot, answer, project_structured, skills).is_empty() { - 521
else_nodes - 522
} else { - 523
then_nodes - 524
}; - 525
out.push_str(&render_nodes( - 526
selected, - 527
answer, - 528
depth + 1, - 529
project_structured, - 530
skills, - 531
)?); - 532
} - 533
} - 534
} - 535
Ok(out) - 536
} - 537
- 538
fn slot_value( - 539
slot: &TemplateSlot, - 540
answer: &AnswerDraft, - 541
project_structured: bool, - 542
skills: &SkillRegistry, - 543
) -> String { - 544
match slot { - 545
TemplateSlot::Title => answer - 546
.blocks - 547
.iter() - 548
.find_map(|block| match block { - 549
Block::Heading { text, .. } => Some(text.clone()), - 550
_ => None, - 551
}) - 552
.or_else(|| answer.metadata.get("title").cloned()) - 553
.unwrap_or_default(), - 554
TemplateSlot::Body => render_plain(answer, project_structured, skills), - 555
TemplateSlot::SourceMarkdown => answer.source_markdown.clone(), - 556
TemplateSlot::BlockCount => answer.blocks.len().to_string(), - 557
TemplateSlot::Metadata { key } => answer.metadata.get(key).cloned().unwrap_or_default(), - 558
} - 559
} - 560
- 561
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 562
pub struct DeliveryJob { - 563
pub job_id: String, - 564
pub target: String, - 565
pub kind: DeliveryKind, - 566
pub content: DeliveryContent, - 567
pub profile: DeliveryProfile, - 568
/// Plugin-merged skill registry for structured-fence validation. When - 569
/// `None`, the worker falls back to `built_in_skill_registry()`. The Core - 570
/// populates this from its capability snapshot so that plugin-declared - 571
/// semantic types are recognized during rendering without the worker - 572
/// having filesystem or network access. - 573
#[serde(default, skip_serializing_if = "Option::is_none")] - 574
pub skill_registry: Option<SkillRegistry>, - 575
} - 576
- 577
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 578
pub struct DeliveryPacket { - 579
pub schema_version: u16, - 580
pub job_id: String, - 581
pub target: String, - 582
pub surface: String, - 583
pub kind: DeliveryKind, - 584
pub payload: DeliveryPayload, - 585
/// Exact source is always available for export, audit, and fallback. - 586
pub fallback_markdown: String, - 587
pub chunks: Vec<String>, - 588
pub actions: Vec<DeliveryAction>, - 589
pub coverage: Vec<Coverage>, - 590
pub diagnostics: Vec<String>, - 591
/// Native semantic projection. Text-only consumers can ignore this and - 592
/// continue using `chunks` or `fallback_markdown`. - 593
#[serde(default, skip_serializing_if = "Option::is_none")] - 594
pub presentation: Option<OutputTimeline>, - 595
} - 596
- 597
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 598
#[serde(tag = "type", content = "value", rename_all = "snake_case")] - 599
#[allow(clippy::large_enum_variant)] - 600
pub enum DeliveryPayload { - 601
Text(String), - 602
Structured(DeliveryContent), - 603
} - 604
- 605
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 606
pub struct Coverage { - 607
pub block_id: String, - 608
pub disposition: CoverageDisposition, - 609
} - 610
- 611
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] - 612
#[serde(tag = "type", rename_all = "snake_case")] - 613
pub enum CoverageDisposition { - 614
Rendered, - 615
PreservedInFallback { reason: String }, - 616
} - 617
- 618
#[derive(Debug, Clone, PartialEq, Eq)] - 619
pub enum DeliveryError { - 620
InvalidProfile(String), - 621
InvalidTemplate(String), - 622
InvalidMessage(String), - 623
UnsupportedSchema(u16), - 624
} - 625
- 626
impl std::fmt::Display for DeliveryError { - 627
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - 628
match self { - 629
Self::InvalidProfile(message) => write!(f, "invalid delivery profile: {message}"), - 630
Self::InvalidTemplate(message) => write!(f, "invalid delivery template: {message}"), - 631
Self::InvalidMessage(message) => write!(f, "invalid delivery message: {message}"), - 632
Self::UnsupportedSchema(version) => { - 633
write!(f, "unsupported delivery schema version: {version}") - 634
} - 635
} - 636
} - 637
} - 638
- 639
impl std::error::Error for DeliveryError {} - 640
- 641
pub fn render(job: &DeliveryJob) -> Result<DeliveryPacket, DeliveryError> { - 642
job.profile.validate()?; - 643
if !job.kind.is_external() { - 644
return Err(DeliveryError::InvalidMessage(format!( - 645
"{} messages are control-plane data and must use the event stream", - 646
format_kind(job.kind) - 647
))); - 648
} - 649
- 650
if !content_matches_kind(job.kind, &job.content) { - 651
return Err(DeliveryError::InvalidMessage( - 652
"message kind and content type do not match".into(), - 653
)); - 654
} - 655
if let DeliveryContent::Answer(answer) = &job.content - 656
&& answer.schema_version != DELIVERY_SCHEMA_VERSION - 657
{ - 658
return Err(DeliveryError::UnsupportedSchema(answer.schema_version)); - 659
} - 660
if job.profile.template.is_some() && !job.kind.allows_template() { - 661
return Err(DeliveryError::InvalidTemplate(format!( - 662
"templates are not allowed for {} messages", - 663
format_kind(job.kind) - 664
))); - 665
} - 666
if let Some(template) = job.profile.template.as_ref() - 667
&& template.activation != TemplateActivation::Active - 668
{ - 669
return Err(DeliveryError::InvalidTemplate( - 670
"agent template proposals require explicit activation".into(), - 671
)); - 672
} - 673
- 674
let (rendered, fallback_markdown, actions, coverage) = render_content(job)?; - 675
let chunks = match job.profile.markup { - 676
Markup::TelegramHtml => telegram::split_html_chunks(&rendered, job.profile.max_chars), - 677
Markup::SlackMrkdwn | Markup::DiscordMarkdown => { - 678
chunk_markdown_preserving_fences(&rendered, job.profile.max_chars) - 679
} - 680
_ => chunk_text(&rendered, job.profile.max_chars), - 681
}; - 682
let payload = match job.profile.markup { - 683
Markup::Json => DeliveryPayload::Structured(job.content.clone()), - 684
_ => DeliveryPayload::Text(rendered), - 685
}; - 686
let mut diagnostics = Vec::new(); - 687
if !actions.is_empty() && !job.profile.supports_actions { - 688
diagnostics.push( - 689
"actions preserved separately; target does not declare interactive action support" - 690
.into(), - 691
); - 692
} - 693
- 694
let presentation = presentation_for_job(job); - 695
Ok(DeliveryPacket { - 696
schema_version: DELIVERY_SCHEMA_VERSION, - 697
job_id: job.job_id.clone(), - 698
target: job.target.clone(), - 699
surface: job.profile.surface.clone(), - 700
kind: job.kind, - 701
payload, - 702
fallback_markdown, - 703
chunks, - 704
actions, - 705
coverage, - 706
diagnostics, - 707
presentation, - 708
}) - 709
} - 710
- 711
fn presentation_for_job(job: &DeliveryJob) -> Option<OutputTimeline> { - 712
let DeliveryContent::Answer(answer) = &job.content else { - 713
return None; - 714
}; - 715
let default_status = match ( - 716
answer - 717
.metadata - 718
.get("outcome_completion") - 719
.map(String::as_str), - 720
answer - 721
.metadata - 722
.get("outcome_evidence_state") - 723
.map(String::as_str), - 724
) { - 725
(Some("complete"), None | Some("fresh")) | (None, None) => OutputStatus::Succeeded, - 726
_ => OutputStatus::Partial, - 727
}; - 728
let default_status = if answer - 729
.metadata - 730
.get("outcome_human_review") - 731
.is_some_and(|value| !value.trim().is_empty() && value != "not_required") - 732
{ - 733
OutputStatus::Partial - 734
} else { - 735
default_status - 736
}; - 737
let legacy_document = answer.document.clone(); - 738
let has_typed_results = !answer.results.is_empty(); - 739
let results = if answer.results.is_empty() { - 740
vec![AnswerResult { - 741
id: "answer".into(), - 742
source_markdown: answer.source_markdown.clone(), - 743
outcome: metadata_outcome(&answer.metadata, default_status), - 744
}] - 745
} else { - 746
answer.results.clone() - 747
}; - 748
let mut diagnostics = Vec::new(); - 749
let items = results - 750
.into_iter() - 751
.map(|result| { - 752
let outcome = result.outcome; - 753
let status = outcome.as_ref().map(|value| value.status).unwrap_or(default_status); - 754
if status == OutputStatus::Partial { - 755
diagnostics.push(format!( - 756
"result '{}' is incomplete or unverified; inspect evidence before relying on it", - 757
result.id - 758
)); - 759
} - 760
OutputItem { - 761
id: format!("{}/{}", job.job_id, result.id), - 762
timestamp: String::new(), - 763
turn_id: job.job_id.clone(), - 764
role: OutputRole::Assistant, - 765
kind: match job.kind { - 766
DeliveryKind::Assistant | DeliveryKind::TaskSummary | DeliveryKind::Alert => OutputKind::Outcome, - 767
_ => OutputKind::Message, - 768
}, - 769
status, - 770
outcome, - 771
content: OutputContent::Document { - 772
document: if !has_typed_results - 773
&& result.id == "answer" - 774
&& legacy_document.source_markdown == result.source_markdown - 775
{ - 776
legacy_document.clone() - 777
} else { - 778
compile_markdown(result.source_markdown.clone()) - 779
}, - 780
}, - 781
provenance: None, - 782
actions: Vec::new(), - 783
fallback_text: result.source_markdown, - 784
} - 785
}) - 786
.collect(); - 787
Some(OutputTimeline { - 788
schema_version: presentation::PRESENTATION_SCHEMA_VERSION, - 789
session_id: String::new(), - 790
cursor: None, - 791
items, - 792
diagnostics, - 793
goal: None, - 794
}) - 795
} - 796
- 797
fn metadata_outcome( - 798
metadata: &BTreeMap<String, String>, - 799
status: OutputStatus, - 800
) -> Option<ResultOutcome> { - 801
let completion = metadata.get("outcome_completion").cloned(); - 802
let evidence_state = metadata.get("outcome_evidence_state").cloned(); - 803
let human_review = metadata.get("outcome_human_review").cloned(); - 804
(completion.is_some() || evidence_state.is_some() || human_review.is_some()).then(|| { - 805
ResultOutcome { - 806
result_id: "answer".into(), - 807
status, - 808
completion, - 809
evidence_state, - 810
requirement_ids: Vec::new(), - 811
evidence_receipt_ids: Vec::new(), - 812
evidence: Vec::new(), - 813
human_review, - 814
} - 815
}) - 816
} - 817
- 818
/// Whether the markup type cannot render semantic AST blocks natively and - 819
/// therefore needs ```vak fences projected to readable text before markup - 820
/// conversion. Native surfaces (desktop, tui, admin) and JSON payloads handle - 821
/// structured blocks through the semantic path; all chat and plain-text - 822
/// surfaces need projection. - 823
fn needs_structured_projection(markup: Markup) -> bool { - 824
!matches!(markup, Markup::Json) - 825
} - 826
- 827
fn render_content( - 828
job: &DeliveryJob, - 829
) -> Result<(String, String, Vec<DeliveryAction>, Vec<Coverage>), DeliveryError> { - 830
let project_structured = needs_structured_projection(job.profile.markup); - 831
let skills = job - 832
.skill_registry - 833
.as_ref() - 834
.map(std::borrow::Cow::Borrowed) - 835
.unwrap_or_else(|| std::borrow::Cow::Owned(built_in_skill_registry())); - 836
let skills_ref = &*skills; - 837
match &job.content { - 838
DeliveryContent::Answer(answer) => { - 839
let projected_answer = answer.channel_projection(); - 840
let rendered = match job.profile.template.as_ref() { - 841
Some(template) => { - 842
let text = - 843
template.render(&projected_answer, project_structured, skills_ref)?; - 844
let text = if project_structured { - 845
project_structured_fences_with(&text, skills_ref) - 846
} else { - 847
text - 848
}; - 849
render_text(&text, job.profile.markup) - 850
} - 851
None => render_answer( - 852
&projected_answer, - 853
job.profile.markup, - 854
project_structured, - 855
skills_ref, - 856
), - 857
}; - 858
let coverage = projected_answer - 859
.blocks - 860
.iter() - 861
.map(|block| Coverage { - 862
block_id: block.id().to_string(), - 863
disposition: CoverageDisposition::Rendered, - 864
}) - 865
.collect(); - 866
Ok(( - 867
rendered, - 868
projected_answer.source_markdown, - 869
Vec::new(), - 870
coverage, - 871
)) - 872
} - 873
DeliveryContent::Approval(approval) => Ok(( - 874
render_approval(approval, job.profile.markup), - 875
approval.detail.clone(), - 876
approval.actions.clone(), - 877
Vec::new(), - 878
)), - 879
DeliveryContent::Progress(progress) => Ok(( - 880
render_progress(progress, job.profile.markup), - 881
format!("{}: {}", progress.label, progress.state), - 882
Vec::new(), - 883
Vec::new(), - 884
)), - 885
DeliveryContent::ToolResult(result) => Ok(( - 886
render_tool_result(result, job.profile.markup, project_structured, skills_ref), - 887
result.output.clone(), - 888
Vec::new(), - 889
Vec::new(), - 890
)), - 891
DeliveryContent::Text { markdown } => { - 892
let projected = if project_structured { - 893
project_structured_fences_with(markdown, skills_ref) - 894
} else { - 895
markdown.clone() - 896
}; - 897
Ok(( - 898
render_text(&projected, job.profile.markup), - 899
markdown.clone(), - 900
Vec::new(), - 901
Vec::new(), - 902
)) - 903
} - 904
} - 905
} - 906
- 907
fn content_matches_kind(kind: DeliveryKind, content: &DeliveryContent) -> bool { - 908
matches!( - 909
(kind, content), - 910
( - 911
DeliveryKind::Assistant | DeliveryKind::TaskSummary | DeliveryKind::Alert, - 912
DeliveryContent::Answer(_) - 913
) | (DeliveryKind::User, DeliveryContent::Answer(_)) - 914
| (DeliveryKind::Approval, DeliveryContent::Approval(_)) - 915
| (DeliveryKind::Progress, DeliveryContent::Progress(_)) - 916
| (DeliveryKind::ToolResult, DeliveryContent::ToolResult(_)) - 917
| (_, DeliveryContent::Text { .. }) - 918
) - 919
} - 920
- 921
fn format_kind(kind: DeliveryKind) -> &'static str { - 922
match kind { - 923
DeliveryKind::Assistant => "assistant", - 924
DeliveryKind::TaskSummary => "task_summary", - 925
DeliveryKind::Alert => "alert", - 926
DeliveryKind::User => "user", - 927
DeliveryKind::Approval => "approval", - 928
DeliveryKind::Progress => "progress", - 929
DeliveryKind::ToolResult => "tool_result", - 930
DeliveryKind::System => "system", - 931
DeliveryKind::Developer => "developer", - 932
DeliveryKind::ToolCall => "tool_call", - 933
DeliveryKind::Steering => "steering", - 934
DeliveryKind::Internal => "internal", - 935
} - 936
} - 937
- 938
fn render_answer( - 939
answer: &AnswerDraft, - 940
markup: Markup, - 941
project_structured: bool, - 942
skills: &SkillRegistry, - 943
) -> String { - 944
let metadata_notice = match ( - 945
answer - 946
.metadata - 947
.get("outcome_completion") - 948
.map(String::as_str), - 949
answer - 950
.metadata - 951
.get("outcome_evidence_state") - 952
.map(String::as_str), - 953
) { - 954
(Some("complete"), None | Some("fresh")) | (None, None) => None, - 955
_ => Some( - 956
"⚠️ Outcome incomplete or unverified — review the evidence before relying on this result.\n\n" - 957
.into(), - 958
), - 959
}; - 960
let result_notice = answer.results.iter().find_map(|result| { - 961
let outcome = result.outcome.as_ref()?; - 962
(outcome.status != OutputStatus::Succeeded - 963
|| outcome.evidence_state.as_deref().is_some_and(|state| state != "fresh") - 964
|| outcome.human_review.is_some()) - 965
.then_some(format!( - 966
"⚠️ Result '{}' is incomplete, unverified, or requires review — inspect its evidence before relying on it.\n\n", - 967
result.id - 968
)) - 969
}); - 970
let notice = result_notice.or(metadata_notice); - 971
let raw_source = if project_structured { - 972
project_structured_fences_with(&answer.source_markdown, skills) - 973
} else { - 974
answer.source_markdown.clone() - 975
}; - 976
let source = clean_scaffolding(&raw_source); - 977
match markup { - 978
Markup::Plain => format!( - 979
"{}{}", - 980
notice.unwrap_or_default(), - 981
render_plain(answer, project_structured, skills) - 982
), - 983
Markup::Markdown => format!("{}{}", notice.unwrap_or_default(), source), - 984
Markup::TelegramHtml => { - 985
telegram::markdown_to_html(&format!("{}{}", notice.unwrap_or_default(), source)) - 986
} - 987
Markup::SlackMrkdwn => { - 988
slack::markdown_to_mrkdwn(&format!("{}{}", notice.unwrap_or_default(), source)) - 989
} - 990
Markup::DiscordMarkdown => { - 991
discord::markdown_to_discord(&format!("{}{}", notice.unwrap_or_default(), source)) - 992
} - 993
Markup::Json => String::new(), - 994
} - 995
} - 996
- 997
/// Markup conversion only — does NOT project structured fences. Callers must - 998
/// project first via `project_structured_fences` when the surface doesn't - 999
/// support structured blocks natively. - 1000
fn render_text(markdown: &str, markup: Markup) -> String { - 1001
match markup { - 1002
Markup::TelegramHtml => telegram::markdown_to_html(markdown), - 1003
Markup::SlackMrkdwn => slack::markdown_to_mrkdwn(markdown), - 1004
Markup::DiscordMarkdown => discord::markdown_to_discord(markdown), - 1005
Markup::Plain | Markup::Markdown | Markup::Json => markdown.to_string(), - 1006
} - 1007
} - 1008
- 1009
fn render_progress(progress: &ProgressPayload, markup: Markup) -> String { - 1010
let text = format!("{}: {}", progress.label, progress.state); - 1011
if matches!(markup, Markup::TelegramHtml) { - 1012
escape_html(&text) - 1013
} else { - 1014
text - 1015
} - 1016
} - 1017
- 1018
fn render_tool_result( - 1019
result: &ToolResultPayload, - 1020
markup: Markup, - 1021
project_structured: bool, - 1022
skills: &SkillRegistry, - 1023
) -> String { - 1024
let prefix = if result.is_error { - 1025
"Tool error" - 1026
} else { - 1027
"Tool result" - 1028
}; - 1029
let formatted = format!("{prefix} ({}\n\n{}", result.tool, result.output); - 1030
let text = if project_structured { - 1031
project_structured_fences_with(&formatted, skills) - 1032
} else { - 1033
formatted - 1034
}; - 1035
if matches!(markup, Markup::TelegramHtml) { - 1036
escape_html(&text) - 1037
} else { - 1038
text - 1039
} - 1040
} - 1041
- 1042
fn render_approval(approval: &ApprovalPayload, markup: Markup) -> String { - 1043
let mut text = format!("{}\n\n{}", approval.title, approval.detail); - 1044
if let Some(expires_at) = &approval.expires_at { - 1045
text.push_str(&format!("\n\nExpires: {expires_at}")); - 1046
} - 1047
if !approval.actions.is_empty() { - 1048
text.push_str("\n\nActions: "); - 1049
text.push_str( - 1050
&approval - 1051
.actions - 1052
.iter()
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.