Skip to main content

layover_core/
payload.rs

1//! What a run is told, and in what order.
2//!
3//! # Why the order is a decision and not a detail
4//!
5//! A run may be handed five different things: the agent's own instructions, its memory, what
6//! earlier runs of it learned, a note explaining that this attempt follows an interrupted one, and
7//! the message that woke it. Models weight the beginning and the end of a context differently, and
8//! whatever arrives last reads as *the current instruction*.
9//!
10//! So the order is:
11//!
12//! ```text
13//! 1. Identity        who you are -- from the Tower, never asserted by the agent
14//! 2. Instructions    the composed prompt, @include resolved
15//! 3. Memory          what this agent wrote down for itself, tail-capped
16//! 4. Brief           what earlier runs learned, and how to ask for help
17//! 5. Handover        only when this run follows an interrupted one, or a human steer
18//! 6. Flight body     the message that woke it -- last
19//! ```
20//!
21//! The body is last because it is the instruction; everything above is context for carrying it
22//! out. The handover sits immediately above the body because it frames *this attempt* — "you are
23//! continuing work that did not finish" only means anything next to what the work is.
24//!
25//! Learnings go above the handover rather than below, so a recovery instruction is never buried
26//! under twenty-five lines of accumulated advice.
27//!
28//! # Why this is a pure function
29//!
30//! Composing what a run is told and *starting* a run are different problems, and only the second
31//! needs a process. Keeping them apart means the thing most likely to be wrong — the text an agent
32//! actually receives — can be read, diffed and tested without spawning anything, and
33//! `layover prompt` can show it to a human before it costs money.
34
35use std::fmt::Write as _;
36
37use crate::agent::AgentName;
38
39/// How much of an agent's memory is injected.
40///
41/// Memory is injected rather than fetched because an agent that forgets to call for it simply has
42/// no memory, and nothing anywhere would report that. Silent failure is the thing this project
43/// exists to avoid.
44///
45/// But memory grows without bound and the payload it joins already runs to tens of kilobytes, so
46/// what arrives is the **tail** — the most recent thing the agent wrote down. When it is cut, the
47/// text says so, because an agent that knows it is seeing an excerpt can go and read the rest.
48pub const MAX_MEMORY: usize = 4_096;
49
50/// Everything one run is told.
51///
52/// Borrowed rather than owned: every part of this already exists somewhere the Tower holds, and
53/// copying a 34 KB prompt to build a struct that immediately concatenates it is wasted work.
54#[derive(Debug, Clone, Copy)]
55pub struct Run<'a> {
56    /// Which agent this is. Comes from the Tower's own record, never from the agent.
57    pub agent: &'a AgentName,
58    /// The agent's composed instructions, with `@include` already resolved.
59    pub instructions: &'a str,
60    /// What the agent wrote down for itself, if anything.
61    pub memory: Option<&'a str>,
62    /// Learnings and the help instructions, from [`crate::brief::brief`].
63    pub brief: &'a str,
64    /// Why this run follows another, from [`crate::handover::Handover::brief`]. Empty for an
65    /// ordinary dispatch.
66    pub handover: Option<&'a str>,
67    /// The message that woke this agent.
68    pub body: &'a str,
69}
70
71/// Renders the payload a run receives on stdin.
72///
73/// This is the whole of what the process is told. There is no second channel: a runner that takes
74/// a file gets this same text written to a path, and one that does not gets it on stdin.
75#[must_use]
76pub fn compose(run: &Run<'_>) -> String {
77    let mut out = String::with_capacity(
78        run.instructions.len() + run.brief.len() + run.body.len() + MAX_MEMORY + 256,
79    );
80
81    write_identity(&mut out, run.agent);
82    write_section(&mut out, run.instructions);
83
84    if let Some(memory) = run.memory {
85        write_memory(&mut out, memory);
86    }
87
88    write_section(&mut out, run.brief);
89
90    if let Some(handover) = run.handover {
91        write_section(&mut out, handover);
92    }
93
94    write_body(&mut out, run.body);
95    out
96}
97
98/// States who the agent is.
99///
100/// Three words, and without them every prompt file has to hard-code its own agent's name — which
101/// drifts the first time somebody renames one. It also has to come from here rather than from the
102/// agent: identity the agent asserts is identity an agent can lie about.
103fn write_identity(out: &mut String, agent: &AgentName) {
104    let _ = writeln!(out, "You are `{agent}`.\n");
105}
106
107/// Appends a block, keeping exactly one blank line between sections.
108fn write_section(out: &mut String, text: &str) {
109    let text = text.trim();
110    if text.is_empty() {
111        return;
112    }
113
114    out.push_str(text);
115    out.push_str("\n\n");
116}
117
118/// Appends the agent's own memory, cut to the most recent [`MAX_MEMORY`] bytes.
119fn write_memory(out: &mut String, memory: &str) {
120    let memory = memory.trim();
121    if memory.is_empty() {
122        return;
123    }
124
125    out.push_str("== WHAT YOU WROTE DOWN LAST TIME ==\n");
126    out.push_str(
127        "Runs are a clean slate, so this is everything you remember. It is what earlier runs of \
128         you chose to record, and nothing else carried over.\n\n",
129    );
130
131    if memory.len() <= MAX_MEMORY {
132        out.push_str(memory);
133        out.push_str("\n\n");
134        return;
135    }
136
137    // Cut from the front: the end of the file is the most recent thing written, and a memory that
138    // keeps only its oldest entries gets less useful the longer an agent runs.
139    let mut start = memory.len() - MAX_MEMORY;
140    while start < memory.len() && !memory.is_char_boundary(start) {
141        start += 1;
142    }
143
144    out.push_str(
145        "[This is the most recent part of your memory. It is longer than fits here — call \
146         `layover_memory_read` for the whole file.]\n\n",
147    );
148    out.push_str(memory[start..].trim_start());
149    out.push_str("\n\n");
150}
151
152/// Appends the message that woke the agent.
153fn write_body(out: &mut String, body: &str) {
154    out.push_str("== WHAT YOU HAVE BEEN ASKED TO DO ==\n\n");
155    out.push_str(body.trim());
156    out.push('\n');
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    fn agent() -> AgentName {
164        AgentName::new("tester")
165    }
166
167    fn minimal<'a>(agent: &'a AgentName, body: &'a str) -> Run<'a> {
168        Run {
169            agent,
170            instructions: "Run the suite and report what failed.",
171            memory: None,
172            brief: "",
173            handover: None,
174            body,
175        }
176    }
177
178    /// The order *is* the decision, so it is pinned rather than left to whoever edits next.
179    #[test]
180    fn the_sections_arrive_in_the_settled_order() {
181        let name = agent();
182        let run = Run {
183            agent: &name,
184            instructions: "INSTRUCTIONS",
185            memory: Some("MEMORY"),
186            brief: "BRIEF",
187            handover: Some("HANDOVER"),
188            body: "BODY",
189        };
190
191        let text = compose(&run);
192        let at = |needle: &str| {
193            text.find(needle)
194                .unwrap_or_else(|| panic!("missing {needle}"))
195        };
196
197        assert!(at("You are `tester`") < at("INSTRUCTIONS"));
198        assert!(at("INSTRUCTIONS") < at("MEMORY"));
199        assert!(at("MEMORY") < at("BRIEF"));
200        assert!(
201            at("BRIEF") < at("HANDOVER"),
202            "a recovery instruction must not be buried under accumulated advice"
203        );
204        assert!(
205            at("HANDOVER") < at("BODY"),
206            "the message that woke the agent reads as the current instruction, so it goes last"
207        );
208    }
209
210    #[test]
211    fn the_body_is_the_last_thing_in_the_payload() {
212        let name = agent();
213        let text = compose(&minimal(&name, "Fix the retry policy."));
214
215        assert!(text.trim_end().ends_with("Fix the retry policy."), "{text}");
216    }
217
218    #[test]
219    fn identity_comes_from_the_tower_and_is_always_present() {
220        let name = agent();
221        assert!(compose(&minimal(&name, "go")).starts_with("You are `tester`."));
222    }
223
224    #[test]
225    fn absent_sections_leave_no_hole() {
226        let name = agent();
227        let text = compose(&minimal(&name, "go"));
228
229        assert!(!text.contains("WHAT YOU WROTE DOWN"), "{text}");
230        assert!(
231            !text.contains("\n\n\n"),
232            "blank lines should not stack: {text:?}"
233        );
234    }
235
236    #[test]
237    fn memory_shorter_than_the_cap_arrives_whole_and_says_nothing_about_cutting() {
238        let name = agent();
239        let run = Run {
240            memory: Some("The e2e suite needs VPN. Ask before assuming a failure is real."),
241            ..minimal(&name, "go")
242        };
243
244        let text = compose(&run);
245        assert!(text.contains("needs VPN"), "{text}");
246        assert!(!text.contains("most recent part"), "{text}");
247    }
248
249    #[test]
250    fn over_long_memory_keeps_the_end_not_the_beginning() {
251        // The end is the most recent thing written. A memory that kept only its oldest entries
252        // would get less useful the longer an agent ran, which is the opposite of the point.
253        let name = agent();
254        let memory = format!("OLDEST{}NEWEST", "x".repeat(MAX_MEMORY * 2));
255        let run = Run {
256            memory: Some(&memory),
257            ..minimal(&name, "go")
258        };
259
260        let text = compose(&run);
261        assert!(text.contains("NEWEST"), "the recent end was dropped");
262        assert!(!text.contains("OLDEST"), "the old end was kept");
263    }
264
265    #[test]
266    fn a_cut_memory_says_so_and_says_where_the_rest_is() {
267        let name = agent();
268        let memory = "y".repeat(MAX_MEMORY * 2);
269        let run = Run {
270            memory: Some(&memory),
271            ..minimal(&name, "go")
272        };
273
274        let text = compose(&run);
275        assert!(text.contains("longer than fits here"), "{text}");
276        assert!(
277            text.contains("layover_memory_read"),
278            "an agent told it has more should be told how to get it: {text}"
279        );
280    }
281
282    #[test]
283    fn cutting_memory_never_splits_a_character() {
284        let name = agent();
285        // Multi-byte throughout, so a naive byte offset lands mid-character.
286        let memory = "é".repeat(MAX_MEMORY);
287        let run = Run {
288            memory: Some(&memory),
289            ..minimal(&name, "go")
290        };
291
292        assert!(compose(&run).contains('é'));
293    }
294
295    #[test]
296    fn an_ordinary_dispatch_carries_no_handover() {
297        let name = agent();
298        let text = compose(&minimal(&name, "go"));
299
300        assert!(!text.contains("continuing"), "{text}");
301    }
302
303    #[test]
304    fn empty_memory_is_the_same_as_no_memory() {
305        let name = agent();
306        let blank = Run {
307            memory: Some("   \n  "),
308            ..minimal(&name, "go")
309        };
310
311        assert_eq!(compose(&blank), compose(&minimal(&name, "go")));
312    }
313}