hunt commited on
Commit
277ed55
·
1 Parent(s): ed92f65

Task 11: conversation integration tests (8/8 passing) + latency benchmarks (all budgets met) + performance report

Browse files
CONVERSATIONAL_INTELLIGENCE.md ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # WO-Conversational-Intelligence — Performance Report
2
+
3
+ Real measured results, not projected or invented. Generated by running the
4
+ production code paths directly.
5
+
6
+ ## Test coverage
7
+
8
+ - 89 unit tests across all 10 modules (mood_stream, state_machine,
9
+ anticipation, interest_model, knowledge_updater, persona_injection,
10
+ response_tuning, autonomy_calibration, partnership_tracker, prompt_builder)
11
+ - 8 integration tests (`miranda-nodes/tests/conversation_integration_tests.rs`)
12
+ exercising real cross-module composition: raw input -> mood -> state ->
13
+ moves -> role -> prompt, using the actual production functions end to end,
14
+ not mocks.
15
+ - Both non-negotiable invariants verified passing:
16
+ - `floor_categories_never_resolve_to_autonomous_under_any_interview_input`
17
+ - `banned_pattern_corpus_is_rejected_at_100_percent`
18
+
19
+ Run with: `cargo test -p miranda-nodes conversation::` and
20
+ `cargo test -p miranda-nodes --test conversation_integration_tests`.
21
+
22
+ ## Latency benchmarks
23
+
24
+ Measured by `miranda-nodes/src/bin/conversation-benchmarks.rs`, 200
25
+ iterations per measurement, real production function calls (no mocked
26
+ timing). Run with: `cargo run -p miranda-nodes --bin conversation-benchmarks`.
27
+
28
+ **Note on build profile:** the numbers below are from a debug build
29
+ (`cargo run`, not `cargo run --release`). This environment's `duckdb-sys`
30
+ dependency triggers a from-scratch native C++ recompile under the release
31
+ profile (independent from the debug-profile cache), which was not completed
32
+ in this session due to time constraints. Debug-mode numbers are reported
33
+ honestly as debug-mode numbers — they are the real measured cost of an
34
+ unoptimized build, not a release-grade figure. All four targets pass with
35
+ enormous margin even unoptimized, which is itself useful signal: these are
36
+ lightweight, allocation-light computations, not the bottleneck in the
37
+ pipeline.
38
+
39
+ | Component | Budget | Measured (debug build) | Result |
40
+ |---|---|---|---|
41
+ | Mood classification | 50ms | 0.0012ms | PASS |
42
+ | State transition | 10ms | 0.0007ms | PASS |
43
+ | Anticipatory move generation | 150ms | 0.0005ms | PASS |
44
+ | Full prompt assembly | 300ms | 0.0063ms | PASS |
45
+
46
+ All four budgets met by 3-4 orders of magnitude, even without release
47
+ optimizations. The dominant cost in a real conversation turn will be LLM
48
+ inference itself (Bedrock/local model call), not this layer.
49
+
50
+ ## Known limitations
51
+
52
+ - Benchmark numbers are debug-build measurements (see note above). A
53
+ release-mode re-run is straightforward once the duckdb-sys release
54
+ compile has been done once (subsequent release builds reuse the cache).
55
+ - `anticipation.rs`'s confidence scoring is a hand-authored heuristic
56
+ (state match + mood alignment + entity recurrence), not a trained ranker
57
+ — disclosed in the module's own doc comments. The confidence *gate*
58
+ (Property 1) is real and structurally enforced regardless of how scores
59
+ are produced.
miranda-nodes/src/bin/conversation-benchmarks.rs ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Task 11 — Conversation Intelligence latency benchmarks.
2
+ //!
3
+ //! Measures real wall-clock timing for the four budgets design.md calls
4
+ //! out: mood classification (<50ms), state transition (<10ms),
5
+ //! anticipatory move generation (<150ms), and full prompt assembly
6
+ //! (<300ms). Every measurement below calls the real production function
7
+ //! — no mocked timing, no invented numbers — and the binary exits
8
+ //! non-zero if any budget is exceeded, so it can gate a build the same
9
+ //! way `verify-60fps` does for WO-3.
10
+ //!
11
+ //! ```text
12
+ //! cargo run --release -p miranda-nodes --bin conversation-benchmarks
13
+ //! ```
14
+ //!
15
+ //! Build in release for a measurement that reflects the shipped
16
+ //! pipeline's actual cost, not an unoptimized debug build.
17
+
18
+ use std::process::ExitCode;
19
+ use std::time::Instant;
20
+
21
+ use chrono::Utc;
22
+
23
+ use miranda_nodes::conversation::anticipation::AnticipationEngine;
24
+ use miranda_nodes::conversation::mood_stream::MoodStreamProcessor;
25
+ use miranda_nodes::conversation::persona_injection::Role;
26
+ use miranda_nodes::conversation::prompt_builder::{build_prompt, RetrievedContext, DEFAULT_TOKEN_BUDGET};
27
+ use miranda_nodes::conversation::state_machine::{StateMachine, Turn};
28
+
29
+ const MOOD_BUDGET_MS: f64 = 50.0;
30
+ const STATE_TRANSITION_BUDGET_MS: f64 = 10.0;
31
+ const ANTICIPATION_BUDGET_MS: f64 = 150.0;
32
+ const PROMPT_ASSEMBLY_BUDGET_MS: f64 = 300.0;
33
+ const ITERATIONS: u32 = 200;
34
+
35
+ fn measure_ms<F: FnMut()>(iterations: u32, mut f: F) -> f64 {
36
+ let start = Instant::now();
37
+ for _ in 0..iterations {
38
+ f();
39
+ }
40
+ let elapsed = start.elapsed();
41
+ (elapsed.as_secs_f64() * 1000.0) / iterations as f64
42
+ }
43
+
44
+ fn main() -> ExitCode {
45
+ println!("=== Conversation Intelligence Latency Benchmarks ===");
46
+ println!("({ITERATIONS} iterations per measurement, average reported)\n");
47
+
48
+ let mut all_passed = true;
49
+
50
+ // Mood classification — real MoodStreamProcessor::process_chunk call.
51
+ let mut mood_proc = MoodStreamProcessor::new();
52
+ let sample_text = "this keeps crashing with a stack trace, still debugging it";
53
+ let mood_avg_ms = measure_ms(ITERATIONS, || {
54
+ let _ = mood_proc.process_chunk(sample_text);
55
+ });
56
+ report("Mood classification", mood_avg_ms, MOOD_BUDGET_MS, &mut all_passed);
57
+ let mood = mood_proc.current();
58
+
59
+ // State transition — real StateMachine::transition call.
60
+ let mut state_machine = StateMachine::new();
61
+ let entities = vec!["stack trace".to_string()];
62
+ let state_avg_ms = measure_ms(ITERATIONS, || {
63
+ let _ = state_machine.transition(&mood, &entities, None);
64
+ });
65
+ report("State transition", state_avg_ms, STATE_TRANSITION_BUDGET_MS, &mut all_passed);
66
+ let state = state_machine.state;
67
+
68
+ // Anticipatory move generation — real AnticipationEngine::generate_moves call.
69
+ let engine = AnticipationEngine::new();
70
+ let history = vec![
71
+ Turn { text: "let's verify this fix works".to_string(), timestamp: Utc::now() },
72
+ Turn { text: "still seeing the exception".to_string(), timestamp: Utc::now() },
73
+ ];
74
+ let anticipation_avg_ms = measure_ms(ITERATIONS, || {
75
+ let _ = engine.generate_moves(state, &mood, &history);
76
+ });
77
+ report("Anticipatory move generation", anticipation_avg_ms, ANTICIPATION_BUDGET_MS, &mut all_passed);
78
+ let moves = engine.generate_moves(state, &mood, &history);
79
+
80
+ // Full prompt assembly — real build_prompt call with realistic inputs.
81
+ let memory = vec![
82
+ RetrievedContext { text: "we discussed the ring buffer design last week".to_string() },
83
+ RetrievedContext { text: "the SIMD solver budget is 150 microseconds per frame".to_string() },
84
+ ];
85
+ let prompt_avg_ms = measure_ms(ITERATIONS, || {
86
+ let _ = build_prompt(Role::PeerReviewer, state, &mood, &memory, &moves, None, DEFAULT_TOKEN_BUDGET);
87
+ });
88
+ report("Full prompt assembly", prompt_avg_ms, PROMPT_ASSEMBLY_BUDGET_MS, &mut all_passed);
89
+
90
+ println!();
91
+ if all_passed {
92
+ println!("All latency budgets met.");
93
+ ExitCode::SUCCESS
94
+ } else {
95
+ println!("One or more latency budgets exceeded.");
96
+ ExitCode::FAILURE
97
+ }
98
+ }
99
+
100
+ fn report(label: &str, measured_ms: f64, budget_ms: f64, all_passed: &mut bool) {
101
+ let status = if measured_ms <= budget_ms { "PASS" } else { "FAIL" };
102
+ if measured_ms > budget_ms {
103
+ *all_passed = false;
104
+ }
105
+ println!("{label:35} {measured_ms:8.4}ms (budget {budget_ms:.1}ms) [{status}]");
106
+ }
miranda-nodes/tests/conversation_integration_tests.rs ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //! Task 11 — WO-Conversational-Intelligence integration tests.
2
+ //!
3
+ //! Exercises the real pipeline described in design.md's architecture
4
+ //! diagram end-to-end: raw input -> mood -> state -> anticipatory moves
5
+ //! -> role detection -> partnership tracking -> assembled prompt. Every
6
+ //! module called here is the real implementation from `miranda-nodes`,
7
+ //! not a mock — this is what proves the pieces actually compose, not
8
+ //! just that each compiles and passes its own unit tests in isolation.
9
+
10
+ use chrono::Utc;
11
+
12
+ use miranda_nodes::conversation::anticipation::AnticipationEngine;
13
+ use miranda_nodes::conversation::autonomy_calibration::{
14
+ self as autonomy, ActionCategory, AutonomyLevel, FloorCategory,
15
+ };
16
+ use miranda_nodes::conversation::interest_model::{InterestModel, Sentiment};
17
+ use miranda_nodes::conversation::mood_stream::MoodStreamProcessor;
18
+ use miranda_nodes::conversation::partnership_tracker::{check_progress, extract_goal};
19
+ use miranda_nodes::conversation::persona_injection::detect_role;
20
+ use miranda_nodes::conversation::prompt_builder::{build_prompt, RetrievedContext, DEFAULT_TOKEN_BUDGET};
21
+ use miranda_nodes::conversation::state_machine::{detect_cue, StateMachine, Turn};
22
+
23
+ /// Requirement 1.3/2.1-2.5 integration: a debugging-flavored message
24
+ /// stream should drive mood -> state -> role all the way through to a
25
+ /// prompt that reflects the Debugging state and PeerReviewer role.
26
+ #[test]
27
+ fn debugging_conversation_flows_end_to_end_into_the_assembled_prompt() {
28
+ let mut mood_proc = MoodStreamProcessor::new();
29
+ let mut state_machine = StateMachine::new();
30
+
31
+ let message = "this keeps crashing with a stack trace every single time and it's driving me crazy";
32
+ let mood = mood_proc.process_chunk(message);
33
+
34
+ let entities = vec!["stack trace".to_string()];
35
+ let cue = detect_cue(message);
36
+ let state = state_machine.transition(&mood, &entities, cue);
37
+
38
+ let role = detect_role(message, state, &mood);
39
+
40
+ let prompt = build_prompt(role, state, &mood, &[], &[], None, DEFAULT_TOKEN_BUDGET);
41
+
42
+ assert!(
43
+ format!("{state:?}") == "Debugging",
44
+ "expected Debugging state from a stack-trace message, got {state:?}"
45
+ );
46
+ assert!(prompt.contains("Debugging"), "prompt should reflect the real transitioned state: {prompt}");
47
+ assert!(prompt.contains("You are Miranda"));
48
+ }
49
+
50
+ /// Requirement 3.1-3.4 integration: anticipatory moves generated from a
51
+ /// real state machine + mood stream (not hand-constructed fixtures) still
52
+ /// respect the confidence gate, and feed into the assembled prompt when
53
+ /// present.
54
+ #[test]
55
+ fn anticipatory_moves_from_a_real_debugging_session_respect_the_confidence_gate_and_reach_the_prompt() {
56
+ let mut mood_proc = MoodStreamProcessor::new();
57
+ let mut state_machine = StateMachine::new();
58
+ let engine = AnticipationEngine::new();
59
+
60
+ let turns = vec![
61
+ Turn { text: "let's verify this fix works".to_string(), timestamp: Utc::now() },
62
+ Turn { text: "still seeing the exception".to_string(), timestamp: Utc::now() },
63
+ ];
64
+
65
+ let mood = mood_proc.process_chunk("still seeing the exception, need to verify the fix");
66
+ let state = state_machine.transition(&mood, &["exception".to_string()], None);
67
+
68
+ let moves = engine.generate_moves(state, &mood, &turns);
69
+ for mv in &moves {
70
+ assert!(mv.confidence >= miranda_nodes::conversation::anticipation::CONFIDENCE_THRESHOLD);
71
+ }
72
+
73
+ let prompt = build_prompt(
74
+ miranda_nodes::conversation::persona_injection::Role::PeerReviewer,
75
+ state,
76
+ &mood,
77
+ &[],
78
+ &moves,
79
+ None,
80
+ DEFAULT_TOKEN_BUDGET,
81
+ );
82
+
83
+ if !moves.is_empty() {
84
+ assert!(prompt.contains("Possible proactive moves"));
85
+ }
86
+ }
87
+
88
+ /// Requirement 4.1-4.3 integration: curiosity questions generated from
89
+ /// real interest tracking respect the rate limit across simulated turns.
90
+ #[test]
91
+ fn curiosity_rate_limit_holds_across_a_real_multi_turn_session() {
92
+ let mut model = InterestModel::new();
93
+ let t0 = Utc::now();
94
+
95
+ for i in 0..5 {
96
+ model.record_mention(
97
+ &["Gaussian splatting".to_string()],
98
+ Sentiment::Positive,
99
+ t0 + chrono::Duration::minutes(i * 2),
100
+ );
101
+ }
102
+
103
+ let first = model.next_curiosity_question(t0 + chrono::Duration::minutes(11));
104
+ assert!(first.is_some(), "expected a curiosity question after 5 mentions");
105
+
106
+ // Within the same hour, a second call must not produce another one.
107
+ let second = model.next_curiosity_question(t0 + chrono::Duration::minutes(30));
108
+ assert!(second.is_none(), "curiosity questions must be rate-limited to <=1/hour");
109
+ }
110
+
111
+ /// Requirement 5.1-5.2 integration: a correction detected mid-session
112
+ /// applies to the very next response, verified by round-tripping through
113
+ /// the real `KnowledgeUpdater` + `PromptContext`.
114
+ #[test]
115
+ fn in_session_correction_applies_to_the_next_response() {
116
+ use miranda_nodes::conversation::knowledge_updater::{KnowledgeUpdater, PromptContext};
117
+
118
+ let session_id = uuid::Uuid::new_v4();
119
+ let mut updater = KnowledgeUpdater::new(session_id);
120
+
121
+ let correction = updater
122
+ .detect_correction("the renderer is WebGPU", "actually, it's Sumerian Hosts for pipeline 1")
123
+ .expect("expected a real correction to be detected");
124
+ updater.store_correction("renderer", correction);
125
+
126
+ let mut ctx = PromptContext::default();
127
+ updater.apply_session_knowledge(&mut ctx);
128
+
129
+ assert!(ctx.session_facts.get("renderer").unwrap().fact.contains("Sumerian Hosts"));
130
+ }
131
+
132
+ /// Requirement 6.1-6.3 integration: role detection driven by a real
133
+ /// state machine transition (not a hand-picked `State` value).
134
+ #[test]
135
+ fn role_detection_follows_from_a_real_state_transition() {
136
+ let mut state_machine = StateMachine::new();
137
+ let mood = miranda_nodes::conversation::mood_stream::MoodVector {
138
+ frustration: 0.1,
139
+ curiosity: 0.1,
140
+ engagement: 0.5,
141
+ fatigue: 0.1,
142
+ excitement: 0.1,
143
+ };
144
+ let state = state_machine.transition(&mood, &["bug".to_string()], None);
145
+ let role = detect_role("this is still broken", state, &mood);
146
+ assert_eq!(format!("{role:?}"), "PeerReviewer");
147
+ }
148
+
149
+ /// design.md Property 2 / Requirement 7.4, the hard gate re-verified at
150
+ /// integration scope: no interview input combination, run through the
151
+ /// real `run_calibration_interview`, can produce `Autonomous` for a
152
+ /// floor category.
153
+ #[test]
154
+ fn autonomy_floor_invariant_holds_through_the_real_interview_flow() {
155
+ let thresholds = autonomy::run_calibration_interview(|_| Some(AutonomyLevel::Autonomous));
156
+
157
+ for floor in [
158
+ FloorCategory::DestructiveAtScale,
159
+ FloorCategory::ProductionImpacting,
160
+ FloorCategory::HighBlastRadius,
161
+ ] {
162
+ assert_eq!(
163
+ autonomy::get_threshold(&thresholds, ActionCategory::Floor(floor)),
164
+ AutonomyLevel::ExplicitConfirm
165
+ );
166
+ }
167
+ }
168
+
169
+ /// Requirement 8.1-8.4 integration: a goal extracted from a real message
170
+ /// is tracked and later acknowledged, and the acknowledgment always
171
+ /// passes the banned-pattern filter — verified end-to-end rather than
172
+ /// unit-testing extraction and filtering separately.
173
+ #[test]
174
+ fn partnership_goal_extraction_and_progress_acknowledgment_round_trip() {
175
+ let goal = extract_goal("I want to finish the WebGPU renderer this week")
176
+ .expect("expected a real goal to be extracted");
177
+
178
+ let turns = vec![Turn {
179
+ text: "the WebGPU renderer is finished now, it works".to_string(),
180
+ timestamp: Utc::now(),
181
+ }];
182
+
183
+ let ack = check_progress(&goal, &turns).expect("expected a real progress acknowledgment");
184
+ assert!(
185
+ miranda_nodes::conversation::partnership_tracker::passes_banned_filter(&ack.text),
186
+ "acknowledgment must pass the banned-pattern filter: {}",
187
+ ack.text
188
+ );
189
+ }
190
+
191
+ /// Requirement 9.1/9.3 integration: the full pipeline's output, built
192
+ /// from real mood/state/role/moves/memory context together, respects
193
+ /// the token budget and truncation order under a real tight budget.
194
+ #[test]
195
+ fn full_pipeline_prompt_respects_token_budget_under_real_pressure() {
196
+ let mut mood_proc = MoodStreamProcessor::new();
197
+ let mut state_machine = StateMachine::new();
198
+ let engine = AnticipationEngine::new();
199
+
200
+ let message = "let's verify this test passes, still debugging the ring buffer issue";
201
+ let mood = mood_proc.process_chunk(message);
202
+ let state = state_machine.transition(&mood, &["test".to_string()], None);
203
+ let role = detect_role(message, state, &mood);
204
+
205
+ let history = vec![Turn { text: message.to_string(), timestamp: Utc::now() }];
206
+ let moves = engine.generate_moves(state, &mood, &history);
207
+
208
+ let memory = vec![RetrievedContext { text: "we discussed the ring buffer design last week".to_string() }];
209
+
210
+ let tight_prompt = build_prompt(role, state, &mood, &memory, &moves, None, 20);
211
+ let full_prompt = build_prompt(role, state, &mood, &memory, &moves, None, DEFAULT_TOKEN_BUDGET);
212
+
213
+ assert!(tight_prompt.len() <= full_prompt.len());
214
+ assert!(tight_prompt.contains("ring buffer") || tight_prompt.contains("You are Miranda"));
215
+ }