- 2006
assert!(!text.contains("```vak"), "raw fence must be projected"); - 2007
} - 2008
- 2009
#[test] - 2010
fn structured_fence_projected_on_discord_markdown() { - 2011
let source = "Done.\n\n```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"latency\",\"value\":42}}\n```\n"; - 2012
let input = DeliveryJob { - 2013
job_id: "struct-3".into(), - 2014
target: "test:one".into(), - 2015
kind: DeliveryKind::Assistant, - 2016
content: DeliveryContent::Answer(AnswerDraft::from_markdown(source)), - 2017
profile: DeliveryProfile { - 2018
surface: "discord".into(), - 2019
markup: Markup::DiscordMarkdown, - 2020
max_chars: Some(1900), - 2021
supports_tables: false, - 2022
supports_code_blocks: true, - 2023
supports_links: true, - 2024
supports_actions: false, - 2025
template: None, - 2026
posture: DeliveryPosture::default(), - 2027
}, - 2028
skill_registry: None, - 2029
}; - 2030
let packet = render(&input).expect("valid delivery job"); - 2031
let text = fallback_text(&packet); - 2032
assert!(text.contains("latency"), "metric label should appear"); - 2033
assert!(text.contains("42"), "metric value should appear"); - 2034
assert!(!text.contains("```vak"), "raw fence must be projected"); - 2035
} - 2036
- 2037
#[test] - 2038
fn structured_fence_passed_through_as_json_for_native_surface() { - 2039
let source = "Done.\n\n```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"tokens\",\"value\":128}}\n```\n"; - 2040
let input = DeliveryJob { - 2041
job_id: "struct-4".into(), - 2042
target: "test:one".into(), - 2043
kind: DeliveryKind::Assistant, - 2044
content: DeliveryContent::Answer(AnswerDraft::from_markdown(source)), - 2045
profile: DeliveryProfile { - 2046
surface: "desktop".into(), - 2047
markup: Markup::Json, - 2048
max_chars: None, - 2049
supports_tables: true, - 2050
supports_code_blocks: true, - 2051
supports_links: true, - 2052
supports_actions: true, - 2053
template: None, - 2054
posture: DeliveryPosture::default(), - 2055
}, - 2056
skill_registry: None, - 2057
}; - 2058
let packet = render(&input).expect("valid delivery job"); - 2059
// For Json markup, the structured fence should NOT be projected - 2060
// — it stays as a structured content block for the desktop renderer. - 2061
let text = fallback_text(&packet); - 2062
assert!( - 2063
text.contains("```vak"), - 2064
"Json surface must preserve raw fence for AST rendering" - 2065
); - 2066
} - 2067
- 2068
#[test] - 2069
fn tool_result_with_vak_fence_is_projected() { - 2070
let tool_output = "Command completed.\n\n```vak\n{\"semantic_type\":\"metric\",\"payload\":{\"label\":\"exit_code\",\"value\":0}}\n```\n"; - 2071
let content = DeliveryContent::ToolResult(ToolResultPayload { - 2072
tool: "bash".into(), - 2073
output: tool_output.into(), - 2074
is_error: false, - 2075
}); - 2076
let input = DeliveryJob { - 2077
job_id: "tool-1".into(), - 2078
target: "test:one".into(), - 2079
kind: DeliveryKind::ToolResult, - 2080
content, - 2081
profile: DeliveryProfile { - 2082
surface: "test".into(), - 2083
markup: Markup::TelegramHtml, - 2084
max_chars: Some(4000), - 2085
supports_tables: false, - 2086
supports_code_blocks: true, - 2087
supports_links: true, - 2088
supports_actions: false, - 2089
template: None, - 2090
posture: DeliveryPosture::default(), - 2091
}, - 2092
skill_registry: None, - 2093
}; - 2094
let packet = render(&input).expect("valid tool result job"); - 2095
let text = fallback_text(&packet); - 2096
assert!( - 2097
text.contains("exit_code"), - 2098
"tool result metric label should appear" - 2099
); - 2100
assert!(text.contains("0"), "tool result metric value should appear"); - 2101
assert!(text.contains("bash"), "tool name should appear in header"); - 2102
} - 2103
- 2104
#[test] - 2105
fn malformed_vak_fence_is_preserved_as_code_block() { - 2106
let source = "Done.\n\n```vak\n{this is not json}\n```\n"; - 2107
let input = DeliveryJob { - 2108
job_id: "struct-5".into(), - 2109
target: "test:one".into(), - 2110
kind: DeliveryKind::Assistant, - 2111
content: DeliveryContent::Answer(AnswerDraft::from_markdown(source)), - 2112
profile: DeliveryProfile { - 2113
surface: "test".into(), - 2114
markup: Markup::TelegramHtml, - 2115
max_chars: Some(4000), - 2116
supports_tables: false, - 2117
supports_code_blocks: true, - 2118
supports_links: true, - 2119
supports_actions: false, - 2120
template: None, - 2121
posture: DeliveryPosture::default(), - 2122
}, - 2123
skill_registry: None, - 2124
}; - 2125
let result = render(&input); - 2126
// Must not panic on malformed JSON inside the fence - 2127
assert!( - 2128
result.is_ok(), - 2129
"malformed fence should not crash the worker" - 2130
); - 2131
} - 2132
- 2133
#[test] - 2134
fn plugin_contributed_skill_is_recognized() { - 2135
use std::collections::BTreeMap; - 2136
let mut registry = built_in_skill_registry(); - 2137
let mut renderers = BTreeMap::new(); - 2138
renderers.insert( - 2139
"desktop".to_string(), - 2140
RendererBinding { - 2141
renderer: "metric".into(), - 2142
interactive: false, - 2143
requires: vec![], - 2144
}, - 2145
); - 2146
let plugin_manifest = PresentationSkillManifest { - 2147
id: "custom_metric".into(), - 2148
version: "1.0.0".into(), - 2149
api: PRESENTATION_SKILL_API.into(), - 2150
provides: vec!["custom.metric".into()], - 2151
renderers, - 2152
schema: None, - 2153
outcome_requirements: Vec::new(), - 2154
}; - 2155
let files = vec![( - 2156
"custom_skill.json".to_string(), - 2157
serde_json::to_vec(&plugin_manifest).expect("plugin manifest must serialize"), - 2158
)]; - 2159
registry.merge_plugin_files(&files); - 2160
assert!(registry.get("custom_metric").is_some()); - 2161
// The plugin skill should be used when parsing its semantic type: - 2162
// skill_id is resolved from the registry, not hardcoded to "core". - 2163
let fence = - 2164
r#"{"semantic_type":"custom.metric","payload":{"label":"throughput","value":42}}"#; - 2165
let output = parse_fragment_with(fence, ®istry) - 2166
.expect("plugin skill should parse with correct skill_id"); - 2167
assert_eq!(output.skill_id, "custom_metric"); - 2168
assert_eq!(output.skill_version, "1.0.0"); - 2169
assert_eq!(output.semantic_type, "custom.metric"); - 2170
} - 2171
} - 2172
- 2173
// ---- engagement posture (docs/design/47-commitment-kernel.md) -------------- - 2174
- 2175
/// When a delivery should reach its reader, and how hard it may push. - 2176
/// - 2177
/// The one behaviour worth having: an unattended overnight run must not send - 2178
/// forty notifications nobody reads. Nothing about the *content* changes — the - 2179
/// semantic contract, the renderer, and the outbox are untouched — this only - 2180
/// decides whether a packet goes out now, waits for the unit of work to - 2181
/// finish, or rolls into the next digest. - 2182
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 2183
#[serde(rename_all = "kebab-case")] - 2184
pub enum Cadence { - 2185
/// Stream as it happens. Somebody is watching. - 2186
Live, - 2187
/// One delivery when the unit of work finishes. - 2188
OnCompletion, - 2189
/// Roll up into the next digest. - 2190
Digest, - 2191
} - 2192
- 2193
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 2194
#[serde(rename_all = "kebab-case")] - 2195
pub enum Urgency { - 2196
/// Break through whatever the cadence says. Reserved for things a person - 2197
/// would want woken for. - 2198
Interrupt, - 2199
Notify, - 2200
Quiet, - 2201
} - 2202
- 2203
/// How a turn's engagement wants its output delivered. - 2204
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] - 2205
pub struct DeliveryPosture { - 2206
pub cadence: Cadence, - 2207
pub urgency: Urgency, - 2208
} - 2209
- 2210
impl Default for DeliveryPosture { - 2211
fn default() -> Self { - 2212
// The pre-kernel behaviour: everything goes out immediately. - 2213
DeliveryPosture { - 2214
cadence: Cadence::Live, - 2215
urgency: Urgency::Notify, - 2216
} - 2217
} - 2218
} - 2219
- 2220
impl DeliveryPosture { - 2221
/// Convert the intent kernel's serialized cadence/urgency labels at the - 2222
/// delivery boundary without introducing a dependency cycle. - 2223
pub fn from_intent_labels(cadence: &str, urgency: &str) -> Self { - 2224
let cadence = match cadence { - 2225
"on-completion" | "on_completion" => Cadence::OnCompletion, - 2226
"digest" => Cadence::Digest, - 2227
_ => Cadence::Live, - 2228
}; - 2229
let urgency = match urgency { - 2230
"interrupt" => Urgency::Interrupt, - 2231
"quiet" => Urgency::Quiet, - 2232
_ => Urgency::Notify, - 2233
}; - 2234
Self { cadence, urgency } - 2235
} - 2236
} - 2237
- 2238
/// What to do with one packet under a posture. - 2239
#[derive(Debug, Clone, Copy, PartialEq, Eq)] - 2240
pub enum Disposition { - 2241
/// Send now. - 2242
Send, - 2243
/// Hold until the unit of work completes. - 2244
HoldUntilComplete, - 2245
/// Hold for the next digest. - 2246
HoldForDigest, - 2247
} - 2248
- 2249
impl DeliveryPosture { - 2250
/// Decide one packet's fate. - 2251
/// - 2252
/// Two rules override the cadence, and both are about not losing something - 2253
/// that matters: - 2254
/// - 2255
/// * an `Interrupt` urgency always sends — an irreversible step's - 2256
/// confirmation must not sit in a digest until morning; - 2257
/// * anything that needs a person *now* (an approval gate) always sends, - 2258
/// because a held gate is a stopped run, and batching it would turn a - 2259
/// question into a hang. - 2260
pub fn disposition(&self, kind: DeliveryKind) -> Disposition { - 2261
if self.urgency == Urgency::Interrupt || needs_a_person_now(kind) { - 2262
return Disposition::Send; - 2263
} - 2264
match self.cadence { - 2265
Cadence::Live => Disposition::Send, - 2266
Cadence::OnCompletion => match kind { - 2267
// Progress and tool chatter are the noise; the answer itself - 2268
// is the thing that was waited for. - 2269
DeliveryKind::Progress | DeliveryKind::ToolResult | DeliveryKind::ToolCall => { - 2270
Disposition::HoldUntilComplete - 2271
} - 2272
_ => Disposition::Send, - 2273
}, - 2274
Cadence::Digest => match kind { - 2275
DeliveryKind::Alert => Disposition::Send, - 2276
_ => Disposition::HoldForDigest, - 2277
}, - 2278
} - 2279
} - 2280
} - 2281
- 2282
/// Deliveries that stop a run until somebody answers. Never batched. - 2283
fn needs_a_person_now(kind: DeliveryKind) -> bool { - 2284
matches!(kind, DeliveryKind::Approval) - 2285
} - 2286
- 2287
#[cfg(test)] - 2288
mod posture_tests { - 2289
use super::*; - 2290
- 2291
fn posture(cadence: Cadence, urgency: Urgency) -> DeliveryPosture { - 2292
DeliveryPosture { cadence, urgency } - 2293
} - 2294
- 2295
#[test] - 2296
fn intent_labels_map_to_delivery_posture() { - 2297
assert_eq!( - 2298
DeliveryPosture::from_intent_labels("on-completion", "quiet"), - 2299
posture(Cadence::OnCompletion, Urgency::Quiet) - 2300
); - 2301
assert_eq!( - 2302
DeliveryPosture::from_intent_labels("unknown", "unknown"), - 2303
DeliveryPosture::default() - 2304
); - 2305
} - 2306
- 2307
/// The behaviour this exists for: overnight work sends a roll-up, not a - 2308
/// stream of progress nobody is awake to read. - 2309
#[test] - 2310
fn an_unattended_run_holds_its_chatter_for_the_digest() { - 2311
let quiet = posture(Cadence::Digest, Urgency::Quiet); - 2312
assert_eq!( - 2313
quiet.disposition(DeliveryKind::Progress), - 2314
Disposition::HoldForDigest - 2315
); - 2316
assert_eq!( - 2317
quiet.disposition(DeliveryKind::Assistant), - 2318
Disposition::HoldForDigest - 2319
); - 2320
} - 2321
- 2322
/// A held approval gate is a stopped run. Batching it would turn a - 2323
/// question into a hang, so it always goes out. - 2324
#[test] - 2325
fn an_approval_always_goes_out_whatever_the_cadence() { - 2326
for cadence in [Cadence::Live, Cadence::OnCompletion, Cadence::Digest] { - 2327
for urgency in [Urgency::Interrupt, Urgency::Notify, Urgency::Quiet] { - 2328
assert_eq!( - 2329
posture(cadence, urgency).disposition(DeliveryKind::Approval), - 2330
Disposition::Send, - 2331
"{cadence:?}/{urgency:?} batched an approval" - 2332
); - 2333
} - 2334
} - 2335
} - 2336
- 2337
/// An irreversible step's confirmation must not wait until morning. - 2338
#[test] - 2339
fn interrupt_urgency_overrides_every_cadence() { - 2340
let urgent = posture(Cadence::Digest, Urgency::Interrupt); - 2341
assert_eq!( - 2342
urgent.disposition(DeliveryKind::Assistant), - 2343
Disposition::Send - 2344
); - 2345
assert_eq!( - 2346
urgent.disposition(DeliveryKind::Progress), - 2347
Disposition::Send - 2348
); - 2349
} - 2350
- 2351
/// Alerts are already the exception path; a digest must not swallow one. - 2352
#[test] - 2353
fn an_alert_is_never_held_for_a_digest() { - 2354
assert_eq!( - 2355
posture(Cadence::Digest, Urgency::Quiet).disposition(DeliveryKind::Alert), - 2356
Disposition::Send - 2357
); - 2358
} - 2359
- 2360
/// On-completion holds the chatter and sends the answer. - 2361
#[test] - 2362
fn on_completion_holds_progress_but_not_the_result() { - 2363
let posture = posture(Cadence::OnCompletion, Urgency::Notify); - 2364
assert_eq!( - 2365
posture.disposition(DeliveryKind::Progress), - 2366
Disposition::HoldUntilComplete - 2367
); - 2368
assert_eq!( - 2369
posture.disposition(DeliveryKind::Assistant), - 2370
Disposition::Send - 2371
); - 2372
} - 2373
- 2374
/// The default reproduces the behaviour before any of this existed. - 2375
#[test] - 2376
fn the_default_posture_sends_everything() { - 2377
let default = DeliveryPosture::default(); - 2378
for kind in [ - 2379
DeliveryKind::Assistant, - 2380
DeliveryKind::Progress, - 2381
DeliveryKind::Alert, - 2382
DeliveryKind::Approval, - 2383
DeliveryKind::ToolResult, - 2384
] { - 2385
assert_eq!(default.disposition(kind), Disposition::Send); - 2386
} - 2387
} - 2388
} - 2389
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.