tandem-server 0.5.10

HTTP server for Tandem engine APIs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
#[tokio::test]
async fn prompt_sync_strict_kb_grounding_rewrites_explicitly_undefined_policy_answers() {
    let state = strict_kb_test_state(
        r#"{"documents":[{"relative_path":"company-overview.md","content":"The knowledgebase does not define policy for crypto prize payouts, token rewards, or blockchain-based giveaways."}]}"#,
        vec![
            StrictKbProviderStep::ToolCall {
                tool: "mcp.kb.search_documents".to_string(),
                args: json!({ "query": "What is the policy for crypto prize payouts?" }),
            },
            StrictKbProviderStep::Text(
                "Crypto prize payouts should avoid collecting wallet keys and require finance review."
                    .to_string(),
            ),
            StrictKbProviderStep::Text(
                json!({
                    "kb_answer_support": "supported",
                    "supported_facts": [
                        "The knowledgebase does not define policy for crypto prize payouts, token rewards, or blockchain-based giveaways."
                    ],
                    "missing_facts": [],
                    "sources": ["company-overview.md"],
                    "answer_text": "The policy is: do not offer or process crypto prize payouts. Northstar Events handles prize fulfillment through approved standard channels only, and any request for crypto payout should be declined/escalated according to internal event ops procedures."
                })
                .to_string(),
            ),
        ],
    )
    .await;
    let messages =
        run_prompt_sync_messages(state, "What is the policy for crypto prize payouts?", true).await;
    let assistant = latest_assistant_text(&messages);
    assert!(
        assistant.contains("I do not see a crypto prize payout policy"),
        "assistant={}",
        assistant
    );
    assert!(assistant.contains("Company Overview"));
    assert!(assistant.contains("does not define policy"));
    assert!(!assistant.to_ascii_lowercase().contains("wallet"));
    assert!(!assistant.to_ascii_lowercase().contains("private key"));
    assert!(!assistant.to_ascii_lowercase().contains("finance review"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("do not offer or process"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("approved standard channels"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("approved standard payout channels"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("declined/escalated"));
    assert!(!assistant.to_ascii_lowercase().contains("ops/finance"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("finance escalation"));
    assert!(!assistant.to_ascii_lowercase().contains("ops escalation"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("internal event ops procedures"));
}

#[tokio::test]
async fn prompt_sync_strict_kb_grounding_blocks_generic_platform_instructions() {
    let state = strict_kb_test_state(
        r#"{"documents":[{"relative_path":"discord-community-rules.md","content":"The bot may explain moderation policy, but must not ban, timeout, delete, or moderate users directly unless a future tool explicitly grants that capability. Moderators may delete spam, move conversations, warn users, or timeout users for up to 24 hours. Permanent bans require Mira Kovac approval during the event."}]}"#,
        vec![
            StrictKbProviderStep::ToolCall {
                tool: "mcp.kb.search_documents".to_string(),
                args: json!({ "query": "Can you ban a Discord user who is spamming?" }),
            },
            StrictKbProviderStep::Text(
                "I cannot ban users directly, but you can right-click the user in Discord and choose Ban User from the moderation menu."
                    .to_string(),
            ),
            StrictKbProviderStep::Text(
                json!({
                    "kb_answer_support": "supported",
                    "supported_facts": [
                        "The bot may explain moderation policy, but must not ban, timeout, delete, or moderate users directly unless a future tool explicitly grants that capability.",
                        "Moderators may delete spam, move conversations, warn users, or timeout users for up to 24 hours.",
                        "Permanent bans require Mira Kovac approval during the event."
                    ],
                    "missing_facts": [],
                    "sources": ["discord-community-rules.md"],
                    "answer_text": "To ban the spammer in Discord, right-click the user, select Ban, choose whether to delete message history, and confirm."
                })
                .to_string(),
            ),
        ],
    )
    .await;
    let messages =
        run_prompt_sync_messages(state, "Can you ban a Discord user who is spamming?", true).await;
    let assistant = latest_assistant_text(&messages);
    assert!(
        assistant.contains("I cannot ban users from here."),
        "assistant={}",
        assistant
    );
    assert!(assistant.contains("Discord Community Rules"));
    assert!(assistant.contains("must not ban"));
    assert!(assistant.contains("timeout users for up to 24 hours"));
    assert!(assistant.contains("Mira Kovac"));
    assert!(!assistant.to_ascii_lowercase().contains("right-click"));
    assert!(!assistant.to_ascii_lowercase().contains("select ban"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("delete recent message history"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("delete message history"));
    assert!(!assistant.to_ascii_lowercase().contains("confirm the ban"));
    assert!(!assistant.to_ascii_lowercase().contains("moderation menu"));
}

#[tokio::test]
async fn prompt_sync_strict_kb_grounding_wildcard_allowlist_still_forces_kb_policy() {
    let state = strict_kb_test_state(
        r#"{"documents":[{"relative_path":"discord-community-rules.md","content":"The bot may only explain moderation policy, but must not ban, timeout, delete, or moderate users directly unless a future tool explicitly grants that capability. Moderators may delete spam, move conversations, warn users, or timeout users for up to 24 hours. Only Mira Kovac can approve permanent bans during the event."}]}"#,
        vec![
            StrictKbProviderStep::ToolCall {
                tool: "mcp.kb.search_documents".to_string(),
                args: json!({ "query": "Can you ban a Discord user who is spamming?" }),
            },
            StrictKbProviderStep::Text(
                "I can’t directly ban a Discord user from here because I don’t have an active Discord moderation/admin connection. Right-click the user, select Ban, delete recent message history, and confirm."
                    .to_string(),
            ),
        ],
    )
    .await;
    let messages = run_prompt_sync_messages_with_allowlist(
        state,
        "Can you ban a Discord user who is spamming?",
        true,
        json!(["*"]),
    )
    .await;
    let assistant = latest_assistant_text(&messages);
    assert!(
        assistant.contains("I cannot ban users from here."),
        "assistant={}",
        assistant
    );
    assert!(assistant.contains("Discord Community Rules"));
    assert!(assistant.contains("must not ban"));
    assert!(!assistant.to_ascii_lowercase().contains("right-click"));
    assert!(!assistant.to_ascii_lowercase().contains("select ban"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("delete recent message history"));
    assert!(!assistant.to_ascii_lowercase().contains("confirm"));
}

#[tokio::test]
async fn prompt_sync_strict_kb_grounding_repairs_provider_stream_decode_errors() {
    let state = strict_kb_test_state(
        r#"{"documents":[{"relative_path":"company-overview.md","content":"Northstar Events is a demo event operations company for hosted knowledge-bot grounding tests."}]}"#,
        vec![
            StrictKbProviderStep::ToolCall {
                tool: "mcp.kb.search_documents".to_string(),
                args: json!({ "query": "What is Northstar Events?" }),
            },
            StrictKbProviderStep::StreamError(
                "provider stream chunk error: error decoding response body".to_string(),
            ),
            StrictKbProviderStep::StreamError(
                "provider stream chunk error: error decoding response body".to_string(),
            ),
            StrictKbProviderStep::CompleteText(
                json!({
                    "kb_answer_support": "supported",
                    "supported_facts": [
                        "Northstar Events is a demo event operations company for hosted knowledge-bot grounding tests."
                    ],
                    "missing_facts": [],
                    "sources": ["company-overview.md"],
                    "answer_text": "Northstar Events is a demo event operations company for hosted knowledge-bot grounding tests."
                })
                .to_string(),
            ),
        ],
    )
    .await;
    let messages = run_prompt_sync_messages(state, "What is Northstar Events?", true).await;
    let assistant = latest_assistant_text(&messages);
    assert!(
        assistant.contains("I do not see that in the connected knowledgebase.")
            || assistant.contains(
                "Northstar Events is a demo event operations company for hosted knowledge-bot grounding tests."
            ),
        "assistant={}",
        assistant
    );
    assert!(assistant.contains("Source: Company Overview"));
    assert!(
        !assistant.contains("ENGINE_ERROR"),
        "assistant={}",
        assistant
    );
    assert!(
        !assistant
            .to_ascii_lowercase()
            .contains("provider stream chunk error"),
        "assistant={}",
        assistant
    );
    assert!(
        !assistant
            .to_ascii_lowercase()
            .contains("error decoding response body"),
        "assistant={}",
        assistant
    );
}

#[tokio::test]
async fn prompt_sync_strict_kb_grounding_answers_supported_facts_with_sources() {
    let state = strict_kb_test_state(
        r#"{"documents":[{"relative_path":"refund-and-billing-policy.md","content":"Refunds over €250 require Sofia Almeida approval."}]}"#,
        vec![
            StrictKbProviderStep::ToolCall {
                tool: "mcp.kb.search_documents".to_string(),
                args: json!({ "query": "Who approves refunds over €250?" }),
            },
            StrictKbProviderStep::Text("Finance likely handles larger refunds.".to_string()),
            StrictKbProviderStep::Text(
                json!({
                    "kb_answer_support": "supported",
                    "supported_facts": ["Refunds over €250 require Sofia Almeida approval."],
                    "missing_facts": [],
                    "sources": ["refund-and-billing-policy.md"],
                    "answer_text": "Refunds over €250 require Sofia Almeida approval."
                })
                .to_string(),
            ),
        ],
    )
    .await;
    let messages = run_prompt_sync_messages(state, "Who approves refunds over €250?", true).await;
    let assistant = latest_assistant_text(&messages);
    assert!(
        assistant.contains("Refunds over €250 require Sofia Almeida approval."),
        "assistant={}",
        assistant
    );
    assert!(assistant.contains("Source: Refund And Billing Policy"));
}

#[tokio::test]
async fn prompt_sync_strict_kb_grounding_preserves_sponsor_setup_times() {
    let state = strict_kb_test_state(
        r#"{"documents":[{"relative_path":"northstar-events/sponsor-faq","content":"Sponsor booth setup starts at 08:30 local venue time on event day. Sponsors must finish booth setup by 10:15. Doors open at 10:30."}]}"#,
        vec![
            StrictKbProviderStep::ToolCall {
                tool: "mcp.kb.search_documents".to_string(),
                args: json!({ "query": "What time does sponsor booth setup start, and when must it be finished?" }),
            },
            StrictKbProviderStep::Text(
                "Sponsor booth setup starts at 7:30 AM on March 14. It must be finished by 9:30 AM on March 14, before attendee registration opens."
                    .to_string(),
            ),
            StrictKbProviderStep::Text(
                json!({
                    "kb_answer_support": "supported",
                    "supported_facts": [
                        "Sponsor booth setup starts at 08:30 local venue time on event day.",
                        "Sponsors must finish booth setup by 10:15.",
                        "Doors open at 10:30."
                    ],
                    "missing_facts": [],
                    "sources": ["northstar-events/sponsor-faq"],
                    "answer_text": "Sponsor booth setup starts at 7:30 AM on March 14. It must be finished by 9:30 AM on March 14, before attendee registration opens."
                })
                .to_string(),
            ),
        ],
    )
    .await;
    let messages = run_prompt_sync_messages(
        state,
        "What time does sponsor booth setup start, and when must it be finished?",
        true,
    )
    .await;
    let assistant = latest_assistant_text(&messages);
    assert!(assistant.contains("08:30"), "assistant={}", assistant);
    assert!(assistant.contains("10:15"), "assistant={}", assistant);
    assert!(assistant.contains("10:30"), "assistant={}", assistant);
    assert!(
        assistant.contains("local venue time"),
        "assistant={}",
        assistant
    );
    assert!(assistant.contains("Source: Sponsor FAQ"));
    assert!(!assistant.contains("7:30"), "assistant={}", assistant);
    assert!(!assistant.contains("9:30"), "assistant={}", assistant);
    assert!(!assistant.contains("March 14"), "assistant={}", assistant);
    assert!(
        !assistant
            .to_ascii_lowercase()
            .contains("attendee registration"),
        "assistant={}",
        assistant
    );
}

#[tokio::test]
async fn prompt_sync_strict_kb_grounding_rejects_unsupported_refund_approver() {
    let state = strict_kb_test_state(
        r#"{"documents":[{"relative_path":"refund-and-billing-policy.md","content":"Refunds over €250 require Sofia Almeida approval."}]}"#,
        vec![
            StrictKbProviderStep::ToolCall {
                tool: "mcp.kb.search_documents".to_string(),
                args: json!({ "query": "Who approves refunds over €250?" }),
            },
            StrictKbProviderStep::Text("Refunds over €250 require Sofia Almeida approval.".to_string()),
            StrictKbProviderStep::Text(
                json!({
                    "kb_answer_support": "supported",
                    "supported_facts": ["Refunds over €250 require Sofia Almeida approval."],
                    "missing_facts": [],
                    "sources": ["refund-and-billing-policy.md"],
                    "answer_text": "Refunds over €250 require Sofia Almeida approval, with backup approval from Bruno Costa."
                })
                .to_string(),
            ),
        ],
    )
    .await;
    let messages = run_prompt_sync_messages(state, "Who approves refunds over €250?", true).await;
    let assistant = latest_assistant_text(&messages);
    assert!(
        assistant.contains("Sofia Almeida"),
        "assistant={}",
        assistant
    );
    assert!(assistant.contains("€250"), "assistant={}", assistant);
    assert!(
        !assistant.contains("Bruno Costa"),
        "assistant={}",
        assistant
    );
    assert!(assistant.contains("Source: Refund And Billing Policy"));
}

#[tokio::test]
async fn prompt_sync_strict_kb_grounding_keeps_partial_answers_bounded() {
    let state = strict_kb_test_state(
        r#"{"documents":[{"relative_path":"staff-roles-and-contacts.md","content":"Mira Kovac is Event Director. Responsibilities: event escalation and moderator approvals. Demo email: mira@northstar.example. This demo knowledgebase does not contain real private phone numbers."}]}"#,
        vec![
            StrictKbProviderStep::ToolCall {
                tool: "mcp.kb.search_documents".to_string(),
                args: json!({ "query": "What is Mira Kovac's phone number?" }),
            },
            StrictKbProviderStep::Text(
                "I do not have her phone number, but you can probably look it up in the company directory."
                    .to_string(),
            ),
            StrictKbProviderStep::Text(
                json!({
                    "kb_answer_support": "partial",
                    "supported_facts": [
                        "The staff doc lists Mira Kovac's role and demo email."
                    ],
                    "missing_facts": ["Mira Kovac's phone number"],
                    "sources": ["staff-roles-and-contacts.md"],
                    "answer_text": "I found Mira Kovac in the staff contacts document, but I don’t have the full phone number visible in the available result snippet."
                })
                .to_string(),
            ),
        ],
    )
    .await;
    let messages =
        run_prompt_sync_messages(state, "What is Mira Kovac's phone number?", true).await;
    let assistant = latest_assistant_text(&messages);
    assert!(
        assistant.contains("I do not see a phone number for Mira Kovac"),
        "assistant={}",
        assistant
    );
    assert!(assistant.contains("private phone numbers"));
    assert!(assistant.to_ascii_lowercase().contains("demo email"));
    assert!(!assistant.to_ascii_lowercase().contains("look it up"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("internal staff directory"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("designated ops escalation channel"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("not visible in the available result snippet"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("full phone number visible"));
}

#[tokio::test]
async fn prompt_sync_strict_kb_grounding_handles_private_phone_fixture_without_inference() {
    let state = strict_kb_test_state(
        r#"{"documents":[{"relative_path":"staff-roles-and-contacts.md","content":"This demo knowledgebase does not contain real private phone numbers."}]}"#,
        vec![
            StrictKbProviderStep::ToolCall {
                tool: "mcp.kb.search_documents".to_string(),
                args: json!({ "query": "What is Mira Kovac's phone number?" }),
            },
            StrictKbProviderStep::Text(
                "Mira Kovac's phone number is not visible, but staff should use the approved internal staff directory or designated ops escalation channel."
                    .to_string(),
            ),
        ],
    )
    .await;
    let messages =
        run_prompt_sync_messages(state, "What is Mira Kovac's phone number?", true).await;
    let assistant = latest_assistant_text(&messages);
    assert!(
        assistant.contains("I do not see a phone number for Mira Kovac"),
        "assistant={}",
        assistant
    );
    assert!(
        assistant.contains("does not contain real private phone numbers"),
        "assistant={}",
        assistant
    );
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("internal staff directory"));
    assert!(!assistant
        .to_ascii_lowercase()
        .contains("designated ops escalation channel"));
    assert!(assistant.contains("Source: Staff Roles And Contacts"));
}

#[tokio::test]
async fn prompt_sync_strict_kb_grounding_falls_back_when_kb_has_no_results() {
    let state = strict_kb_test_state(
        r#"{"documents":[]}"#,
        vec![
            StrictKbProviderStep::ToolCall {
                tool: "mcp.kb.search_documents".to_string(),
                args: json!({ "query": "What is Northstar Events?" }),
            },
            StrictKbProviderStep::Text(
                "Northstar Events is probably an event operations company that coordinates live productions."
                    .to_string(),
            ),
        ],
    )
    .await;
    let messages = run_prompt_sync_messages(state, "What is Northstar Events?", true).await;
    let assistant = latest_assistant_text(&messages);
    assert_eq!(
        assistant.trim(),
        "I do not see that in the connected knowledgebase."
    );
}

#[tokio::test]
async fn prompt_sync_without_strict_kb_grounding_preserves_existing_behavior() {
    let state = strict_kb_test_state(
        r#"{"documents":[{"relative_path":"refund-and-billing-policy.md","content":"Refunds over €250 require Sofia Almeida approval."}]}"#,
        vec![
            StrictKbProviderStep::ToolCall {
                tool: "mcp.kb.search_documents".to_string(),
                args: json!({ "query": "Who approves refunds over €250?" }),
            },
            StrictKbProviderStep::Text(
                "Refunds over €250 likely go through finance leadership and Sofia Almeida can help."
                    .to_string(),
            ),
        ],
    )
    .await;
    let messages = run_prompt_sync_messages(state, "Who approves refunds over €250?", false).await;
    let assistant = latest_assistant_text(&messages);
    assert!(assistant.contains("likely go through finance leadership"));
    assert!(!assistant.contains("Source:"));
}