layover-core 0.23.1

Domain types for Layover: factory configuration, route graph, itinerary accounting and rendezvous barriers.
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
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
//! Checks that every agent's prompt actually composes, for every way it can be reached.
//!
//! These need to read prompt files, so they are separate from the pure-TOML checks and are run
//! through [`crate::validate::validate_prompts`].
//!
//! The subtle part is *which* flags an agent may assume. A run receives the flags of the one
//! pipeline that triggered it, never the union of every pipeline in the factory. So a prompt is
//! only safe if every flag it tests is declared by **each** entry point that can reach it —
//! checking against the union would pass a factory that fails the moment the other pipeline runs.

use std::collections::{BTreeMap, BTreeSet};

use crate::agent::{AgentName, PromptSpec};
use crate::config::Config;
use crate::graph::RouteGraph;
use crate::pipeline::Flags;
use crate::prompt::{PromptSource, referenced_flags, resolve};
use crate::tools::unknown_tools_in;

use super::Diagnostic;

pub(super) fn check_prompt_files(
    config: &Config,
    source: &dyn PromptSource,
    found: &mut Vec<Diagnostic>,
) {
    let referenced = collect_referenced_flags(config, source, found);
    check_flags_are_available_at_every_entry(config, &referenced, found);
    check_tools_exist(config, source, found);
}

/// Refuses a prompt that tells an agent to call a tool Layover does not offer.
///
/// An agent instructed to use a tool it does not have will improvise, and improvising is what a
/// factory is meant not to do unattended. This is also the check that would have caught the drift
/// it was written in response to: eleven tool names were documented across prompts and the book,
/// and none of them existed.
fn check_tools_exist(config: &Config, source: &dyn PromptSource, found: &mut Vec<Diagnostic>) {
    for (name, agent) in &config.agents {
        let text = match agent.prompt_spec() {
            Ok(PromptSpec::Inline(text)) => text,
            Ok(PromptSpec::File(path)) => match resolve(source, &path, &Flags::default()) {
                Ok(text) => text,
                // A prompt that will not compose is already reported elsewhere; saying so twice
                // makes the real problem harder to find.
                Err(_) => continue,
            },
            Err(_) => continue,
        };

        for unknown in unknown_tools_in(&text) {
            found.push(Diagnostic::error(format!(
                "agent `{name}`'s prompt tells it to call `{unknown}`, which is not a tool \
                 Layover offers; an agent told to use a tool it does not have will improvise"
            )));
        }
    }
}

/// Reads every agent's prompt once, reporting anything that does not compose.
///
/// Conditions are not evaluated, so a flag behind a branch that is currently false is still
/// collected: it has to be declared, or the run that turns it on would fail.
fn collect_referenced_flags(
    config: &Config,
    source: &dyn PromptSource,
    found: &mut Vec<Diagnostic>,
) -> BTreeMap<AgentName, BTreeSet<String>> {
    let mut referenced = BTreeMap::new();

    for (name, agent) in &config.agents {
        let Ok(PromptSpec::File(path)) = agent.prompt_spec() else {
            continue;
        };

        match referenced_flags(source, &path) {
            Ok(flags) => {
                referenced.insert(name.clone(), flags);
            }
            Err(error) => found.push(Diagnostic::error(format!(
                "agent `{name}` has an unusable prompt: {error}"
            ))),
        }
    }

    referenced
}

/// Every flag a reachable agent tests must be declared by the entry point that can reach it.
fn check_flags_are_available_at_every_entry(
    config: &Config,
    referenced: &BTreeMap<AgentName, BTreeSet<String>>,
    found: &mut Vec<Diagnostic>,
) {
    if referenced.is_empty() {
        return;
    }

    let graph = RouteGraph::from_config(config);

    for entry in entry_points(config) {
        let reachable = graph.reachable_from([&entry.agent]);

        for (agent, flags) in referenced {
            if !reachable.contains(agent) {
                continue;
            }

            for flag in flags {
                if entry.available.contains(flag.as_str()) {
                    continue;
                }

                found.push(Diagnostic::error(format!(
                    "agent `{agent}` tests flag `{flag}` in its prompt, but {} can reach it \
                     without declaring that flag; the run would fail when the prompt is composed",
                    entry.label
                )));
            }
        }
    }
}

/// One way work can enter the mesh, and the flags a run entering that way would carry.
struct Entry<'a> {
    agent: AgentName,
    available: BTreeSet<&'a str>,
    label: String,
}

fn entry_points(config: &Config) -> Vec<Entry<'_>> {
    let mut entries: Vec<Entry<'_>> = config
        .pipelines
        .iter()
        .map(|(name, pipeline)| Entry {
            agent: pipeline.entry.clone(),
            available: pipeline.flags.keys().map(String::as_str).collect(),
            label: format!("pipeline `{name}`"),
        })
        .collect();

    // A bare `entry = true` agent is triggered without a pipeline, so no flags are declared and
    // none can be supplied. Any conditional prompt downstream of it is unreachable in practice.
    entries.extend(
        config
            .agents
            .iter()
            .filter(|(_, agent)| agent.entry)
            .map(|(name, _)| Entry {
                agent: name.clone(),
                available: BTreeSet::new(),
                label: format!("`entry = true` on agent `{name}`"),
            }),
    );

    entries
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::prompt::PromptMap;
    use crate::validate::{Severity, validate_prompts};

    fn config(body: &str) -> Config {
        Config::from_toml(
            &format!(
                r#"
                [runners.claude]
                command = ["claude", "-p", "{{prompt}}"]
                {body}
                "#
            ),
            "test.toml",
        )
        .expect("config parses")
    }

    fn errors(config: &Config, source: &PromptMap) -> Vec<String> {
        validate_prompts(config, source)
            .into_iter()
            .filter(|d| d.severity == Severity::Error)
            .map(|d| d.message)
            .collect()
    }

    fn tester_source() -> PromptMap {
        PromptMap::new()
            .with("tester.md", "Run the suite.\n@include(run_e2e) e2e.md\n")
            .with("e2e.md", "Also run the remote suite.\n")
    }

    #[test]
    fn an_inline_prompt_is_not_inspected() {
        let config = config(
            r#"
            [agents.planner]
            runner = "claude"
            prompt = "plan"
            entry = true
            "#,
        );

        assert_eq!(validate_prompts(&config, &PromptMap::new()), Vec::new());
    }

    #[test]
    fn a_missing_prompt_file_is_an_error() {
        let config = config(
            r#"
            [agents.planner]
            runner = "claude"
            prompt_file = "planner.md"
            entry = true
            "#,
        );

        let found = errors(&config, &PromptMap::new());
        assert!(
            found.iter().any(|m| m.contains("does not exist")),
            "got {found:?}"
        );
    }

    #[test]
    fn a_malformed_directive_is_an_error() {
        let config = config(
            r#"
            [agents.tester]
            runner = "claude"
            prompt_file = "tester.md"
            entry = true
            "#,
        );
        let source = PromptMap::new().with("tester.md", "@include(  ) e2e.md\n");

        let found = errors(&config, &source);
        assert!(
            found.iter().any(|m| m.contains("@include directive")),
            "got {found:?}"
        );
    }

    #[test]
    fn a_prompt_testing_a_flag_its_pipeline_declares_is_accepted() {
        let config = config(
            r#"
            [agents.tester]
            runner = "claude"
            prompt_file = "tester.md"

            [pipelines.development]
            entry = "tester"

            [pipelines.development.flags]
            run_e2e = { default = false }
            "#,
        );

        assert_eq!(validate_prompts(&config, &tester_source()), Vec::new());
    }

    #[test]
    fn a_prompt_testing_an_undeclared_flag_is_an_error() {
        let config = config(
            r#"
            [agents.tester]
            runner = "claude"
            prompt_file = "tester.md"

            [pipelines.development]
            entry = "tester"
            "#,
        );

        let found = errors(&config, &tester_source());
        assert!(found.iter().any(|m| m.contains("run_e2e")), "got {found:?}");
    }

    #[test]
    fn a_second_pipeline_that_omits_the_flag_is_an_error() {
        // The check that the union-of-all-flags version got wrong. `nightly` can reach the tester
        // without declaring `run_e2e`, so a nightly run would fail while a development run
        // succeeded — and the factory would look fine at load time.
        let config = config(
            r#"
            [agents.tester]
            runner = "claude"
            prompt_file = "tester.md"

            [pipelines.development]
            entry = "tester"

            [pipelines.development.flags]
            run_e2e = { default = false }

            [pipelines.nightly]
            entry = "tester"
            trigger = { every = "1d" }
            "#,
        );

        let found = errors(&config, &tester_source());
        assert!(
            found
                .iter()
                .any(|m| m.contains("run_e2e") && m.contains("`nightly`")),
            "got {found:?}"
        );
    }

    #[test]
    fn a_flag_must_be_declared_by_every_pipeline_that_reaches_it_transitively() {
        // The tester is two edges downstream, so this is about reachability rather than about
        // being an entry agent.
        let config = config(
            r#"
            [agents.analyst]
            runner = "claude"
            prompt = "analyse"

            [agents.tester]
            runner = "claude"
            prompt_file = "tester.md"

            [[routes]]
            from = "analyst"
            to = "tester"

            [pipelines.development]
            entry = "analyst"

            [pipelines.development.flags]
            run_e2e = { default = false }

            [pipelines.nightly]
            entry = "analyst"
            trigger = { every = "1d" }
            "#,
        );

        let found = errors(&config, &tester_source());
        assert!(
            found.iter().any(|m| m.contains("`nightly`")),
            "a flag must survive every route into the agent, got {found:?}"
        );
    }

    #[test]
    fn a_bare_entry_agent_cannot_reach_a_conditional_prompt() {
        // `entry = true` is triggered without a pipeline, so no flags exist to be supplied. A
        // conditional prompt downstream of it would fail at composition time.
        let config = config(
            r#"
            [agents.tester]
            runner = "claude"
            prompt_file = "tester.md"
            entry = true
            "#,
        );

        let found = errors(&config, &tester_source());
        assert!(
            found.iter().any(|m| m.contains("entry = true")),
            "got {found:?}"
        );
    }

    #[test]
    fn an_agent_no_entry_point_reaches_is_not_flagged_here() {
        // Unreachability is reported by the reach check; reporting it twice, as a prompt error,
        // would be noise.
        let config = config(
            r#"
            [agents.planner]
            runner = "claude"
            prompt = "plan"
            entry = true

            [agents.tester]
            runner = "claude"
            prompt_file = "tester.md"
            "#,
        );

        assert_eq!(errors(&config, &tester_source()), Vec::<String>::new());
    }

    #[test]
    fn a_prompt_that_includes_itself_is_refused_at_load_time() {
        // Validation applies exactly the rules composition does, so a cycle is caught here rather
        // than by the first run that tries to assemble the prompt.
        let config = config(
            r#"
            [agents.tester]
            runner = "claude"
            prompt_file = "tester.md"
            entry = true
            "#,
        );
        let source = PromptMap::new().with("tester.md", "@include tester.md\n");

        let found = errors(&config, &source);
        assert!(
            found.iter().any(|m| m.contains("includes itself")),
            "got {found:?}"
        );
    }

    #[test]
    fn a_prompt_nested_too_deeply_is_refused_at_load_time() {
        let config = config(
            r#"
            [agents.tester]
            runner = "claude"
            prompt_file = "l0.md"
            entry = true
            "#,
        );

        let mut source = PromptMap::new();
        for level in 0..12 {
            source = source.with(
                format!("l{level}.md"),
                format!("@include l{}.md\n", level + 1),
            );
        }
        source = source.with("l12.md", "bottom\n");

        let found = errors(&config, &source);
        assert!(
            found.iter().any(|m| m.contains("nests includes")),
            "got {found:?}"
        );
    }
}