- 1001
.collect::<Vec<_>>() - 1002
.join(","), - 1003
); - 1004
} - 1005
if !rejected_outcome_requirements.is_empty() { - 1006
document.metadata.insert( - 1007
"outcome_requirement_rejections".into(), - 1008
rejected_outcome_requirements.join("; "), - 1009
); - 1010
} - 1011
if let Some(status) = turn_evaluations.get(&turn) { - 1012
let mut parts = status.splitn(2, '|'); - 1013
let outcome_status = - 1014
parts.next().map_or("unknown", |value| value); - 1015
document.metadata.insert( - 1016
"outcome_status".into(), - 1017
outcome_status.into(), - 1018
); - 1019
if let Some(evaluation) = parts.next() { - 1020
let evaluation_json = - 1021
evaluation.split('|').next().map_or("[]", |value| value); - 1022
document.metadata.insert( - 1023
"outcome_evaluation".into(), - 1024
evaluation_json.into(), - 1025
); - 1026
if let Some(receipts) = evaluation.split('|').nth(2) { - 1027
document.metadata.insert( - 1028
"outcome_evidence_receipts".into(), - 1029
receipts.into(), - 1030
); - 1031
} - 1032
if let Some(completion) = evaluation.split('|').nth(3) - 1033
{ - 1034
document.metadata.insert( - 1035
"outcome_completion".into(), - 1036
completion.into(), - 1037
); - 1038
} - 1039
} - 1040
} - 1041
if let Some(evidence_state) = turn_evidence_state.get(&turn) { - 1042
document.metadata.insert( - 1043
"outcome_evidence_state".into(), - 1044
evidence_state.clone(), - 1045
); - 1046
} - 1047
if let Some(review) = turn_human_review.get(&turn) { - 1048
document.metadata.insert( - 1049
"outcome_human_review".into(), - 1050
review.clone(), - 1051
); - 1052
} - 1053
if let Some(verdict) = turn_review_verdict.get(&turn) { - 1054
document.metadata.insert( - 1055
"outcome_review_verdict".into(), - 1056
verdict.clone(), - 1057
); - 1058
} - 1059
document.diagnostics.extend(plan.rejected.iter().map( - 1060
|item| { - 1061
format!("{}: {}", item.semantic_type, item.reason) - 1062
}, - 1063
)); - 1064
if assistant - 1065
&& plan.recipe.as_ref().is_some_and(|decision| { - 1066
decision.requires_typed_output - 1067
}) - 1068
&& !plan.accepted.iter().any(|candidate| { - 1069
plan.recipe.as_ref().is_some_and(|decision| { - 1070
decision.typed_output_types.iter().any(|kind| { - 1071
candidate.semantic_type == *kind - 1072
}) - 1073
}) - 1074
}) - 1075
{ - 1076
document.diagnostics.push( - 1077
"research-shaped prose has no typed evidence contract; rendered as an unverified document".into(), - 1078
); - 1079
} - 1080
document - 1081
}, - 1082
}, - 1083
outcome: Some(result_outcome( - 1084
format!("{}-text-{index}", entry.id), - 1085
output_status, - 1086
projected_outcome.as_ref(), - 1087
turn_evaluations.get(&turn).map(String::as_str), - 1088
turn_evidence_state.get(&turn), - 1089
turn_human_review.get(&turn), - 1090
)), - 1091
provenance: Some(OutputProvenance { - 1092
session_id: Some(session_id.into()), - 1093
entry_id: Some(entry.id.clone()), - 1094
tool_call_id: assistant_tool_call_id, - 1095
source: Some("session_ledger".into()), - 1096
presentation_id: None, - 1097
}), - 1098
actions: Vec::new(), - 1099
fallback_text: text.clone(), - 1100
}); - 1101
if let (Some(library), Some((spec_id, revision))) = - 1102
(adaptive_library, selected_for_output.as_ref()) - 1103
&& let Some(stored) = library.definitions().find(|stored| { - 1104
stored.spec.id == *spec_id && stored.spec.revision == *revision - 1105
}) - 1106
&& let Some(candidate) = plan.accepted.iter().find(|candidate| { - 1107
stored - 1108
.spec - 1109
.accepts - 1110
.iter() - 1111
.any(|kind| kind == &candidate.semantic_type) - 1112
}) - 1113
&& let Some(content) = OutputContent::from_compiled_adaptive( - 1114
vak_presentation::compile( - 1115
&stored.spec, - 1116
&vak_presentation::CompileInput { - 1117
semantic_type: candidate.semantic_type.clone(), - 1118
payload: candidate.payload.clone(), - 1119
fallback_text: text.clone(), - 1120
}, - 1121
), - 1122
text.clone(), - 1123
) - 1124
{ - 1125
timeline.items.push(OutputItem { - 1126
id: format!("{}-adaptive", entry.id), - 1127
timestamp: entry.ts.to_rfc3339(), - 1128
turn_id: turn_id.clone(), - 1129
role: OutputRole::Assistant, - 1130
kind: OutputKind::Outcome, - 1131
status: output_status, - 1132
outcome: None, - 1133
content, - 1134
provenance: Some(OutputProvenance { - 1135
session_id: Some(session_id.into()), - 1136
entry_id: Some(entry.id.clone()), - 1137
tool_call_id: None, - 1138
source: Some("adaptive_library".into()), - 1139
presentation_id: None, - 1140
}), - 1141
actions: Vec::new(), - 1142
fallback_text: text.clone(), - 1143
}); - 1144
} - 1145
for (link_index, preview) in plan - 1146
.accepted - 1147
.into_iter() - 1148
.filter(|candidate| candidate.semantic_type == "link.preview") - 1149
.enumerate() - 1150
{ - 1151
timeline.items.push(OutputItem { - 1152
id: format!("{}-link-{link_index}", entry.id), - 1153
timestamp: entry.ts.to_rfc3339(), - 1154
turn_id: turn_id.clone(), - 1155
role: OutputRole::Assistant, - 1156
kind: OutputKind::Card, - 1157
status: OutputStatus::Succeeded, - 1158
outcome: None, - 1159
content: OutputContent::Structured { - 1160
output: preview.clone(), - 1161
}, - 1162
provenance: Some(OutputProvenance { - 1163
session_id: Some(session_id.into()), - 1164
entry_id: Some(entry.id.clone()), - 1165
tool_call_id: None, - 1166
source: Some("link_extractor".into()), - 1167
presentation_id: None, - 1168
}), - 1169
actions: Vec::new(), - 1170
fallback_text: preview - 1171
.payload - 1172
.get("url") - 1173
.and_then(serde_json::Value::as_str) - 1174
.unwrap_or_default() - 1175
.into(), - 1176
}); - 1177
} - 1178
} - 1179
ContentBlock::ToolUse { id, name, input } => { - 1180
let result = tool_results.get(id); - 1181
let failed = result.is_some_and(|(_, failed)| *failed); - 1182
let recovered = failed && successful_runs.contains(&turn); - 1183
let detail = result.map(|(content, _)| content.clone()); - 1184
timeline.items.push(OutputItem { - 1185
id: id.clone(), - 1186
timestamp: entry.ts.to_rfc3339(), - 1187
turn_id: turn_id.clone(), - 1188
role: OutputRole::Tool, - 1189
kind: if failed && !recovered { - 1190
OutputKind::Error - 1191
} else { - 1192
OutputKind::Progress - 1193
}, - 1194
outcome: None, - 1195
status: if failed { - 1196
OutputStatus::Failed - 1197
} else { - 1198
OutputStatus::Succeeded - 1199
}, - 1200
content: if failed && !recovered { - 1201
OutputContent::Error { - 1202
message: detail - 1203
.clone() - 1204
.unwrap_or_else(|| "Tool failed".into()), - 1205
source: Some(name.clone()), - 1206
retryable: false, - 1207
} - 1208
} else { - 1209
OutputContent::Progress { - 1210
label: if recovered { - 1211
format!("Recovered {name}") - 1212
} else { - 1213
name.clone() - 1214
}, - 1215
detail: detail.clone(), - 1216
percent: None, - 1217
} - 1218
}, - 1219
provenance: Some(OutputProvenance { - 1220
session_id: Some(session_id.into()), - 1221
entry_id: Some(entry.id.clone()), - 1222
tool_call_id: Some(id.clone()), - 1223
source: Some("tool_call".into()), - 1224
presentation_id: None, - 1225
}), - 1226
actions: Vec::new(), - 1227
fallback_text: detail - 1228
.clone() - 1229
.unwrap_or_else(|| format!("{name} completed")), - 1230
}); - 1231
// A tool renders richly one of two ways, neither of - 1232
// which names the tool: it self-declares a - 1233
// `semantic_type` in its own result (fenced or bare — - 1234
// `structured_outputs_from_text`), or — since most - 1235
// tools are third-party and cannot be asked to adopt - 1236
// our envelope — a registered `ResultAdapter` - 1237
// recognizes its specific, known response shape - 1238
// (`built_in_adapters`). Either way the candidate - 1239
// still has to pass `SkillRegistry::validate` before - 1240
// anything renders from it, and the tool's own - 1241
// stored result is read, never rewritten. - 1242
if !failed { - 1243
// An `emit_*_card` call's card is read from - 1244
// its own `Presentation` ledger entry - 1245
// (docs/design/68-context-engine.md §10), - 1246
// keyed by this call's `tool_use_id` — - 1247
// written once, at validation, and never - 1248
// rebuilt from the call's arguments here. - 1249
let name_is_card = vak_core::presentation_tools::is_card_tool(name); - 1250
let recorded = |(entry_id, record): &( - 1251
String, - 1252
&vak_session::types::PresentationRecord, - 1253
)| { - 1254
( - 1255
vak_delivery::StructuredOutput { - 1256
semantic_type: record.semantic_type.clone(), - 1257
schema_version: u16::try_from(record.schema_version) - 1258
.unwrap_or(u16::MAX), - 1259
skill_id: record.skill_id.clone(), - 1260
skill_version: record.skill_version.clone(), - 1261
payload: record.payload.clone(), - 1262
}, - 1263
Some(entry_id.clone()), - 1264
) - 1265
}; - 1266
// Each output with the Presentation entry it - 1267
// was written as, when it has one. - 1268
let outputs: Vec<(vak_delivery::StructuredOutput, Option<String>)> = - 1269
if name_is_card { - 1270
presentation_by_tool_use_id - 1271
.get(id) - 1272
.map(|(entry_id, record)| { - 1273
recorded(&(entry_id.clone(), *record)) - 1274
}) - 1275
.into_iter() - 1276
.collect() - 1277
} else { - 1278
let mut outputs: Vec<_> = delegated_by_tool_use_id - 1279
.get(id) - 1280
.map(|cards| cards.iter().map(recorded).collect()) - 1281
.unwrap_or_default(); - 1282
outputs.extend( - 1283
detail - 1284
.as_deref() - 1285
.map(|text| { - 1286
structured_outputs_from_tool_result_with( - 1287
text, - 1288
"desktop", - 1289
&built_in_adapters(), - 1290
&planner.skills, - 1291
) - 1292
}) - 1293
.unwrap_or_default() - 1294
.into_iter() - 1295
.map(|output| (output, None)), - 1296
); - 1297
outputs - 1298
}; - 1299
for (structured_index, (output, presentation_entry_id)) in - 1300
outputs.into_iter().enumerate() - 1301
{ - 1302
// The same card emitted twice in one answer is one - 1303
// card, armed retry or not: identical content has - 1304
// nothing to supersede and nothing to add. - 1305
if name_is_card - 1306
&& !seen_cards.insert( - 1307
serde_json::to_string(&output).unwrap_or_default(), - 1308
) - 1309
{ - 1310
continue; - 1311
} - 1312
let item_id = format!("{id}-structured-{structured_index}"); - 1313
let previous = card_group_by_type - 1314
.insert(output.semantic_type.clone(), item_id.clone()); - 1315
if repair_armed && let Some(superseded) = previous { - 1316
ids_to_remove.insert(superseded); - 1317
} - 1318
let fallback_text = structured_markdown(&output); - 1319
let workspace_owner = session - 1320
.header() - 1321
.map(|header| { - 1322
header.contract_cwd().to_string_lossy().into_owned() - 1323
}) - 1324
.unwrap_or_else(|| "workspace".into()); - 1325
let selected = adaptive_library.and_then(|library| { - 1326
selected_presentation - 1327
.as_ref() - 1328
.and_then(|(spec_id, revision)| { - 1329
library.get(spec_id, *revision) - 1330
}) - 1331
.filter(|stored| { - 1332
stored.spec.accepts.contains(&output.semantic_type) - 1333
}) - 1334
.or_else(|| { - 1335
library.select_preferred( - 1336
&output.semantic_type, - 1337
"user", - 1338
&workspace_owner, - 1339
) - 1340
}) - 1341
}); - 1342
let adaptive = selected - 1343
.filter(|stored| { - 1344
stored.spec.metadata.get("seed").map(String::as_str) - 1345
!= Some("true") - 1346
|| stored - 1347
.spec - 1348
.metadata - 1349
.get("certified") - 1350
.map(String::as_str) - 1351
== Some("true") - 1352
}) - 1353
.and_then(|stored| { - 1354
OutputContent::from_compiled_adaptive( - 1355
vak_presentation::compile( - 1356
&stored.spec, - 1357
&vak_presentation::CompileInput { - 1358
semantic_type: output.semantic_type.clone(), - 1359
payload: output.payload.clone(), - 1360
fallback_text: fallback_text.clone(), - 1361
}, - 1362
), - 1363
fallback_text.clone(), - 1364
) - 1365
}); - 1366
timeline.items.push(OutputItem { - 1367
id: item_id, - 1368
timestamp: entry.ts.to_rfc3339(), - 1369
turn_id: turn_id.clone(), - 1370
role: OutputRole::Tool, - 1371
kind: OutputKind::Card, - 1372
status: OutputStatus::Succeeded, - 1373
outcome: None, - 1374
fallback_text, - 1375
content: adaptive - 1376
.unwrap_or(OutputContent::Structured { output }), - 1377
provenance: Some(OutputProvenance { - 1378
session_id: Some(session_id.into()), - 1379
entry_id: Some(entry.id.clone()), - 1380
tool_call_id: Some(id.clone()), - 1381
source: Some(name.clone()), - 1382
presentation_id: presentation_entry_id, - 1383
}), - 1384
actions: Vec::new(), - 1385
}); - 1386
} - 1387
} - 1388
if !failed && let Some(artifact) = artifact_from_tool(name, input) { - 1389
let mut data = BTreeMap::new(); - 1390
if let Some(path) = &artifact.path { - 1391
data.insert("path".into(), path.clone()); - 1392
} - 1393
timeline.items.push(OutputItem { - 1394
id: format!("{id}-artifact"), - 1395
timestamp: entry.ts.to_rfc3339(), - 1396
turn_id: turn_id.clone(), - 1397
role: OutputRole::Tool, - 1398
kind: OutputKind::Artifact, - 1399
status: OutputStatus::Succeeded, - 1400
outcome: None, - 1401
content: OutputContent::Artifact { - 1402
artifact: artifact.clone(), - 1403
}, - 1404
provenance: Some(OutputProvenance { - 1405
session_id: Some(session_id.into()), - 1406
entry_id: Some(entry.id.clone()), - 1407
tool_call_id: Some(id.clone()), - 1408
source: Some(name.clone()), - 1409
presentation_id: None, - 1410
}), - 1411
actions: vec![DeliveryAction { - 1412
id: format!("open-{id}"), - 1413
label: "Open".into(), - 1414
verb: "open_artifact".into(), - 1415
data, - 1416
}], - 1417
fallback_text: artifact - 1418
.path - 1419
.clone() - 1420
.unwrap_or_else(|| artifact.name.clone()), - 1421
}); - 1422
} - 1423
} - 1424
_ => {} - 1425
} - 1426
} - 1427
} - 1428
EntryPayload::Activity(activity) if is_user_facing_activity(activity) => { - 1429
timeline.items.push(activity_item( - 1430
session_id, - 1431
&entry.id, - 1432
&entry.ts.to_rfc3339(), - 1433
&format!("turn-{}", activity.turn.unwrap_or(turn)), - 1434
activity, - 1435
)); - 1436
} - 1437
// Goal changes reach the client through `timeline.goal_state`; - 1438
// a per-update progress row is runtime bookkeeping. - 1439
EntryPayload::GoalUpdate(_) => {} - 1440
_ => {} - 1441
} - 1442
} - 1443
let mut positions = HashMap::new(); - 1444
let mut deduplicated = Vec::with_capacity(timeline.items.len()); - 1445
for item in timeline.items.drain(..) { - 1446
if let Some(index) = positions.get(&item.id).copied() { - 1447
deduplicated[index] = item; - 1448
} else { - 1449
positions.insert(item.id.clone(), deduplicated.len()); - 1450
deduplicated.push(item); - 1451
} - 1452
} - 1453
if !rejected_drafts.is_empty() { - 1454
deduplicated.retain(|item| { - 1455
!(item.role == OutputRole::Assistant - 1456
&& item - 1457
.provenance - 1458
.as_ref() - 1459
.and_then(|p| p.entry_id.as_ref()) - 1460
.is_some_and(|entry| rejected_drafts.contains(entry))) - 1461
}); - 1462
} - 1463
if !ids_to_remove.is_empty() { - 1464
deduplicated.retain(|item| !ids_to_remove.contains(&item.id)); - 1465
} - 1466
deduplicate_file_artifacts(&mut deduplicated); - 1467
let card_turns: std::collections::HashSet<String> = deduplicated - 1468
.iter() - 1469
.filter(|item| { - 1470
item.kind == OutputKind::Card - 1471
&& item - 1472
.provenance - 1473
.as_ref() - 1474
.and_then(|p| p.source.as_deref()) - 1475
.is_some_and(|source| source.starts_with("emit_") && source.ends_with("_card")) - 1476
}) - 1477
.map(|item| item.turn_id.clone()) - 1478
.collect(); - 1479
for item in &mut deduplicated { - 1480
if !card_turns.contains(&item.turn_id) || item.role != OutputRole::Assistant { - 1481
continue; - 1482
} - 1483
let document = match &mut item.content { - 1484
OutputContent::Document { document } => Some(document), - 1485
OutputContent::Outcome { document, .. } => document.as_mut(), - 1486
_ => None, - 1487
}; - 1488
if let Some(document) = document - 1489
&& let Some(note) = vak_delivery::supplemental_card_note(&document.source_markdown) - 1490
{ - 1491
document.metadata.insert("card_note".into(), note.into()); - 1492
} - 1493
} - 1494
timeline.items = deduplicated; - 1495
timeline.cursor = chain_cursor(session); - 1496
log_turns_with_no_visible_answer(session_id, &timeline); - 1497
timeline - 1498
} - 1499
- 1500
/// Mirrors the client's `Turn` filter (vak-client-ui's - 1501
/// `PresentationRenderer.tsx`): a turn is a *real answer* only if it carries - 1502
/// a `Document`/`Structured`/`Adaptive` item, or an `Outcome` with a - 1503
/// document attached. Everything else — bare outcomes, tool errors, - 1504
/// progress/retry/information — is invisible to a non-operator client. - 1505
/// - 1506
/// If a turn has items but none of them qualify, the chat pane renders - 1507
/// nothing for it (now backstopped by a "no result" notice client-side, - 1508
/// but that's a fallback, not an explanation). Log it here so *why* is - 1509
/// inspectable from this process's log instead of only guessable from the - 1510
/// UI after the fact. - 1511
/// A `PresentationDocument` with no blocks and no source markdown renders - 1512
/// as a literal empty `<div>` client-side (see `PresentationDocumentView` - 1513
/// in vak-client-ui's PresentationRenderer.tsx) — the client now shows a - 1514
/// "no result" notice for that case too, but it still counts as "nothing" - 1515
/// for diagnostic purposes here. - 1516
fn document_has_content(document: &PresentationDocument) -> bool { - 1517
!document.blocks.is_empty() || !document.source_markdown.trim().is_empty() - 1518
} - 1519
- 1520
fn log_turns_with_no_visible_answer(session_id: &str, timeline: &OutputTimeline) { - 1521
static WARNED: OnceLock<Mutex<HashSet<String>>> = OnceLock::new(); - 1522
let mut turns: BTreeMap<&str, Vec<&OutputItem>> = BTreeMap::new(); - 1523
for item in &timeline.items { - 1524
turns.entry(item.turn_id.as_str()).or_default().push(item); - 1525
} - 1526
for (turn_id, items) in turns { - 1527
let has_real_answer = items.iter().any(|item| match &item.content { - 1528
OutputContent::Document { document } => document_has_content(document), - 1529
OutputContent::Structured { .. } | OutputContent::Adaptive { .. } => true, - 1530
OutputContent::Outcome { document, .. } => { - 1531
document.as_ref().is_some_and(document_has_content) - 1532
} - 1533
_ => false, - 1534
}); - 1535
if has_real_answer { - 1536
continue; - 1537
} - 1538
let non_progress: Vec<&&OutputItem> = items - 1539
.iter() - 1540
.filter(|item| { - 1541
!matches!( - 1542
item.kind, - 1543
OutputKind::Progress | OutputKind::Retry | OutputKind::Information - 1544
) - 1545
}) - 1546
.collect(); - 1547
if non_progress.is_empty() { - 1548
// Nothing happened in this turn yet (still streaming) — not a failure. - 1549
continue; - 1550
} - 1551
let is_settled = non_progress - 1552
.iter() - 1553
.any(|item| !matches!(item.status, OutputStatus::Pending | OutputStatus::Running)); - 1554
if !is_settled { - 1555
// The turn's only non-progress item is itself still Pending/Running - 1556
// (e.g. a bare "Outcome/Running" marker) — this fires on every - 1557
// poll of a turn that simply hasn't finished yet, not a failure. - 1558
continue; - 1559
} - 1560
let kinds: Vec<String> = non_progress - 1561
.iter() - 1562
.map(|item| format!("{:?}/{:?}", item.kind, item.status)) - 1563
.collect(); - 1564
let fingerprint = format!("{session_id}\0{turn_id}\0{}", kinds.join(",")); - 1565
let mut warned = WARNED - 1566
.get_or_init(|| Mutex::new(HashSet::new())) - 1567
.lock() - 1568
.unwrap_or_else(std::sync::PoisonError::into_inner); - 1569
if warned.contains(&fingerprint) { - 1570
continue; - 1571
} - 1572
// This is diagnostic process state, not session truth. Bound it so a - 1573
// long-lived daemon cannot grow forever; clearing may repeat an old - 1574
// warning once, which is preferable to unbounded memory or per-poll - 1575
// log spam. - 1576
if warned.len() >= 4096 { - 1577
warned.clear(); - 1578
} - 1579
warned.insert(fingerprint); - 1580
eprintln!( - 1581
"[projection] session={session_id} turn={turn_id} produced no visible answer ({} non-progress item(s): {}) — client renders a fallback notice for this turn", - 1582
non_progress.len(), - 1583
kinds.join(", ") - 1584
); - 1585
} - 1586
} - 1587
- 1588
fn chain_cursor(session: &SessionLog) -> Option<String> { - 1589
session.chain_to_root().last().map(|entry| entry.id.clone()) - 1590
} - 1591
- 1592
fn artifact_from_tool(name: &str, input: &serde_json::Value) -> Option<ArtifactRef> { - 1593
let name = vak_tools::canonical_tool_name(name); - 1594
if !matches!(name, "write" | "edit" | "apply_patch" | "imagegen") { - 1595
return None; - 1596
} - 1597
let path = ["path", "file_path", "filename", "output_path"] - 1598
.iter() - 1599
.find_map(|key| input.get(*key).and_then(serde_json::Value::as_str))? - 1600
.to_string(); - 1601
let filename = std::path::Path::new(&path) - 1602
.file_name() - 1603
.and_then(|name| name.to_str()) - 1604
.unwrap_or(&path) - 1605
.to_string(); - 1606
Some(ArtifactRef { - 1607
name: filename, - 1608
media_type: media_type_for_path(&path), - 1609
path: Some(path), - 1610
description: None, - 1611
size_bytes: None, - 1612
status: Some(ArtifactStatus::InFolder), - 1613
}) - 1614
} - 1615
- 1616
fn media_type_for_path(path: &str) -> Option<String> { - 1617
let extension = std::path::Path::new(path).extension()?.to_str()?; - 1618
Some( - 1619
match extension.to_ascii_lowercase().as_str() { - 1620
"png" => "image/png", - 1621
"jpg" | "jpeg" => "image/jpeg", - 1622
"webp" => "image/webp", - 1623
"svg" => "image/svg+xml", - 1624
"html" | "htm" => "text/html", - 1625
"css" => "text/css", - 1626
"js" | "mjs" | "cjs" => "text/javascript", - 1627
"ts" | "tsx" => "text/typescript", - 1628
"pdf" => "application/pdf", - 1629
"md" => "text/markdown", - 1630
"json" => "application/json", - 1631
"csv" => "text/csv", - 1632
_ => "text/plain", - 1633
} - 1634
.into(), - 1635
) - 1636
} - 1637
- 1638
fn is_presentation_envelope(text: &str) -> bool { - 1639
let lines = text - 1640
.lines() - 1641
.map(str::trim) - 1642
.filter(|line| !line.is_empty()) - 1643
.collect::<Vec<_>>(); - 1644
!lines.is_empty() && lines.iter().all(|line| is_scaffolding_line(line)) - 1645
} - 1646
- 1647
/// Whether an activity is something a person did or would recognise as - 1648
/// progress on their request. Everything else (admission, capacity, - 1649
/// diagnostics, retries, route fallbacks, voice and presentation accounting) - 1650
/// is runtime bookkeeping that stays in the ledger and never reaches a client. - 1651
fn is_user_facing_activity(activity: &vak_session::ActivityRecord) -> bool { - 1652
match activity.kind { - 1653
ActivityKind::Approval - 1654
| ActivityKind::Worker - 1655
| ActivityKind::CandidateComment - 1656
| ActivityKind::CandidateRevision => true, - 1657
ActivityKind::Run => activity.label != "Request accepted", - 1658
_ => false, - 1659
} - 1660
} - 1661
- 1662
fn activity_item( - 1663
session_id: &str, - 1664
entry_id: &str, - 1665
timestamp: &str, - 1666
turn_id: &str, - 1667
activity: &vak_session::ActivityRecord, - 1668
) -> OutputItem { - 1669
let status = match activity.status { - 1670
ActivityStatus::Pending => OutputStatus::Pending, - 1671
ActivityStatus::Running => OutputStatus::Running, - 1672
ActivityStatus::Succeeded => OutputStatus::Succeeded, - 1673
ActivityStatus::Failed => OutputStatus::Failed, - 1674
ActivityStatus::Denied => OutputStatus::Denied, - 1675
ActivityStatus::Cancelled => OutputStatus::Cancelled, - 1676
ActivityStatus::Partial => OutputStatus::Partial, - 1677
}; - 1678
let (role, kind, content) = match activity.kind { - 1679
ActivityKind::Approval => ( - 1680
OutputRole::System, - 1681
OutputKind::Approval, - 1682
OutputContent::Approval { - 1683
request_id: activity.data.get("request_id").cloned().unwrap_or_default(), - 1684
tool: activity.data.get("tool").cloned().unwrap_or_default(), - 1685
args_json: activity.data.get("args_json").cloned().unwrap_or_default(), - 1686
reason: activity.detail.clone().unwrap_or_default(), - 1687
expires_at: None, - 1688
}, - 1689
), - 1690
ActivityKind::Retry => ( - 1691
OutputRole::System, - 1692
OutputKind::Retry, - 1693
OutputContent::Retry { - 1694
attempt: activity - 1695
.data - 1696
.get("attempt") - 1697
.and_then(|value| value.parse().ok()) - 1698
.unwrap_or(1), - 1699
delay_ms: activity - 1700
.data - 1701
.get("delay_ms") - 1702
.and_then(|value| value.parse().ok()) - 1703
.unwrap_or(0), - 1704
reason: activity.detail.clone().unwrap_or_default(), - 1705
}, - 1706
), - 1707
ActivityKind::Diagnostic if activity.label == "Outcome evaluation" => ( - 1708
OutputRole::System, - 1709
OutputKind::Outcome, - 1710
OutputContent::Outcome { - 1711
summary: activity - 1712
.detail - 1713
.clone() - 1714
.unwrap_or_else(|| activity.label.clone()), - 1715
document: None, - 1716
}, - 1717
), - 1718
ActivityKind::Diagnostic - 1719
if matches!( - 1720
activity.label.as_str(), - 1721
"Run paused" | "Run resumed" | "Run cancelled" | "Intervention queued" - 1722
) => - 1723
{ - 1724
( - 1725
OutputRole::System, - 1726
OutputKind::Information, - 1727
OutputContent::Information { - 1728
label: activity.label.clone(), - 1729
detail: activity - 1730
.detail - 1731
.clone() - 1732
.or_else(|| activity.data.get("reason").cloned()), - 1733
}, - 1734
) - 1735
} - 1736
ActivityKind::RouteFallback - 1737
| ActivityKind::Diagnostic - 1738
| ActivityKind::VoiceTranscript - 1739
| ActivityKind::VoicePlayback - 1740
| ActivityKind::PresentationSelection - 1741
| ActivityKind::PresentationProposal - 1742
| ActivityKind::PresentationFeedback - 1743
| ActivityKind::CapacityProbe - 1744
| ActivityKind::CapacityFeedback => ( - 1745
OutputRole::System, - 1746
OutputKind::Information, - 1747
OutputContent::Information { - 1748
label: activity.label.clone(), - 1749
detail: activity.detail.clone(), - 1750
}, - 1751
), - 1752
ActivityKind::CandidateComment => ( - 1753
OutputRole::User, - 1754
OutputKind::Information, - 1755
OutputContent::Information { - 1756
label: activity - 1757
.data - 1758
.get("path") - 1759
.map(|path| format!("Comment on {path}")) - 1760
.unwrap_or_else(|| "Comment on saved draft".into()), - 1761
detail: activity.data.get("comment").cloned(), - 1762
}, - 1763
), - 1764
ActivityKind::CandidateRevision => ( - 1765
OutputRole::Assistant, - 1766
OutputKind::Progress, - 1767
OutputContent::Progress { - 1768
label: activity.label.clone(), - 1769
detail: activity.detail.clone(), - 1770
percent: None, - 1771
}, - 1772
), - 1773
ActivityKind::Worker => ( - 1774
OutputRole::Worker, - 1775
OutputKind::Progress, - 1776
OutputContent::Progress { - 1777
label: activity.label.clone(), - 1778
detail: activity.detail.clone(), - 1779
percent: None, - 1780
}, - 1781
), - 1782
// Admission bookkeeping (`vak-server/src/lib.rs`'s `run`/`steering` - 1783
// handlers append this to guard duplicate request ids). It is never - 1784
// a real outcome; without this guard it fell into the generic - 1785
// `ActivityKind::Run` arm below and rendered as a fake completed - 1786
// "Request accepted" answer (docs/audits Finding 3). - 1787
ActivityKind::Run if activity.label == "Request accepted" => ( - 1788
OutputRole::System, - 1789
OutputKind::Information, - 1790
OutputContent::Information { - 1791
label: activity.label.clone(), - 1792
detail: None, - 1793
}, - 1794
), - 1795
ActivityKind::Run => ( - 1796
OutputRole::Assistant, - 1797
if matches!( - 1798
status, - 1799
OutputStatus::Failed - 1800
| OutputStatus::Denied - 1801
| OutputStatus::Cancelled - 1802
| OutputStatus::Partial - 1803
) { - 1804
OutputKind::Error - 1805
} else { - 1806
OutputKind::Outcome - 1807
}, - 1808
if matches!( - 1809
status, - 1810
OutputStatus::Failed - 1811
| OutputStatus::Denied - 1812
| OutputStatus::Cancelled - 1813
| OutputStatus::Partial - 1814
) { - 1815
// `activity.detail` is internal bookkeeping recorded at run - 1816
// completion and can legitimately be a raw provider/internal - 1817
// error string (docs/audits Finding 3); it never reaches an - 1818
// item's text. The typed `status` alone selects one of the - 1819
// same small human sentences `ClientEvent::RunFinished` - 1820
// uses (`client_events::run_outcome_message`). - 1821
use crate::client_events::{RunOutcome, run_outcome_message}; - 1822
let message = match status { - 1823
OutputStatus::Denied => "This step was denied.", - 1824
OutputStatus::Cancelled => run_outcome_message(RunOutcome::Stopped), - 1825
OutputStatus::Partial => run_outcome_message(RunOutcome::MaxTurns), - 1826
_ => run_outcome_message(RunOutcome::Failed), - 1827
}; - 1828
OutputContent::Error { - 1829
message: message.into(), - 1830
source: Some("run".into()), - 1831
retryable: false, - 1832
} - 1833
} else { - 1834
OutputContent::Outcome { - 1835
summary: activity - 1836
.detail - 1837
.clone() - 1838
.unwrap_or_else(|| activity.label.clone()), - 1839
document: None, - 1840
} - 1841
}, - 1842
), - 1843
}; - 1844
let actions = if activity.label == "Outcome evaluation" && status == OutputStatus::Succeeded { - 1845
[ - 1846
("accepted", "Accept result"), - 1847
("needs_work", "Mark needs work"), - 1848
("rejected", "Reject result"), - 1849
] - 1850
.into_iter() - 1851
.map(|(verdict, label)| DeliveryAction { - 1852
id: format!("review-{verdict}-{}", activity.activity_id), - 1853
label: label.into(), - 1854
verb: "record_outcome_review".into(), - 1855
data: { - 1856
let mut data = BTreeMap::from([ - 1857
("session_id".into(), session_id.into()), - 1858
("verdict".into(), verdict.into()), - 1859
]); - 1860
if let Some(turn) = activity.turn { - 1861
data.insert("turn".into(), turn.to_string()); - 1862
} - 1863
data - 1864
}, - 1865
}) - 1866
.collect() - 1867
} else if kind == OutputKind::Approval && status == OutputStatus::Pending { - 1868
let request_id = activity.data.get("request_id").cloned().unwrap_or_default(); - 1869
vec![ - 1870
DeliveryAction { - 1871
id: format!("approve-{request_id}"), - 1872
label: "Allow once".into(), - 1873
verb: "resolve_approval".into(), - 1874
data: BTreeMap::from([ - 1875
("request_id".into(), request_id.clone()), - 1876
("verdict".into(), "allow".into()), - 1877
]), - 1878
}, - 1879
DeliveryAction { - 1880
id: format!("deny-{request_id}"), - 1881
label: "Deny".into(), - 1882
verb: "resolve_approval".into(), - 1883
data: BTreeMap::from([ - 1884
("request_id".into(), request_id), - 1885
("verdict".into(), "deny".into()), - 1886
]), - 1887
}, - 1888
] - 1889
} else { - 1890
Vec::new() - 1891
}; - 1892
OutputItem { - 1893
id: activity.activity_id.clone(), - 1894
timestamp: timestamp.into(), - 1895
turn_id: turn_id.into(), - 1896
role, - 1897
kind, - 1898
status, - 1899
outcome: None, - 1900
content, - 1901
provenance: Some(OutputProvenance { - 1902
session_id: Some(session_id.into()), - 1903
entry_id: Some(entry_id.into()), - 1904
tool_call_id: None, - 1905
source: Some("activity_ledger".into()), - 1906
presentation_id: None, - 1907
}), - 1908
actions, - 1909
fallback_text: activity - 1910
.detail - 1911
.clone() - 1912
.unwrap_or_else(|| activity.label.clone()), - 1913
} - 1914
} - 1915
- 1916
pub(crate) fn live_event(session_id: &str, event: AgentEvent) -> Option<OutputStreamEvent> { - 1917
let now = chrono::Utc::now().to_rfc3339(); - 1918
match event { - 1919
AgentEvent::TurnStart { turn } => Some(OutputStreamEvent::ItemStarted { - 1920
item: OutputItem { - 1921
id: "live-assistant".into(), - 1922
timestamp: now, - 1923
turn_id: format!("turn-{turn}"), - 1924
role: OutputRole::Assistant, - 1925
kind: OutputKind::Outcome, - 1926
status: OutputStatus::Running, - 1927
outcome: None, - 1928
content: OutputContent::Document { - 1929
document: compile_markdown(""), - 1930
}, - 1931
provenance: Some(OutputProvenance { - 1932
session_id: Some(session_id.into()), - 1933
entry_id: None, - 1934
tool_call_id: None, - 1935
source: Some("live_event".into()), - 1936
presentation_id: None, - 1937
}), - 1938
actions: Vec::new(), - 1939
fallback_text: String::new(), - 1940
}, - 1941
}), - 1942
AgentEvent::Stream(vak_llm::StreamEvent::TextDelta { delta, partial }) => { - 1943
Some(OutputStreamEvent::TextDelta { - 1944
item_id: "live-assistant".into(), - 1945
delta, - 1946
text: partial.text_content(), - 1947
}) - 1948
} - 1949
AgentEvent::ToolCallStart { id, name, .. } => Some(OutputStreamEvent::ItemStarted { - 1950
item: live_item( - 1951
session_id, - 1952
id, - 1953
now, - 1954
OutputRole::Tool, - 1955
OutputKind::Progress, - 1956
OutputStatus::Running, - 1957
OutputContent::Progress { - 1958
label: name.clone(), - 1959
detail: None, - 1960
percent: None, - 1961
}, - 1962
format!("{name} running"), - 1963
), - 1964
}), - 1965
AgentEvent::ToolCallEnd { - 1966
id, - 1967
name, - 1968
is_error, - 1969
result_preview, - 1970
} => Some(OutputStreamEvent::ItemReplaced { - 1971
item: live_item( - 1972
session_id, - 1973
id, - 1974
now, - 1975
OutputRole::Tool, - 1976
if is_error { - 1977
OutputKind::Error - 1978
} else { - 1979
OutputKind::Progress - 1980
}, - 1981
if is_error { - 1982
OutputStatus::Failed - 1983
} else { - 1984
OutputStatus::Succeeded - 1985
}, - 1986
if is_error { - 1987
OutputContent::Error { - 1988
message: result_preview - 1989
.clone() - 1990
.unwrap_or_else(|| "Tool failed".into()), - 1991
source: Some(name), - 1992
retryable: false, - 1993
} - 1994
} else { - 1995
OutputContent::Progress { - 1996
label: name, - 1997
detail: result_preview.clone(), - 1998
percent: None, - 1999
} - 2000
},
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.