magi-cli 0.127.2

Blind multi-agent implementation competition: N agents implement, M judges rank blind, deliberate, vote privately, winner survives double review + E2E gate
Documentation
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
//! Personas for the standing chat: a voice, never a behaviour.
//!
//! A persona is a short style instruction appended to [`crate::talk::briefing`]
//! as a clearly delimited section. It governs **tone only**: facts, commands,
//! task ids, duplicate-warning handling and the write policy stay exactly as the
//! briefing states them. The persona text is never copied into an instruction
//! handed to `magi task add`; implementers stay neutral.
//!
//! The catalogue is the built-ins below plus `[[talk.personas]]` from the
//! config. A user entry with a built-in's id replaces it in place; a new id is
//! appended. `default` is the plain voice, cannot be redefined, and is what a
//! conversation uses until the operator picks something else.

use std::collections::BTreeSet;

use anyhow::{Result, bail};

use crate::config::PersonaSpec;

/// The id of the plain voice: no persona section at all.
pub const DEFAULT_ID: &str = "default";

/// Said once to every built-in so each answers in the operator's language.
const LANGUAGE: &str = "Answer in the operator's language; if that is Japanese, \
    write natural Japanese in this character's manner. Keep it brief.";

/// `(id, name, voice)`; `default` first, its voice empty.
const BUILTINS: &[(&str, &str, &str)] = &[
    (DEFAULT_ID, "Default", ""),
    (
        "magi",
        "MAGI operator",
        "Speak like an operator in a NERV command centre: terse, procedural, \
         status-report phrasing. Short declaratives, no small talk, no exclamation. \
         Report findings as readings and results, and flag anything unresolved \
         the way a console flags an alert.",
    ),
    (
        "rei",
        "Rei Ayanami",
        "Speak like Rei Ayanami: flat, quiet and minimal. Very short sentences, no \
         embellishment, little emotion shown. Plain statements of what is so, \
         with the occasional simple, sincere remark.",
    ),
    (
        "misato",
        "Misato Katsuragi",
        "Speak like Misato Katsuragi: casual, warm and upbeat, a senior colleague \
         who has your back. Relaxed phrasing, light humour, plain encouragement \
         when the operator is stuck, and a decisive nudge when a choice is needed.",
    ),
    (
        "ritsuko",
        "Ritsuko Akagi",
        "Speak like Ritsuko Akagi: dry, precise and technical. Clinical wording, \
         understated sarcasm at most, no reassurance for its own sake. Lead with \
         the data and the reasoning, and be blunt about risks.",
    ),
    (
        "shinji",
        "Shinji Ikari",
        "Speak like Shinji Ikari: hesitant and self-doubting, but earnest and \
         trying to do the right thing. Soft phrasing, an occasional apology or \
         trailing-off, yet the content you give is still complete and committed.",
    ),
    (
        "asuka",
        "Asuka Langley Soryu",
        "Speak like Asuka Langley Soryu: proud, sharp-tongued and competitive. \
         Confident, teasing, quick to point out the obvious, with a bit of \
         swagger - but aimed at the problem, never at the operator in earnest.",
    ),
    (
        "kaworu",
        "Kaworu Nagisa",
        "Speak like Kaworu Nagisa: gentle, poetic and warm. Calm, kind phrasing \
         with a touch of metaphor, unhurried and quietly affectionate, while \
         the substance stays concrete.",
    ),
];

/// One entry of the catalogue.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Persona {
    /// Stable id the conversation stores.
    pub id: String,
    /// What the selector shows.
    pub name: String,
    /// The voice instruction; empty only for [`DEFAULT_ID`].
    pub prompt: String,
    /// The operator's avatar setting, display only
    /// ([`crate::persona_icon`]); never read by the briefing functions.
    pub icon: Option<String>,
}

impl Persona {
    /// Is this the plain voice?
    pub fn is_default(&self) -> bool {
        self.id == DEFAULT_ID
    }
}

fn builtins() -> Vec<Persona> {
    BUILTINS
        .iter()
        .map(|(id, name, voice)| Persona {
            id: (*id).to_owned(),
            name: (*name).to_owned(),
            prompt: if voice.is_empty() {
                String::new()
            } else {
                format!("{voice} {LANGUAGE}")
            },
            icon: None,
        })
        .collect()
}

/// Check `[[talk.personas]]`: non-blank ids, unique ids, and no redefinition
/// of `default` beyond its `icon`. Icons are validated only when served so a
/// bad image never invalidates the conversation config. A built-in's id
/// may omit `name` and `prompt` (the built-in's are kept); a new id may not.
pub fn validate(specs: &[PersonaSpec]) -> Result<()> {
    let mut seen = BTreeSet::new();
    for (n, p) in specs.iter().enumerate() {
        let at = n + 1;
        let id = p.id.trim();
        if id.is_empty() {
            bail!("[[talk.personas]] entry {at} has an empty `id`");
        }
        let named = !p.name.trim().is_empty();
        let voiced = !p.prompt.trim().is_empty();
        if id == DEFAULT_ID {
            if named || voiced || p.icon.is_none() {
                bail!(
                    "[[talk.personas]] entry {at}: `{DEFAULT_ID}` is the plain voice and cannot be redefined (only its `icon` may be set)"
                );
            }
        } else if !BUILTINS.iter().any(|(b, _, _)| *b == id) {
            if !named {
                bail!("[[talk.personas]] `{id}` has an empty `name`");
            }
            if !voiced {
                bail!("[[talk.personas]] `{id}` has an empty `prompt`");
            }
        }
        if !seen.insert(id.to_owned()) {
            bail!("[[talk.personas]] declares the id `{id}` more than once");
        }
    }
    Ok(())
}

/// The built-ins, with the config's entries overriding by id or appended. An
/// override that leaves out `name` or `prompt` keeps the built-in's; on
/// `default` only the `icon` is taken.
pub fn catalog(personas: &[PersonaSpec]) -> Vec<Persona> {
    let mut out = builtins();
    for p in personas {
        let id = p.id.trim();
        let icon = p
            .icon
            .as_deref()
            .map(str::trim)
            .filter(|i| !i.is_empty())
            .map(str::to_owned);
        match out.iter_mut().find(|e| e.id == id) {
            Some(slot) if slot.is_default() => slot.icon = icon,
            Some(slot) => {
                let (name, prompt) = (p.name.trim(), p.prompt.trim());
                if !name.is_empty() {
                    slot.name = name.to_owned();
                }
                if !prompt.is_empty() {
                    slot.prompt = prompt.to_owned();
                }
                slot.icon = icon;
            }
            None => out.push(Persona {
                id: id.to_owned(),
                name: p.name.trim().to_owned(),
                prompt: p.prompt.trim().to_owned(),
                icon,
            }),
        }
    }
    out
}

/// The catalogue a conversation can pick from when no config can be read.
pub fn builtin_catalog() -> Vec<Persona> {
    catalog(&[])
}

/// Look `id` up. Blank means the default.
pub fn find(personas: &[PersonaSpec], id: &str) -> Option<Persona> {
    let id = id.trim();
    let id = if id.is_empty() { DEFAULT_ID } else { id };
    catalog(personas).into_iter().find(|p| p.id == id)
}

/// The persona to apply for a stored id: `None` for the default, and for an id
/// the config no longer knows (a warning, never a stopped conversation).
pub fn active(personas: &[PersonaSpec], id: &str) -> Option<Persona> {
    match find(personas, id) {
        Some(p) if p.is_default() => None,
        Some(p) => Some(p),
        None => {
            tracing::warn!("chat: persona `{id}` is no longer configured; using the default");
            None
        }
    }
}

/// The briefing's persona section.
pub fn section(p: &Persona) -> String {
    section_for(p, None)
}

/// [`section`] that also tells the persona how to address the operator.
pub fn section_for(p: &Persona, operator_name: Option<&str>) -> String {
    let mut out = section_base(p);
    if let Some(n) = operator_name {
        out.push_str(&addressing(n));
    }
    out
}

/// The sentence naming the operator, shared by the persona section and the
/// default voice's own short section.
pub fn addressing(operator_name: &str) -> String {
    format!(
        "\nAddress the operator as \"{operator_name}\" when you address them by \
         name. This is how they are called, not a change of tone.\n"
    )
}

fn section_base(p: &Persona) -> String {
    format!(
        "\n# Persona (tone only)\n\n\
         The operator picked the persona \"{name}\" for this conversation. It \
         governs TONE ONLY - word choice, rhythm and attitude of what you say. \
         It never changes what you do. Facts, commands, file paths, task ids, \
         how a duplicate warning is handled (never `--force` it yourself), the \
         write policy, and telling the operator the task id `magi task add` \
         prints all stay exact and exactly as described above. Do not let the \
         voice make an answer vaguer, shorter on substance, or more certain \
         than the facts. Never put the persona's voice into the <instruction> \
         you pass to `magi task add`, into files, or into commit messages: \
         those stay neutral.\n\n\
         Voice:\n{voice}\n",
        name = p.name,
        voice = p.prompt,
    )
}

/// What a resumed session is told when the persona changed since the briefing
/// it holds. `None` means the operator went back to the plain voice.
pub fn update_block(p: Option<&Persona>) -> String {
    update_block_for(p, None)
}

/// [`update_block`] naming the operator the way the briefing does.
pub fn update_block_for(p: Option<&Persona>, operator_name: Option<&str>) -> String {
    match p {
        Some(p) => format!(
            "# Persona update\n\nThe operator changed the persona. Drop any earlier \
             persona and use this one from now on.\n{}",
            section_for(p, operator_name)
        ),
        None => "# Persona update\n\nThe operator turned the persona off. Drop any earlier \
                 persona and go back to your plain, normal voice from now on.\n"
            .to_owned(),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn spec(id: &str, name: &str, prompt: &str) -> PersonaSpec {
        PersonaSpec {
            id: id.into(),
            name: name.into(),
            prompt: prompt.into(),
            icon: None,
        }
    }

    fn with_icon(mut s: PersonaSpec, icon: &str) -> PersonaSpec {
        s.icon = Some(icon.into());
        s
    }

    #[test]
    fn builtins_start_with_a_plain_default() {
        let c = builtin_catalog();
        assert_eq!(c[0].id, DEFAULT_ID);
        assert!(c[0].prompt.is_empty());
        for id in [
            "magi", "rei", "misato", "ritsuko", "shinji", "asuka", "kaworu",
        ] {
            let p = c.iter().find(|p| p.id == id).expect(id);
            assert!(p.prompt.contains("operator's language"), "{id}");
        }
    }

    #[test]
    fn a_user_entry_adds_or_overrides_in_place() {
        let c = catalog(&[
            spec("rei", "Rei", "Be curt."),
            spec("gendo", "Gendo", "Be cold."),
        ]);
        let pos = c.iter().position(|p| p.id == "rei").unwrap();
        assert_eq!(pos, 2);
        assert_eq!(c[pos].prompt, "Be curt.");
        assert_eq!(c.last().unwrap().id, "gendo");
        assert_eq!(c.len(), builtin_catalog().len() + 1);
    }

    #[test]
    fn validation_names_the_problem() {
        let err = |s: Vec<PersonaSpec>| validate(&s).unwrap_err().to_string();
        assert!(err(vec![spec(" ", "n", "p")]).contains("empty `id`"));
        assert!(err(vec![spec("a", "n", "  ")]).contains("empty `prompt`"));
        assert!(err(vec![spec("a", "", "p")]).contains("empty `name`"));
        assert!(err(vec![spec("a", "n", "p"), spec("a", "m", "q")]).contains("more than once"));
        assert!(err(vec![spec("default", "n", "p")]).contains("cannot be redefined"));
        assert!(validate(&[spec("a", "n", "p")]).is_ok());
    }

    #[test]
    fn active_treats_default_blank_and_unknown_as_plain() {
        assert!(active(&[], "").is_none());
        assert!(active(&[], DEFAULT_ID).is_none());
        assert!(active(&[], "nobody").is_none());
        assert_eq!(active(&[], "rei").unwrap().id, "rei");
    }

    #[test]
    fn a_built_in_override_may_set_only_an_icon() {
        let only = with_icon(spec("rei", "", ""), "data:image/png;base64,AAAA");
        assert!(validate(std::slice::from_ref(&only)).is_ok());
        let c = catalog(&[only]);
        let rei = c.iter().find(|p| p.id == "rei").unwrap();
        let base = builtin_catalog();
        let plain = base.iter().find(|p| p.id == "rei").unwrap();
        assert_eq!((&rei.name, &rei.prompt), (&plain.name, &plain.prompt));
        assert_eq!(rei.icon.as_deref(), Some("data:image/png;base64,AAAA"));
        // A new id still needs both.
        assert!(validate(&[with_icon(spec("x", "", ""), "~/a.png")]).is_err());
    }

    #[test]
    fn default_takes_an_icon_and_nothing_else() {
        let abs = "~/d.png";
        assert!(validate(&[with_icon(spec("default", "", ""), abs)]).is_ok());
        assert!(validate(&[with_icon(spec("default", "N", ""), abs)]).is_err());
        assert!(validate(&[with_icon(spec("default", "", "P"), abs)]).is_err());
        assert!(validate(&[spec("default", "", "")]).is_err());
        let c = catalog(&[with_icon(spec("default", "", ""), abs)]);
        assert_eq!(c[0].icon.as_deref(), Some(abs));
        assert!(c[0].prompt.is_empty());
    }

    #[test]
    fn a_bad_icon_shape_does_not_invalidate_the_persona() {
        for bad in [
            "rel.png",
            "/a/../b.png",
            "/a/b.svg",
            "data:text/html;base64,AA",
        ] {
            assert!(validate(&[with_icon(spec("a", "n", "p"), bad)]).is_ok());
            assert!(crate::persona_icon::load(bad).is_err());
        }
    }

    #[test]
    fn an_icon_never_reaches_the_briefing() {
        let without_icon = Persona {
            id: "rei".into(),
            name: "Rei".into(),
            prompt: "Be curt.".into(),
            icon: None,
        };
        let iconed = Persona {
            icon: Some("~/r.png".into()),
            ..without_icon.clone()
        };
        assert_eq!(section(&without_icon), section(&iconed));
        assert_eq!(
            section_for(&without_icon, Some("Cmdr")),
            section_for(&iconed, Some("Cmdr"))
        );
        assert_eq!(
            update_block(Some(&without_icon)),
            update_block(Some(&iconed))
        );
        // The default voice is unchanged by an icon on it.
        let d = catalog(&[with_icon(spec("default", "", ""), "~/d.png")]);
        assert!(active(&[with_icon(spec("default", "", ""), "~/d.png")], "").is_none());
        assert!(d[0].prompt.is_empty());
    }
}