Skip to main content

lash_protocol_rlm/protocol/
prompt.rs

1pub const LASHLANG_BUILTINS_SECTION: &str = r#"### Builtins
2
3Call as functions (e.g. `len(x)`, `slice(s, 0, 200)`). For `slice`, `null` bounds mean start/end; negative bounds count from the end.
4
5- `len(x)` — length of string/list/record (0 for null); use `image.size` for images
6- `empty(x)` — true if length is 0
7- `slice(s, start, end)` — substring or sublist
8- `range(end)` / `range(start, end)` / `range(start, end, step)` — integer list, end-exclusive; positive or negative `step`, never `0`
9- `ceil_div(a, b)` / `floor_div(a, b)` — integer division helpers for chunk/count math; divisor must not be `0`
10- `push(list, item)` — new list with one item appended
11- `split(s, sep)` / `join(list, sep)` — string split/join
12- `find(s, needle, start?)` — zero-based character index of the first literal match, or `null`; `start` defaults to `0` and is a non-negative character index; an empty `needle` returns `start` when it is in bounds
13- `grep_text(s, needle)` — literal in-memory line search; `needle` must be non-empty; returns one record per matching line: `{ line: int, text: str, match: str, start: int, end: int }`, where `line` is 1-based, `text` is the line without its line ending, and `start`/`end` are zero-based character offsets within that line's `text` with `end` exclusive
14- `trim(s)` — strip whitespace
15- `starts_with(s, prefix)` / `ends_with(s, suffix)` / `contains(haystack, needle)`
16- `keys(record)` / `values(record)`
17- `to_string(x)` / `to_int(x)` / `to_float(x)`
18- `json_parse(s)` — parse a JSON string into a value
19- `format(template, arg0, arg1, ...)` — positional interpolation: `{}` auto-numbers, `{0}` / `{1}` pick a specific arg, `{{` / `}}` escape literal braces. Do not wrap args in a list: use `format("It is {}.", trim(now.output))`, not `format("It is {}.", [trim(now.output)])`.
20- `validate(value, Type { ... })` — check an intermediate value against a Type literal and return it unchanged, or abort with a validation error
21"#;
22
23pub const LASHLANG_BUILTINS_NO_IMAGES_SECTION: &str = r#"### Builtins
24
25Call as functions (e.g. `len(x)`, `slice(s, 0, 200)`). For `slice`, `null` bounds mean start/end; negative bounds count from the end.
26
27- `len(x)` — length of string/list/record (0 for null)
28- `empty(x)` — true if length is 0
29- `slice(s, start, end)` — substring or sublist
30- `range(end)` / `range(start, end)` / `range(start, end, step)` — integer list, end-exclusive; positive or negative `step`, never `0`
31- `ceil_div(a, b)` / `floor_div(a, b)` — integer division helpers for chunk/count math; divisor must not be `0`
32- `push(list, item)` — new list with one item appended
33- `split(s, sep)` / `join(list, sep)` — string split/join
34- `find(s, needle, start?)` — zero-based character index of the first literal match, or `null`; `start` defaults to `0` and is a non-negative character index; an empty `needle` returns `start` when it is in bounds
35- `grep_text(s, needle)` — literal in-memory line search; `needle` must be non-empty; returns one record per matching line: `{ line: int, text: str, match: str, start: int, end: int }`, where `line` is 1-based, `text` is the line without its line ending, and `start`/`end` are zero-based character offsets within that line's `text` with `end` exclusive
36- `trim(s)` — strip whitespace
37- `starts_with(s, prefix)` / `ends_with(s, suffix)` / `contains(haystack, needle)`
38- `keys(record)` / `values(record)`
39- `to_string(x)` / `to_int(x)` / `to_float(x)`
40- `json_parse(s)` — parse a JSON string into a value
41- `format(template, arg0, arg1, ...)` — positional interpolation: `{}` auto-numbers, `{0}` / `{1}` pick a specific arg, `{{` / `}}` escape literal braces. Do not wrap args in a list: use `format("It is {}.", trim(now.output))`, not `format("It is {}.", [trim(now.output)])`.
42- `validate(value, Type { ... })` — check an intermediate value against a Type literal and return it unchanged, or abort with a validation error
43"#;
44
45pub const LASHLANG_COMMON_PATTERNS_SECTION: &str = r#"### Common patterns
46
47Operation-level errors are different from successful results that contain domain errors. `?` aborts the block only when the module operation itself failed:
48
49```lashlang
50probe = await web.search({ query: "value" })?
51submit format("Search returned {} characters.", len(to_string(probe)))
52```
53
54Build collections with explicit loops, not comprehensions:
55
56```lashlang
57items = []
58for key in ["a", "b"] {
59  items = push(items, { key: key, size: len(key) })
60}
61submit format("Built {} items.", len(items))
62```
63
64Print what you need to see. Pull in the parts relevant to your next step — a whole value when it is small and all useful, otherwise keys, lengths, selected fields, or slices of large ones:
65
66```lashlang
67result = await web.search({ query: "value" })?
68text = to_string(result)
69print { chars: len(text), head: slice(text, 0, 1200) }
70```
71
72For dependent multi-step work, inspect intermediate results before submitting success. Reaching `submit` ends the turn even mid-block:
73
74```lashlang
75first = await web.search({ query: "value" })?
76second = await web.fetch({ url: first[0].url })?
77if contains(to_string(second), "needs_more_work") {
78  print { first: first, second: second }
79} else {
80  submit format("Fetched result: {}", slice(to_string(second), 0, 1200))
81}
82```
83"#;
84
85pub const LASHLANG_TYPE_LITERALS_SECTION: &str = r#"### Type literals
86
87`Type { field: shape, ... }` describes a record shape. Field separators are commas (trailing comma OK).
88
89- Scalars: `str`, `int`, `float`, `bool`, `dict`, `any`, `null`.
90- Collections: `list[shape]`, `enum["a", "b"]`, nested `Type { ... }`.
91- **Optional field** — put `?` after the type: `email: str?` means the field may be absent from the record. If the field IS present, its value must be a string; `null` is **not** allowed.
92- **Nullable field** — use a union with `null`: `email: str | null` means the field is required and its value is either a string or null.
93- **Unions** — `a | b | c`, e.g. `status: str | int`, `value: str | null`.
94- Nested shapes require the `Type` keyword: `nested: Type { ok: bool }` (bare `{ ok: bool }` is rejected — that's a record value, not a type).
95
96```lashlang
97profile = validate(record, Type { name: str, email: str?, tags: list[str] })
98```
99"#;
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
102#[serde(default)]
103pub struct RlmPromptFeatures {
104    pub images: bool,
105    pub common_patterns: bool,
106    pub type_literals: bool,
107    pub decomposition: bool,
108}
109
110impl Default for RlmPromptFeatures {
111    fn default() -> Self {
112        Self {
113            images: true,
114            common_patterns: true,
115            type_literals: true,
116            decomposition: true,
117        }
118    }
119}
120
121pub fn rlm_execution_section_for_surface(
122    features: RlmPromptFeatures,
123    surface: &lashlang::LashlangSurface,
124) -> String {
125    let has_operations = surface.resources.has_operations();
126    let mut sections = Vec::new();
127    sections.push(render_execution_intro(has_operations));
128    sections.push(render_language_section(
129        features.images,
130        has_operations,
131        &surface.abilities,
132        &surface.language_features,
133    ));
134    if let Some(section) = render_host_surface_section(surface) {
135        sections.push(section);
136    }
137    sections.push(if features.images {
138        LASHLANG_BUILTINS_SECTION.to_string()
139    } else {
140        LASHLANG_BUILTINS_NO_IMAGES_SECTION.to_string()
141    });
142    if features.common_patterns {
143        sections.push(render_common_patterns(has_operations));
144    }
145    if features.type_literals {
146        sections.push(LASHLANG_TYPE_LITERALS_SECTION.to_string());
147    }
148    if features.decomposition {
149        sections.push(render_decomposition_section(
150            has_operations,
151            surface.abilities.processes,
152        ));
153    }
154    sections.join("\n\n")
155}
156
157fn render_host_surface_section(surface: &lashlang::LashlangSurface) -> Option<String> {
158    let mut operation_lines = Vec::new();
159    for (_, module) in surface.resources.module_instances() {
160        if let Some(resource_type) =
161            surface
162                .resources
163                .resolve_alias(&lashlang::ResourceRefExpr::resolved(
164                    module
165                        .path
166                        .iter()
167                        .map(|segment| segment.as_str().into())
168                        .collect(),
169                    module.resource_type.clone(),
170                    module.alias.clone(),
171                ))
172        {
173            for (operation, binding) in &resource_type.operations {
174                operation_lines.push(format!(
175                    "- `await {}.{}({})? -> {}`",
176                    module.alias,
177                    operation,
178                    lashlang::format_type_expr(&binding.input_ty),
179                    lashlang::format_type_expr(&binding.output_ty)
180                ));
181            }
182        }
183    }
184    let mut data_type_lines = Vec::new();
185    for (_, data_type) in surface.resources.named_data_types() {
186        data_type_lines.push(format!(
187            "- `type {} = {}`",
188            data_type.name(),
189            lashlang::format_type_expr(data_type.ty())
190        ));
191    }
192    let mut constructor_lines = Vec::new();
193    for (_, constructor) in surface.resources.value_constructors() {
194        let output_ty = match &constructor.output_ty {
195            lashlang::TypeExpr::Ref(name) => surface
196                .resources
197                .resolve_trigger_source(name.as_str())
198                .map(|binding| format!("TriggerSource<{}>", binding.event_type_name()))
199                .unwrap_or_else(|| lashlang::format_type_expr(&constructor.output_ty)),
200            _ => lashlang::format_type_expr(&constructor.output_ty),
201        };
202        constructor_lines.push(format!(
203            "- `{}({}) -> {}`",
204            constructor.path.join("."),
205            lashlang::format_type_expr(&constructor.input_ty),
206            output_ty
207        ));
208    }
209    let mut protocol_lines = Vec::new();
210    let trigger_register = lashlang::TriggerHostOperation::Register.host_operation();
211    for (source_ty, binding) in surface.resources.trigger_sources() {
212        protocol_lines.push(format!(
213            "- `{}` can be passed to `{}` and emits `{}`",
214            source_ty,
215            trigger_register,
216            binding.event_type_name()
217        ));
218    }
219    if operation_lines.is_empty()
220        && data_type_lines.is_empty()
221        && constructor_lines.is_empty()
222        && protocol_lines.is_empty()
223    {
224        return None;
225    }
226    let mut section = String::from("### Host Surface");
227    if !operation_lines.is_empty() {
228        section.push_str("\n\nAwaited runtime operations:\n\n");
229        section.push_str(&operation_lines.join("\n"));
230    }
231    if !data_type_lines.is_empty() {
232        section.push_str("\n\nNamed host data types:\n\n");
233        section.push_str(&data_type_lines.join("\n"));
234    }
235    if !constructor_lines.is_empty() {
236        section.push_str("\n\nPure value constructors. Do not `await` these; use them wherever expressions are allowed:\n\n");
237        section.push_str(&constructor_lines.join("\n"));
238    }
239    if !protocol_lines.is_empty() {
240        section.push_str("\n\nTrigger source protocol metadata:\n\n");
241        section.push_str(&protocol_lines.join("\n"));
242    }
243    Some(section)
244}
245
246fn render_execution_intro(has_operations: bool) -> String {
247    let mut section = String::from("**All actions go through `lashlang`.** ");
248    if has_operations {
249        section.push_str("Invoke documented operations with module syntax like `await agents.spawn({ ... })?` or `await web.search({ ... })?` from inside a fenced `lashlang` block. Start from operations listed under **Showcased Tools**; if a discovery tool is available, use it to find additional module call forms before calling them. Emit a block whenever you need to call an available operation or compute a value. Plain prose is for direct conversational replies that need no action.");
250    } else {
251        section.push_str("Use fenced `lashlang` blocks to compute values, inspect current variables, and submit final answers. No module operations are available in this turn, so do not invent tool calls. Plain prose is for direct conversational replies that need no computation.");
252    }
253    section.push_str(
254        r#"
255
256### `print` vs `submit`
257
258- `print <expr>` — inspect a value and keep going; output appears on the next step. Print the part you need to decide the next step: a whole value when it is small and all of it is useful (e.g. state you consult each turn), otherwise selected fields, samples, or slices. Avoid dumping a large value when only part of it is relevant.
259- `submit <expr>` — final answer; ends the turn. Strings pass through as the reply. Non-string values render as pretty JSON for machine consumers; for user-facing turns, follow the current final-answer format guidance.
260
261Never `submit` a raw tool-result dump. If you need to look at something, `print` it, then `submit` a summary on a later step.
262
263### Turn shape
264
265**Exactly one ` ```lashlang ` fenced block per response.** Anything after the first block closes is dropped. Only ` ```lashlang ` is recognised — `rlm` and other labels are treated as plain prose.
266
267- Write small blocks. Each should do one focused step.
268- Keep prose around the block to one or two sentences of reasoning. Don't describe an action in prose instead of executing it.
269- After each result, decide: another block (more work), or finish.
270"#,
271    );
272    if has_operations {
273        section.push_str(
274            r#"
275Example — inspect with an available operation, then submit:
276
277````
278Checking the available data.
279
280```lashlang
281result = await web.search({ query: "value" })?
282print { summary: slice(to_string(result), 0, 200) }
283```
284````
285
286…then on the next turn, once you've seen what you need:
287
288````
289```lashlang
290submit "The bound version is 0.2.61."
291```
292````
293"#,
294        );
295    } else {
296        section.push_str(
297            r#"
298Example — compute and submit:
299
300````
301```lashlang
302items = ["alpha", "beta"]
303submit format("Found {} items; first item: {}.", len(items), items[0])
304```
305````
306"#,
307        );
308    }
309    section
310}
311
312fn render_language_section(
313    images: bool,
314    has_operations: bool,
315    abilities: &lashlang::LashlangAbilities,
316    language_features: &lashlang::LashlangLanguageFeatures,
317) -> String {
318    let mut bullets = Vec::new();
319    if images {
320        bullets.push("- Values: null, booleans, numbers, strings, lists, records, and immutable `Image` handles. Literals: `[a, b]`, `{ a: 1, b: 2 }`.".to_string());
321        bullets.push("- Images: image-producing tools may return an `Image` value. Read metadata with `.id`, `.label`, `.size`, `.width`, `.height`; fields are read-only. `print(image)` or `print` on a list/record containing images sends both descriptor text and the actual image attachment to the next model call. `submit(image)`, `to_string(image)`, and JSON-like serialization emit only `{ \"type\": \"image\", \"id\": ..., \"label\": ..., \"size\": ..., \"width\": ..., \"height\": ... }`. `len(image)` is invalid; use `.size`.".to_string());
322    } else {
323        bullets.push("- Values: null, booleans, numbers, strings, lists, and records. Literals: `[a, b]`, `{ a: 1, b: 2 }`.".to_string());
324    }
325    bullets.push("- Strings: `\"...\"` supports `\\n`, `\\r`, `\\t`, `\\\"`, and `\\\\`; `\"\"\"...\"\"\"` is multiline with the same escapes; `r\"\"\"...\"\"\"` and `r'''...'''` are raw multiline strings and preserve content exactly. Use raw multiline strings for JSON, Markdown, and other payloads with braces, backslashes, quotes, heredocs, or `@@` hunk markers.".to_string());
326    bullets.push("- Assign with `name = expr`. Variables persist across fenced blocks within the turn. You can also update mutable collection paths rooted at a variable: `record.field = value`, `record[key] = value`, `list[i] = value`, and nested forms like `state.groups[g].count = count + 1`. Record field/index assignment inserts or replaces fields; list assignment replaces an existing integer index only. Record indexing reads dynamic string-coerced keys and returns `null` when missing, so histogram code can use `counts[g] = counts[g] + 1`.".to_string());
327    if has_operations {
328        bullets.push("- Module operations: call host capabilities through documented lowercase module paths, e.g. `await agents.spawn({ task: task })?`, `await web.search({ query: q })?`, or `await gmail.work.send({ to: to, body: body })?`. Host operations are awaited effects; pure UpperCamel value constructors shown in the Host Surface are ordinary expressions and must not be awaited. Bare calls are builtins only, not tools. `?` aborts the block with sanitized operation metadata if the operation fails.".to_string());
329    }
330    if abilities.sleep {
331        bullets.push("- Sleep: pause foreground code or process code with `sleep for \"5s\"` or `sleep until deadline`. Durations accept milliseconds, `ms`, `s`, `m`, or `h`; deadlines accept RFC3339 text or Unix epoch milliseconds.".to_string());
332    }
333    if abilities.processes {
334        let mut forms = vec![
335            "`yield value`",
336            "`wake value`",
337            "`finish value`",
338            "`fail value`",
339        ];
340        if abilities.process_signals {
341            forms.push("`payload = wait signal`");
342        }
343        let trigger_process_note = if abilities.triggers {
344            " matching host-event occurrences can also create process runs through registered triggers."
345        } else {
346            ""
347        };
348        bullets.push(format!(
349            "- Background processes: `process name(param: TYPE) {{ ... }}` declares a reusable process definition. `handle = start name(param: value)` creates one process run from that definition and returns its run handle;{trigger_process_note} For account-parametric work, pass typed module authorities explicitly, e.g. `process notify(mail: Gmail) {{ await mail.send({{ body: body }})? finish true }}` and `start notify(mail: gmail.work)`. For one-off concrete automations, a process body may reference concrete host paths such as `agents`, `web`, or `gmail.work` directly; params and locals shadow those captures. Inside a process use {}. `wake value` emits a `process.wake` event that notifies the agent/session with `value`; use it when process progress, trigger output, or other background work should re-enter the model as context. `finish value` completes the run and stores `value` as the process success value. `fail value` completes it as failed; falling off the end is `finish null`. `submit` and `print` are foreground-only and invalid inside processes. Parallelism comes from starting all independent process handles before waiting for any of them; join a list or record of handles with `results = await handles`. `await handle` waits and returns a result wrapper like `{{ ok: true, value: ... }}`; when you need fields from the `finish` value, use `result = (await handle)?` and then read `result.field`. Cancel a live run with `cancel handle` (best-effort). If the Host Surface includes `processes.list`, use `await processes.list({{}})?` for running runs, `await processes.list({{ definition: name }})?` for runs of a definition, and `await processes.list({{ status: \"any\" }})?` for visible run history.",
350            join_words(&forms),
351        ));
352        if abilities.process_signals {
353            bullets.push("- Signalling processes: `signal run handle with payload` sends a `process.signal` to a running process and may be used from the foreground turn as well as inside a process body, like `await handle` and `cancel handle`. The receiving side, `payload = wait signal`, parks a process until a signal arrives and is only valid inside a process body.".to_string());
354        }
355    }
356    if language_features.label_annotations {
357        bullets.push("- Execution labels: use `@label(title: \"Label\")` or `@label(title: \"Label\", description: \"Details\")` to name important Lashlang phases and graph steps. At top level, label meaningful setup, resource calls, submissions, branches, loops, or process declarations. Inside a `process` body, label durable steps such as awaited module calls, `start`, `sleep`, `wait signal`, `signal run`, `wake`, `yield`, `finish`, `fail`, `if`, loops, and setup statements that explain the process. Titles/descriptions must be string literals; do not use variables, interpolation, icons, colors, layout hints, or extra keys.".to_string());
358    }
359    if abilities.triggers && abilities.processes {
360        let trigger_register = lashlang::TriggerHostOperation::Register.host_operation();
361        let trigger_list = lashlang::TriggerHostOperation::List.host_operation();
362        let trigger_cancel = lashlang::TriggerHostOperation::Cancel.host_operation();
363        bullets.push(format!("- Trigger registry: a trigger registration connects a typed source value to a process definition plus explicit inputs. Register with `handle = await {trigger_register}({{ source: source, target: daily_digest, inputs: {{ tick: trigger.event }}, name: \"daily_digest\" }})?`. Constructors build source values; the host/plugin that owns the source lists stored subscriptions by source type/key and emits host-event occurrences when source-specific events happen. `target` is a process definition value. `inputs` is required and maps every process param exactly once. `trigger.event` is the direct whole-event value inside `inputs`; fixed inputs can pass concrete authorities like `gmail.work` or `agents` for account-parametric processes. Use `await {trigger_list}({{}})?` to discover visible registrations, or filter with `{{ target: daily_digest }}`, `{{ name: \"daily_digest\" }}`, `{{ source_type: \"cron.Schedule\" }}`, and `{{ enabled: true }}`. Use `await {trigger_cancel}({{ handle: handle }})?` to disable future occurrence delivery for that registration."));
364    }
365    if has_operations {
366        let scheduling = if abilities.processes {
367            "- Operation scheduling: consecutive `await module.op(...)` statements run one at a time. For independent slow effects, wrap one branch in a named `process`, `start` every branch first, then join the handles with `await handles` or `await { key: handle }`. Call operations directly only for single effects, cheap probes, or steps that depend on earlier results."
368        } else {
369            "- Operation scheduling: consecutive `await module.op(...)` statements run one at a time. Direct operation calls are serial when process handles are unavailable; do not describe sequential awaits as parallel."
370        };
371        bullets.push(scheduling.to_string());
372    }
373    bullets.push("- Control flow: statement `if`/`for`/`while`; `break` exits the nearest loop; `continue` skips to the nearest loop's next iteration; expression ternary `cond ? yes : no` (there is no expression-form `if`); boolean negation via `!cond` or `not cond`. Prefer bounded `while` loops where possible and bounded `for` loops over ranges/lists for fill or retry logic. `submit` is different from `break`: it ends the whole program/turn.".to_string());
374    bullets.push("- Bare expressions are valid statements in normal blocks.".to_string());
375    bullets.push("- The **Bound Variables** section lists values already in scope, plus `history` — use them directly in lashlang, don't recreate them. Small values show inline; large values show only type and size. Other available read-only values may be listed separately without value previews. `print` a variable (or the part you need) to see contents it only summarizes.".to_string());
376    format!("### Language\n\n{}", bullets.join("\n"))
377}
378
379fn render_common_patterns(has_operations: bool) -> String {
380    if has_operations {
381        return LASHLANG_COMMON_PATTERNS_SECTION.to_string();
382    }
383    r#"### Common patterns
384
385Build collections with explicit loops, not comprehensions:
386
387```lashlang
388items = []
389for key in ["a", "b"] {
390  items = push(items, { key: key, size: len(key) })
391}
392submit format("Built {} items.", len(items))
393```
394
395Print what you need to see. Pull in the parts relevant to your next step — a whole value when it is small and all useful, otherwise keys, lengths, selected fields, or slices of large ones:
396
397```lashlang
398text = to_string(input)
399print { chars: len(text), head: slice(text, 0, 1200) }
400```
401
402For multi-step work, inspect intermediate results before submitting success. Reaching `submit` ends the turn even mid-block:
403
404```lashlang
405first = trim(input.question)
406if empty(first) {
407  print { problem: "missing question" }
408} else {
409  submit format("Question: {}", first)
410}
411```"#
412        .to_string()
413}
414
415fn render_decomposition_section(has_operations: bool, processes: bool) -> String {
416    let mut section = String::from(
417        "### Working with context\n\nYour turn's REPL trace is your working memory — keep it decision-sized and current. Two kinds of values need different handling: large transient artifacts (files, search results, long pages, raw tool dumps) stay in variables — `print` only the fields or slices you need, not the whole thing; small durable state you consult each turn (a map, plan, or checklist) should stay visible — small values show inline under **Bound Variables**, and you can `print` them whole when you need to reason over all of them.\n\nChoose the lightest mechanism that preserves progress:\n\n- Current variables already hold what you need → reason inline in lashlang.",
418    );
419    if has_operations {
420        if processes {
421            section.push_str("\n- Several independent slow operations are needed -> define one small branch `process`, start every branch handle first, then join the handles with `await handles`.");
422        } else {
423            section.push_str("\n- Several operations are needed -> call each module operation and keep the values in variables; these awaits are serial without process handles.");
424        }
425    }
426    section.push_str("\n- The trace is bloated, stale, or failed attempts dominate -> use an available continuation tool to switch to a fresh AgentFrame with concrete state.");
427    if has_operations && processes {
428        section.push_str("\n- Anything tool-specific (parameters, return shapes, lifecycle) lives under **Showcased Tools** — don't infer a tool exists from these generic examples.\n\nExample parallel fan-out around an available operation (start first, then await handles; use `?` to unwrap each finished value):\n\n```lashlang\nprocess lookup(query: str) {\n  result = await web.search({ query: query })?\n  finish result\n}\n\nhandles = {\n  one: start lookup(query: \"one\"),\n  two: start lookup(query: \"two\")\n}\nresults = await handles\none = results.one?\ntwo = results.two?\nsubmit format(\"First result: {}\\n\\nSecond result: {}\", slice(to_string(one), 0, 800), slice(to_string(two), 0, 800))\n```");
429    } else if has_operations {
430        section.push_str("\n- Anything tool-specific (parameters, return shapes, lifecycle) lives under **Showcased Tools** — don't infer a tool exists from these generic examples.");
431    } else {
432        section.push_str("\n- No module operations are available in this turn — don't infer one exists from generic lashlang syntax.");
433    }
434    section
435}
436
437fn join_words(words: &[&str]) -> String {
438    match words {
439        [] => String::new(),
440        [one] => (*one).to_string(),
441        [one, two] => format!("{one} or {two}"),
442        _ => {
443            let mut out = words[..words.len() - 1].join(", ");
444            out.push_str(", or ");
445            out.push_str(words[words.len() - 1]);
446            out
447        }
448    }
449}