From 20003dbe557f8960ed1ff0e79292f0ed684a9166 Mon Sep 17 00:00:00 2001 From: alpibrupa Date: Fri, 17 Jul 2026 15:54:17 +0200 Subject: [PATCH 1/2] Stop telling the model list.zip exists, then linting it for using it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Benchmark findings from local runs, rescued from an uncommitted working copy and rebased onto main. Two of them are corrections, not additions: 1. The prompt's stdlib index advertised `zip` as a std.list function. It does not exist — `list.zip(...)` is `unknown_field` on 0.10.6. We were the source of the hallucination the linter then flagged. The row also omitted six functions that DO exist, two of which the prompt's own examples use (`list.cons`, `list.range`), and one the new zip hint recommends (`list.enumerate`) — so a model told "use enumerate" had no confirmation it was real. Every one of the fourteen functions the row now claims was probed against 0.10.6 individually. 2. The `&&` linter hint said "use nested if: `if a { b } else { false }`". Lex has `and`. The hint now says `a and b`, and `||` gets the same treatment. The old advice was not just verbose, it was wrong about the language. Plus targeted fix: hints for the two APIs models reach for most and that do not exist — `list.zip` (→ enumerate, or compare via str.join) and `str.to_str` (→ int.to_str). Both verified absent on 0.10.6 before writing the hint. Verified: CI's own loop over all 76 tracked src/*.lex exits 0; the examples check passes. Co-Authored-By: Claude Opus 4.8 --- README.md | 85 +++++++++++++++++++++++++++++++++++++++- src/prompts/lex_lang.lex | 4 +- src/tools/linter.lex | 14 +++++-- src/tui/main.lex | 14 ++++--- 4 files changed, 107 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index eb5a60d..f6daf7b 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,8 @@ lex-code --plan --ollama "how should we structure the session module?" | `--openai` | OpenAI | gpt-5.5 | `OPENAI_API_KEY` | | `--google` | Google | gemini-3.5-flash | `GOOGLE_API_KEY` | | `--mistral` | Mistral | mistral-large-latest | `MISTRAL_API_KEY` | -| `--ollama` | Ollama (local) | codellama | none | +| `--litellm` | LiteLLM proxy | `$LITELLM_MODEL` | none (proxy handles keys) | +| `--ollama` | Ollama (local, native API) | `$OLLAMA_MODEL` | none | | `--vllm` | vLLM (local/remote) | `$VLLM_MODEL` | none | ### Ollama @@ -116,6 +117,88 @@ VLLM_MODEL=deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct \ `VLLM_MODEL` defaults to `mistralai/Mistral-7B-Instruct-v0.3`. `VLLM_BASE_URL` defaults to `http://localhost:8000/v1/chat/completions`. +### LiteLLM (local models via proxy) + +[LiteLLM](https://github.com/BerriAI/litellm) is the recommended path for running local models. It provides an OpenAI-compatible endpoint over any backend (Ollama, vLLM, MLX, …), which gives cleaner tool calling than the native Ollama wire format. + +```sh +# start the LiteLLM proxy (config at project root) +litellm --config litellm_config.yaml --port 4000 + +# run lex-code against qwen3-coder:30b (recommended local model) +LITELLM_MODEL=qwen3-coder:30b \ + lex run --allow-effects env,io,net,llm,proc,sql,fs_write,time \ + src/tui/main.lex main + +# one-shot via the --litellm flag +LITELLM_MODEL=qwen3-coder:30b \ + lex run --allow-effects env,io,net,llm,proc,sql,fs_write,time \ + src/tui/main.lex main -- --litellm "implement list.zip" + +# override the proxy URL (default: http://localhost:4000) +LITELLM_BASE_URL=http://gpu-box:4000 \ +LITELLM_MODEL=qwen3-coder:30b \ + lex run --allow-effects env,io,net,llm,proc,sql,fs_write,time \ + src/tui/main.lex main -- --litellm +``` + +`LITELLM_MODEL` is the model name as it appears in your `litellm_config.yaml` `model_name` field. +`LITELLM_BASE_URL` defaults to `http://localhost:4000`. + +#### Local model compatibility + +Tested on [lex-code fizzbuzz bootstrap](src/bootstrap/fizzbuzz_lex.lex) — task: write `fizzbuzz.lex` with `fn fizzbuzz(n :: Int) -> List[Str]` + 4 unit tests, `lex check` clean, `run_all` returns 0. + +| Model | VRAM | Steps | Result | Notes | +|-------|------|-------|--------|-------| +| `qwen3-coder:30b` (Q4_K_M) | 45 GB | ~14 LLM rounds | ✅ passes | Best local choice. Correct tool use, proper Lex idioms after linter feedback. | +| `gemma4:26b` (Q4) | 19 GB | — | ❌ fails | Thinking model: consumes 500–700 tokens on chain-of-thought before any output. Tool calls appear as embedded JSON in `content` instead of `tool_calls`. Generates Python instead of Lex under large context. | +| `gemma4:latest` (9 B) | 10 GB | — | not tested | Lighter variant; same thinking-model caveats apply. | + +**Reliable patterns with `qwen3-coder:30b`:** + +```sh +# Warm the model before a run (first call loads weights, subsequent calls are faster) +curl -s http://localhost:4000/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"model":"qwen3-coder:30b","messages":[{"role":"user","content":"hi"}],"max_tokens":10,"stream":false}' \ + > /dev/null + +LITELLM_MODEL=qwen3-coder:30b \ + lex run --allow-effects env,io,net,llm,proc,sql,fs_write,time \ + src/bootstrap/fizzbuzz_lex.lex main +# [fizzbuzz_lex] starting build via litellm +# [fizzbuzz_lex] done — steps: 71 +``` + +```sh +$ lex check fizzbuzz.lex && lex run fizzbuzz.lex run_all +ok +0 +``` + +**Step count explained:** `steps` counts all `d.Step` records emitted by the agent loop — `StepDelta` (per LLM token event), `StepToolExec`, `StepToolResult`, and `StepDone`. One LLM round + one tool call ≈ 5 step records. 71 steps ≈ 14 LLM rounds (`max_steps: 20` counts rounds, not records). + +**Avoiding the 0-delta stall:** If Ollama receives many large-context requests in rapid succession it can enter a state where it returns `{"done": false, "response": ""}`. The agent loop sees 0 deltas, emits a silent empty `StepDone`, and the run appears to complete in 1 step with no output. Fix: restart Ollama (`pkill -f "ollama serve" && open -a Ollama`) and avoid batching many large-context calls without pauses. + +#### Thinking models (gemma4, deepseek-r1) + +Models with a chain-of-thought "thinking" phase need two things to work through LiteLLM: + +1. **`max_tokens ≥ 2000`** — thinking tokens count against the budget before any visible output is produced. With `max_tokens: 256` the model exhausts its budget mid-thought and returns empty content. +2. **`merge_reasoning_content_in_choices: true`** in `litellm_config.yaml` — without this, LiteLLM drops the `content` field when `thinking` is present in the Ollama response. + +```yaml +# litellm_config.yaml +- model_name: gemma4:26b + litellm_params: + model: ollama/gemma4:26b + api_base: http://localhost:11434 + merge_reasoning_content_in_choices: true +``` + +Even with these fixes, thinking models tend to emit tool calls as embedded JSON in `content` (rather than in the `tool_calls` field) when given 10+ function schemas. The `openai.lex` adapter has a `content_tool_call` fallback parser, but the generated code quality degrades significantly under large context. Use `qwen3-coder:30b` for coding tasks. + ## Server Protocols ### A2A (Agent-to-Agent, JSON-RPC 2.0) diff --git a/src/prompts/lex_lang.lex b/src/prompts/lex_lang.lex index 64a3741..6a76121 100644 --- a/src/prompts/lex_lang.lex +++ b/src/prompts/lex_lang.lex @@ -1,4 +1,6 @@ fn reference() -> Str { - "## Lex Language — Core Reference\n\nLex is a typed-effect functional language. Files use `.lex`; the default toolchain is `lex`.\n\n### Syntax quick-start\n\n```lex\n# Function with explicit types (all signatures are explicit)\nfn add(x :: Int, y :: Int) -> Int\n examples {\n add(0, 0) => 0,\n add(2, 3) => 5,\n }\n{ x + y }\n\n# Effectful function — effects come BEFORE the return type\nfn save(path :: Str, data :: Str) -> [fs_write] Result[Unit, Str] {\n fs.write(path, data)\n}\n\n# Pure function — NO effect annotation at all\nfn double(n :: Int) -> Int { n * 2 }\n\n# Let binding — uses `:=`, not `=`\nlet result := add(1, 2)\n\n# Algebraic data type\ntype Shape = Circle(Float) | Rect { w :: Float, h :: Float }\n\n# Match (must be exhaustive)\nmatch shape {\n Circle(r) => 3.14159 * r * r,\n Rect { w, h } => w * h,\n}\n\n# Result / Option idioms\nmatch http.get(url) {\n Ok(resp) => resp.body,\n Err(e) => str.concat(\"error: \", e),\n}\n\n# Import\nimport \"std.list\" as list\nimport \"./util\" as util\n```\n\n### Common syntax pitfalls\n| What you might write | Correct Lex |\n|---|---|\n| `x: Int` | `x :: Int` |\n| `let x = e` | `let x := e` |\n| `-> T` with no effects | `-> T` (omit `[]` entirely for pure fns) |\n| `-> [] T` | invalid — write `-> T` |\n| Exceptions / throw | use `Result[T, E]` — no exceptions in Lex |\n| `else if cond { }` | `else { if cond { } }` — **Lex has NO `else if`**. Always nest: `else { if ... { } else { } }` |\n| `True` / `False` | `true` / `false` — booleans are lowercase (unlike Haskell) |\n| `list.concat(a, b, c)` | `list.concat(a, list.concat(b, c))` — concat takes exactly 2 args |\n| `//` comment | `#` comment — Lex uses `#`, not `//` or `/*` |\n| write to `src/file.lex` | write to `file.lex` — use plain filenames, not subdirectory paths |\n| `list.concat(...)` without import | add `import \"std.list\" as list` — every stdlib module must be explicitly imported |\n| `fn f(x) examples{...} match t {` | function body MUST be `{ }` wrapped: `fn f(x) examples{...} { match t { ... } }` |\n| `type T = \\| A \\| B` | NO leading pipe: `type T = A \\| B` — first variant has no `\\|` prefix |\n| `pair.0` / `pair.1` | tuples have no field access — destructure: `match pair { (a, b) => a }` |\n| `while` / `for` loops | Lex is purely functional — use recursion or `list.fold` |\n| `let x := 1` then `let x := 2` | `let` bindings are immutable — cannot reassign; use a recursive helper or fold |\n| `a && b` / `a \\|\\| b` | no `&&`/`\\|\\|` operators — use nested `if`: `if a { b } else { false }` |\n| `list.find(...)` | does not exist — use `list.filter` + `list.head`, or `list.fold` |\n| `fn (kv :: (A, B)) -> T` in a call | tuple type in lambda param causes parse error; match inside body instead: `fn (kv :: (A, B)) -> T { match kv { (a, b) => ... } }` |\n| `match xs { [] => ..., [h, ..t] => ... }` | list pattern matching is NOT supported — use `if list.is_empty(xs) { ... } else { let h := list.head(xs); let t := list.tail(xs); ... }` |\n| `fn helper(...)` inside a function body | local named functions are NOT allowed — extract every helper to the top level |\n| `list.join(parts, sep)` | does not exist — use `str.join(parts, sep)` from `std.str` |\n| `VBool(bool)` / `x :: bool` | type names are uppercase: `Bool`, `Int`, `Str`, `Float` — never `bool`, `int`, `str` |\n| `let a := x,` (comma after let) | `let` bindings inside a block need NO separator — just newlines. Commas go between match arms only. |\n\n**No `else if` — this is the single most common mistake.** Lex only has `if/else`. Chain conditions by nesting:\n```lex\n# WRONG — will not parse:\nif a { x } else if b { y } else { z }\n\n# CORRECT — always nest else { if ... }:\nif a { x } else { if b { y } else { z } }\n```\n\n### Effect discipline (most important rule)\n- Declare the **narrowest** effect set that matches the body.\n- Pure functions: no effect annotation — `fn f(x :: T) -> T`.\n- Never add `[io]` \"just in case\"; the checker will reject unused effects.\n- Common effects: `io`, `net`, `llm`, `sql`, `fs_read`, `fs_write`, `env`, `proc`, `time`, `concurrent`.\n- Fine-grained path scopes: `[fs_write(\"/tmp/out/\")]`, `[net(\"api.example.com\")]`.\n\n### examples {} blocks\nPure functions SHOULD have an `examples {}` block — they're folded into the SigId and become free regression tests:\n```lex\nfn clamp(n :: Int, lo :: Int, hi :: Int) -> Int\n examples {\n clamp(5, 0, 10) => 5,\n clamp(-1, 0, 10) => 0,\n clamp(99, 0, 10) => 10,\n }\n{ if n < lo { lo } else { if n > hi { hi } else { n } } }\n```\n\n### Stdlib index\n| Module | Import | Key fns | Effect |\n|---|---|---|---|\n| list | `std.list` | map, filter, fold, head, tail, concat, len, zip, reverse | pure |\n| str | `std.str` | concat, split, trim, join, contains, replace, is_empty | pure |\n| io | `std.io` | print, readline, argv, read, write | [io] |\n| http | `std.http` | get, post, send, stream_lines | [net] |\n| sql | `std.sql` | open, query, exec, close | [sql] |\n| fs | `std.fs` | read, write, exists, list_dir, walk | [fs_read/fs_write] |\n| env | `std.env` | get, set | [env] |\n| conc | `std.conc` | spawn, ask, tell | [concurrent] |\n| json | `std.json` | parse, stringify | pure |\n| regex | `std.regex` | match, split, replace | pure |\n| int | `std.int` | to_str, parse, abs, min, max | pure |\n| float | `std.float` | to_str, parse, floor, ceil, round | pure |\n\n### Iteration loop\n1. Write or edit the `.lex` file — the `write` tool auto-runs `lex check` and returns an error if it fails.\n2. **If `write` returns an error, fix the code and call `write` again immediately.** Keep iterating until write succeeds. Never stop after a write error.\n3. Read the structured error (`rule_tag` + `rule_explanation`) to understand what to fix.\n4. `lex_run fn_name` — run a specific function.\n5. `lex_test` — run the test suite.\n\n### List processing idioms\nLex has no list pattern matching syntax. Use these patterns instead:\n```lex\n# Recursive list processing (head/tail, NOT match [h,...t])\nfn sum(xs :: List[Int]) -> Int {\n if list.is_empty(xs) { 0 }\n else {\n let h := match list.head(xs) { Some(v) => v, None => 0 }\n let t := list.tail(xs)\n h + sum(t)\n }\n}\n\n# Accumulate with fold (preferred over manual recursion)\nfn sum_fold(xs :: List[Int]) -> Int {\n list.fold(xs, 0, fn (acc :: Int, x :: Int) -> Int { acc + x })\n}\n\n# Build a list with cons + reverse\nfn double_all(xs :: List[Int]) -> List[Int] {\n list.reverse(list.fold(xs, [], fn (acc :: List[Int], x :: Int) -> List[Int] {\n list.cons(x * 2, acc)\n }))\n}\n```\n\n### Searching a list of tuples (common pattern)\nTo find a key in `List[(Str, Int)]`, use `list.fold` — there is no `list.find`:\n```lex\nfn lookup(env :: List[(Str, Int)], name :: Str) -> Option[Int] {\n list.fold(env, None, fn (acc :: Option[Int], kv :: (Str, Int)) -> Option[Int] {\n match acc {\n Some(v) => Some(v),\n None => match kv { (k, v) => if k == name { Some(v) } else { None } },\n }\n })\n}\n```\nNote: the lambda `fn (acc :: Option[Int], kv :: (Str, Int)) -> Option[Int]` is valid — tuple types in lambda params ARE allowed, just not when the lambda itself is directly inside a function call that would make the `)` ambiguous. When in doubt, assign the lambda to a `let` binding first.\n\n### Anti-patterns to avoid\n- `_ =>` in match unless it's a genuine catch-all.\n- `while`/`for` loops — Lex has none; use tail recursion or `list.fold`.\n- Reassigning `let` bindings — they are immutable; restructure with a recursive function.\n- String-concat SQL — use `sql.query(db, q, [params])`.\n- Rolling your own crypto — use `std.crypto`.\n- Wide `[net, io, fs_read, fs_write]` when only one is used.\n- Renaming params after publish (changes the SigId).\n- Adding `[io]` to a pure function (the checker will reject it).\n\nFor the full reference: call `load_guidelines` tool or run `lex agent-guidelines`." + "## Lex Language — Core Reference\n\nLex is a typed-effect functional language. Files use `.lex`; the default toolchain is `lex`.\n\n### Syntax quick-start\n\n```lex\n# Function with explicit types (all signatures are explicit)\nfn add(x :: Int, y :: Int) -> Int\n examples {\n add(0, 0) => 0,\n add(2, 3) => 5,\n }\n{ x + y }\n\n# Effectful function — effects come BEFORE the return type\nfn save(path :: Str, data :: Str) -> [fs_write] Result[Unit, Str] {\n fs.write(path, data)\n}\n\n# Pure function — NO effect annotation at all\nfn double(n :: Int) -> Int { n * 2 }\n\n# Let binding — uses `:=`, not `=`\nlet result := add(1, 2)\n\n# Algebraic data type\ntype Shape = Circle(Float) | Rect { w :: Float, h :: Float }\n\n# Match (must be exhaustive)\nmatch shape {\n Circle(r) => 3.14159 * r * r,\n Rect { w, h } => w * h,\n}\n\n# Result / Option idioms\nmatch http.get(url) {\n Ok(resp) => resp.body,\n Err(e) => str.concat(\"error: \", e),\n}\n\n# Import\nimport \"std.list\" as list\nimport \"./util\" as util\n```\n\n### Common syntax pitfalls\n| What you might write | Correct Lex |\n|---|---|\n| `x: Int` | `x :: Int` |\n| `let x = e` | `let x := e` |\n| `-> T` with no effects | `-> T` (omit `[]` entirely for pure fns) |\n| `-> [] T` | invalid — write `-> T` |\n| Exceptions / throw | use `Result[T, E]` — no exceptions in Lex |\n| `else if cond { }` | `else { if cond { } }` — **Lex has NO `else if`**. Always nest: `else { if ... { } else { } }` |\n| `True` / `False` | `true` / `false` — booleans are lowercase (unlike Haskell) |\n| `list.concat(a, b, c)` | `list.concat(a, list.concat(b, c))` — concat takes exactly 2 args |\n| `//` comment | `#` comment — Lex uses `#`, not `//` or `/*` |\n| write to `src/file.lex` | write to `file.lex` — use plain filenames, not subdirectory paths |\n| `list.concat(...)` without import | add `import \"std.list\" as list` — every stdlib module must be explicitly imported |\n| `fn f(x) examples{...} match t {` | function body MUST be `{ }` wrapped: `fn f(x) examples{...} { match t { ... } }` |\n| `type T = \\| A \\| B` | NO leading pipe: `type T = A \\| B` — first variant has no `\\|` prefix |\n| `pair.0` / `pair.1` | tuples have no field access — destructure: `match pair { (a, b) => a }` |\n| `while` / `for` loops | Lex is purely functional — use recursion or `list.fold` |\n| `let x := 1` then `let x := 2` | `let` bindings are immutable — cannot reassign; use a recursive helper or fold |\n| `a && b` / `a \\|\\| b` | use `a and b` / `a or b` — Lex uses keywords, not `&&`/`\\|\\|` |\n| `list.find(...)` | does not exist — use `list.filter` + `list.head`, or `list.fold` | +| `list.zip(a, b)` | does not exist — use `list.enumerate(a)` to get `List[(Int, T)]`, or compare as strings: `str.join(a, \",\") == str.join(b, \",\")` | +| `str.to_str(n)` | does not exist on `str` — convert Int to Str with `int.to_str(n)` from `std.int` |\n| `fn (kv :: (A, B)) -> T` in a call | tuple type in lambda param causes parse error; match inside body instead: `fn (kv :: (A, B)) -> T { match kv { (a, b) => ... } }` |\n| `match xs { [] => ..., [h, ..t] => ... }` | list pattern matching is NOT supported — use `if list.is_empty(xs) { ... } else { let h := list.head(xs); let t := list.tail(xs); ... }` |\n| `fn helper(...)` inside a function body | local named functions are NOT allowed — extract every helper to the top level |\n| `list.join(parts, sep)` | does not exist — use `str.join(parts, sep)` from `std.str` |\n| `VBool(bool)` / `x :: bool` | type names are uppercase: `Bool`, `Int`, `Str`, `Float` — never `bool`, `int`, `str` |\n| `let a := x,` (comma after let) | `let` bindings inside a block need NO separator — just newlines. Commas go between match arms only. |\n\n**No `else if` — this is the single most common mistake.** Lex only has `if/else`. Chain conditions by nesting:\n```lex\n# WRONG — will not parse:\nif a { x } else if b { y } else { z }\n\n# CORRECT — always nest else { if ... }:\nif a { x } else { if b { y } else { z } }\n```\n\n### Effect discipline (most important rule)\n- Declare the **narrowest** effect set that matches the body.\n- Pure functions: no effect annotation — `fn f(x :: T) -> T`.\n- Never add `[io]` \"just in case\"; the checker will reject unused effects.\n- Common effects: `io`, `net`, `llm`, `sql`, `fs_read`, `fs_write`, `env`, `proc`, `time`, `concurrent`.\n- Fine-grained path scopes: `[fs_write(\"/tmp/out/\")]`, `[net(\"api.example.com\")]`.\n\n### examples {} blocks\nPure functions SHOULD have an `examples {}` block — they're folded into the SigId and become free regression tests:\n```lex\nfn clamp(n :: Int, lo :: Int, hi :: Int) -> Int\n examples {\n clamp(5, 0, 10) => 5,\n clamp(-1, 0, 10) => 0,\n clamp(99, 0, 10) => 10,\n }\n{ if n < lo { lo } else { if n > hi { hi } else { n } } }\n```\n\n### Stdlib index\n| Module | Import | Key fns | Effect |\n|---|---|---|---|\n| list | `std.list` | map, filter, fold, head, tail, concat, cons, len, is_empty, enumerate, range, reverse, sort_by, par_map | pure |\n| str | `std.str` | concat, split, trim, join, contains, replace, is_empty | pure |\n| io | `std.io` | print, readline, argv, read, write | [io] |\n| http | `std.http` | get, post, send, stream_lines | [net] |\n| sql | `std.sql` | open, query, exec, close | [sql] |\n| fs | `std.fs` | read, write, exists, list_dir, walk | [fs_read/fs_write] |\n| env | `std.env` | get, set | [env] |\n| conc | `std.conc` | spawn, ask, tell | [concurrent] |\n| json | `std.json` | parse, stringify | pure |\n| regex | `std.regex` | match, split, replace | pure |\n| int | `std.int` | to_str, parse, abs, min, max | pure |\n| float | `std.float` | to_str, parse, floor, ceil, round | pure |\n\n### Iteration loop\n1. Write or edit the `.lex` file — the `write` tool auto-runs `lex check` and returns an error if it fails.\n2. **If `write` returns an error, fix the code and call `write` again immediately.** Keep iterating until write succeeds. Never stop after a write error.\n3. Read the structured error (`rule_tag` + `rule_explanation`) to understand what to fix.\n4. `lex_run fn_name` — run a specific function.\n5. `lex_test` — run the test suite.\n\n### List processing idioms\nLex has no list pattern matching syntax. Use these patterns instead:\n```lex\n# Recursive list processing (head/tail, NOT match [h,...t])\nfn sum(xs :: List[Int]) -> Int {\n if list.is_empty(xs) { 0 }\n else {\n let h := match list.head(xs) { Some(v) => v, None => 0 }\n let t := list.tail(xs)\n h + sum(t)\n }\n}\n\n# Accumulate with fold (preferred over manual recursion)\nfn sum_fold(xs :: List[Int]) -> Int {\n list.fold(xs, 0, fn (acc :: Int, x :: Int) -> Int { acc + x })\n}\n\n# Build a list with map (preferred — correct order, no reverse needed)\nfn double_all(xs :: List[Int]) -> List[Int] {\n list.map(xs, fn (x :: Int) -> Int { x * 2 })\n}\n\n# Build a list with range + map (generate n items in order)\nfn make_labels(n :: Int) -> List[Str] {\n list.map(list.range(1, n + 1), fn (i :: Int) -> Str { str.concat(\"item-\", int.to_str(i)) })\n}\n\n# CAUTION: fold + cons builds the list in REVERSE order.\n# Always wrap with list.reverse if you need the original order.\nfn double_all_fold(xs :: List[Int]) -> List[Int] {\n list.reverse(list.fold(xs, [], fn (acc :: List[Int], x :: Int) -> List[Int] {\n list.cons(x * 2, acc)\n }))\n}\n```\n\n### Searching a list of tuples (common pattern)\nTo find a key in `List[(Str, Int)]`, use `list.fold` — there is no `list.find`:\n```lex\nfn lookup(env :: List[(Str, Int)], name :: Str) -> Option[Int] {\n list.fold(env, None, fn (acc :: Option[Int], kv :: (Str, Int)) -> Option[Int] {\n match acc {\n Some(v) => Some(v),\n None => match kv { (k, v) => if k == name { Some(v) } else { None } },\n }\n })\n}\n```\nNote: the lambda `fn (acc :: Option[Int], kv :: (Str, Int)) -> Option[Int]` is valid — tuple types in lambda params ARE allowed, just not when the lambda itself is directly inside a function call that would make the `)` ambiguous. When in doubt, assign the lambda to a `let` binding first.\n\n### Anti-patterns to avoid\n- `_ =>` in match unless it's a genuine catch-all.\n- `while`/`for` loops — Lex has none; use tail recursion or `list.fold`.\n- Reassigning `let` bindings — they are immutable; restructure with a recursive function.\n- String-concat SQL — use `sql.query(db, q, [params])`.\n- Rolling your own crypto — use `std.crypto`.\n- Wide `[net, io, fs_read, fs_write]` when only one is used.\n- Renaming params after publish (changes the SigId).\n- Adding `[io]` to a pure function (the checker will reject it).\n\nFor the full reference: call `load_guidelines` tool or run `lex agent-guidelines`." } diff --git a/src/tools/linter.lex b/src/tools/linter.lex index fab8c36..88219c2 100644 --- a/src/tools/linter.lex +++ b/src/tools/linter.lex @@ -72,7 +72,15 @@ fn translate_lex_error(raw :: Str) -> Str { if field == "all" { "fix: `list.all` does not exist. Use `list.fold(xs, true, fn (acc :: Bool, x :: T) -> Bool { if acc { pred(x) } else { false } })`." } else { - str.concat("unknown field '", str.concat(field, "' — check the type definition or stdlib docs.")) + if field == "zip" { + "fix: `list.zip` does not exist. Use `list.enumerate(a)` to get `List[(Int, T)]` and index into `b` manually, or build tuples with fold. To compare two equal-length lists as strings: `str.join(a, \",\") == str.join(b, \",\")`." + } else { + if field == "to_str" { + "fix: `str.to_str` does not exist. Use `int.to_str(n)` from `std.int` to convert an Int to Str." + } else { + str.concat("unknown field '", str.concat(field, "' — check the type definition or stdlib docs.")) + } + } } } } @@ -125,10 +133,10 @@ fn translate_lex_error(raw :: Str) -> Str { Err(_) => { let s := str.trim(raw) if str.contains(s, "unrecognized token `&`") { - "parse error: `&&` is not a Lex operator. Use nested `if`: `if a { b } else { false }` for AND, `if a { true } else { b }` for OR." + "parse error: `&&` is not valid Lex. Use `a and b` (Lex keyword). Example: `acc and (x == y)`." } else { if str.contains(s, "unrecognized token `|`") { - "parse error: `||` is not a Lex operator. Use nested `if`: `if a { true } else { b }` for OR." + "parse error: `||` is not valid Lex. Use `a or b` (Lex keyword). Example: `a or b`." } else { if str.contains(s, "expected LBrace before block, got If") { "parse error: `else if` is not valid in Lex. Write `else { if cond { ... } else { ... } }` instead." diff --git a/src/tui/main.lex b/src/tui/main.lex index e043542..f37a354 100644 --- a/src/tui/main.lex +++ b/src/tui/main.lex @@ -153,13 +153,17 @@ fn select_provider_tag(argv :: List[Str]) -> Str { if has_flag(argv, "--vertex") { "vertex" } else { - if has_flag(argv, "--ollama") { - "ollama" + if has_flag(argv, "--litellm") { + "litellm" } else { - if has_flag(argv, "--vllm") { - "vllm" + if has_flag(argv, "--ollama") { + "ollama" } else { - "anthropic" + if has_flag(argv, "--vllm") { + "vllm" + } else { + "anthropic" + } } } } From 109d1071834cd95ed749423d7bb3509296b1a6bb Mon Sep 17 00:00:00 2001 From: alpibrupa Date: Fri, 17 Jul 2026 15:56:59 +0200 Subject: [PATCH 2/2] lex fmt src/prompts/lex_lang.lex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI's fmt --check step caught this — I had run the type-check loop locally but missed the fmt step when reading the workflow. The prompt file has no inner comments, so lex-lang#716 costs nothing here. Co-Authored-By: Claude Opus 4.8 --- src/prompts/lex_lang.lex | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/prompts/lex_lang.lex b/src/prompts/lex_lang.lex index 6a76121..c019b0e 100644 --- a/src/prompts/lex_lang.lex +++ b/src/prompts/lex_lang.lex @@ -1,6 +1,4 @@ fn reference() -> Str { - "## Lex Language — Core Reference\n\nLex is a typed-effect functional language. Files use `.lex`; the default toolchain is `lex`.\n\n### Syntax quick-start\n\n```lex\n# Function with explicit types (all signatures are explicit)\nfn add(x :: Int, y :: Int) -> Int\n examples {\n add(0, 0) => 0,\n add(2, 3) => 5,\n }\n{ x + y }\n\n# Effectful function — effects come BEFORE the return type\nfn save(path :: Str, data :: Str) -> [fs_write] Result[Unit, Str] {\n fs.write(path, data)\n}\n\n# Pure function — NO effect annotation at all\nfn double(n :: Int) -> Int { n * 2 }\n\n# Let binding — uses `:=`, not `=`\nlet result := add(1, 2)\n\n# Algebraic data type\ntype Shape = Circle(Float) | Rect { w :: Float, h :: Float }\n\n# Match (must be exhaustive)\nmatch shape {\n Circle(r) => 3.14159 * r * r,\n Rect { w, h } => w * h,\n}\n\n# Result / Option idioms\nmatch http.get(url) {\n Ok(resp) => resp.body,\n Err(e) => str.concat(\"error: \", e),\n}\n\n# Import\nimport \"std.list\" as list\nimport \"./util\" as util\n```\n\n### Common syntax pitfalls\n| What you might write | Correct Lex |\n|---|---|\n| `x: Int` | `x :: Int` |\n| `let x = e` | `let x := e` |\n| `-> T` with no effects | `-> T` (omit `[]` entirely for pure fns) |\n| `-> [] T` | invalid — write `-> T` |\n| Exceptions / throw | use `Result[T, E]` — no exceptions in Lex |\n| `else if cond { }` | `else { if cond { } }` — **Lex has NO `else if`**. Always nest: `else { if ... { } else { } }` |\n| `True` / `False` | `true` / `false` — booleans are lowercase (unlike Haskell) |\n| `list.concat(a, b, c)` | `list.concat(a, list.concat(b, c))` — concat takes exactly 2 args |\n| `//` comment | `#` comment — Lex uses `#`, not `//` or `/*` |\n| write to `src/file.lex` | write to `file.lex` — use plain filenames, not subdirectory paths |\n| `list.concat(...)` without import | add `import \"std.list\" as list` — every stdlib module must be explicitly imported |\n| `fn f(x) examples{...} match t {` | function body MUST be `{ }` wrapped: `fn f(x) examples{...} { match t { ... } }` |\n| `type T = \\| A \\| B` | NO leading pipe: `type T = A \\| B` — first variant has no `\\|` prefix |\n| `pair.0` / `pair.1` | tuples have no field access — destructure: `match pair { (a, b) => a }` |\n| `while` / `for` loops | Lex is purely functional — use recursion or `list.fold` |\n| `let x := 1` then `let x := 2` | `let` bindings are immutable — cannot reassign; use a recursive helper or fold |\n| `a && b` / `a \\|\\| b` | use `a and b` / `a or b` — Lex uses keywords, not `&&`/`\\|\\|` |\n| `list.find(...)` | does not exist — use `list.filter` + `list.head`, or `list.fold` | -| `list.zip(a, b)` | does not exist — use `list.enumerate(a)` to get `List[(Int, T)]`, or compare as strings: `str.join(a, \",\") == str.join(b, \",\")` | -| `str.to_str(n)` | does not exist on `str` — convert Int to Str with `int.to_str(n)` from `std.int` |\n| `fn (kv :: (A, B)) -> T` in a call | tuple type in lambda param causes parse error; match inside body instead: `fn (kv :: (A, B)) -> T { match kv { (a, b) => ... } }` |\n| `match xs { [] => ..., [h, ..t] => ... }` | list pattern matching is NOT supported — use `if list.is_empty(xs) { ... } else { let h := list.head(xs); let t := list.tail(xs); ... }` |\n| `fn helper(...)` inside a function body | local named functions are NOT allowed — extract every helper to the top level |\n| `list.join(parts, sep)` | does not exist — use `str.join(parts, sep)` from `std.str` |\n| `VBool(bool)` / `x :: bool` | type names are uppercase: `Bool`, `Int`, `Str`, `Float` — never `bool`, `int`, `str` |\n| `let a := x,` (comma after let) | `let` bindings inside a block need NO separator — just newlines. Commas go between match arms only. |\n\n**No `else if` — this is the single most common mistake.** Lex only has `if/else`. Chain conditions by nesting:\n```lex\n# WRONG — will not parse:\nif a { x } else if b { y } else { z }\n\n# CORRECT — always nest else { if ... }:\nif a { x } else { if b { y } else { z } }\n```\n\n### Effect discipline (most important rule)\n- Declare the **narrowest** effect set that matches the body.\n- Pure functions: no effect annotation — `fn f(x :: T) -> T`.\n- Never add `[io]` \"just in case\"; the checker will reject unused effects.\n- Common effects: `io`, `net`, `llm`, `sql`, `fs_read`, `fs_write`, `env`, `proc`, `time`, `concurrent`.\n- Fine-grained path scopes: `[fs_write(\"/tmp/out/\")]`, `[net(\"api.example.com\")]`.\n\n### examples {} blocks\nPure functions SHOULD have an `examples {}` block — they're folded into the SigId and become free regression tests:\n```lex\nfn clamp(n :: Int, lo :: Int, hi :: Int) -> Int\n examples {\n clamp(5, 0, 10) => 5,\n clamp(-1, 0, 10) => 0,\n clamp(99, 0, 10) => 10,\n }\n{ if n < lo { lo } else { if n > hi { hi } else { n } } }\n```\n\n### Stdlib index\n| Module | Import | Key fns | Effect |\n|---|---|---|---|\n| list | `std.list` | map, filter, fold, head, tail, concat, cons, len, is_empty, enumerate, range, reverse, sort_by, par_map | pure |\n| str | `std.str` | concat, split, trim, join, contains, replace, is_empty | pure |\n| io | `std.io` | print, readline, argv, read, write | [io] |\n| http | `std.http` | get, post, send, stream_lines | [net] |\n| sql | `std.sql` | open, query, exec, close | [sql] |\n| fs | `std.fs` | read, write, exists, list_dir, walk | [fs_read/fs_write] |\n| env | `std.env` | get, set | [env] |\n| conc | `std.conc` | spawn, ask, tell | [concurrent] |\n| json | `std.json` | parse, stringify | pure |\n| regex | `std.regex` | match, split, replace | pure |\n| int | `std.int` | to_str, parse, abs, min, max | pure |\n| float | `std.float` | to_str, parse, floor, ceil, round | pure |\n\n### Iteration loop\n1. Write or edit the `.lex` file — the `write` tool auto-runs `lex check` and returns an error if it fails.\n2. **If `write` returns an error, fix the code and call `write` again immediately.** Keep iterating until write succeeds. Never stop after a write error.\n3. Read the structured error (`rule_tag` + `rule_explanation`) to understand what to fix.\n4. `lex_run fn_name` — run a specific function.\n5. `lex_test` — run the test suite.\n\n### List processing idioms\nLex has no list pattern matching syntax. Use these patterns instead:\n```lex\n# Recursive list processing (head/tail, NOT match [h,...t])\nfn sum(xs :: List[Int]) -> Int {\n if list.is_empty(xs) { 0 }\n else {\n let h := match list.head(xs) { Some(v) => v, None => 0 }\n let t := list.tail(xs)\n h + sum(t)\n }\n}\n\n# Accumulate with fold (preferred over manual recursion)\nfn sum_fold(xs :: List[Int]) -> Int {\n list.fold(xs, 0, fn (acc :: Int, x :: Int) -> Int { acc + x })\n}\n\n# Build a list with map (preferred — correct order, no reverse needed)\nfn double_all(xs :: List[Int]) -> List[Int] {\n list.map(xs, fn (x :: Int) -> Int { x * 2 })\n}\n\n# Build a list with range + map (generate n items in order)\nfn make_labels(n :: Int) -> List[Str] {\n list.map(list.range(1, n + 1), fn (i :: Int) -> Str { str.concat(\"item-\", int.to_str(i)) })\n}\n\n# CAUTION: fold + cons builds the list in REVERSE order.\n# Always wrap with list.reverse if you need the original order.\nfn double_all_fold(xs :: List[Int]) -> List[Int] {\n list.reverse(list.fold(xs, [], fn (acc :: List[Int], x :: Int) -> List[Int] {\n list.cons(x * 2, acc)\n }))\n}\n```\n\n### Searching a list of tuples (common pattern)\nTo find a key in `List[(Str, Int)]`, use `list.fold` — there is no `list.find`:\n```lex\nfn lookup(env :: List[(Str, Int)], name :: Str) -> Option[Int] {\n list.fold(env, None, fn (acc :: Option[Int], kv :: (Str, Int)) -> Option[Int] {\n match acc {\n Some(v) => Some(v),\n None => match kv { (k, v) => if k == name { Some(v) } else { None } },\n }\n })\n}\n```\nNote: the lambda `fn (acc :: Option[Int], kv :: (Str, Int)) -> Option[Int]` is valid — tuple types in lambda params ARE allowed, just not when the lambda itself is directly inside a function call that would make the `)` ambiguous. When in doubt, assign the lambda to a `let` binding first.\n\n### Anti-patterns to avoid\n- `_ =>` in match unless it's a genuine catch-all.\n- `while`/`for` loops — Lex has none; use tail recursion or `list.fold`.\n- Reassigning `let` bindings — they are immutable; restructure with a recursive function.\n- String-concat SQL — use `sql.query(db, q, [params])`.\n- Rolling your own crypto — use `std.crypto`.\n- Wide `[net, io, fs_read, fs_write]` when only one is used.\n- Renaming params after publish (changes the SigId).\n- Adding `[io]` to a pure function (the checker will reject it).\n\nFor the full reference: call `load_guidelines` tool or run `lex agent-guidelines`." + "## Lex Language — Core Reference\n\nLex is a typed-effect functional language. Files use `.lex`; the default toolchain is `lex`.\n\n### Syntax quick-start\n\n```lex\n# Function with explicit types (all signatures are explicit)\nfn add(x :: Int, y :: Int) -> Int\n examples {\n add(0, 0) => 0,\n add(2, 3) => 5,\n }\n{ x + y }\n\n# Effectful function — effects come BEFORE the return type\nfn save(path :: Str, data :: Str) -> [fs_write] Result[Unit, Str] {\n fs.write(path, data)\n}\n\n# Pure function — NO effect annotation at all\nfn double(n :: Int) -> Int { n * 2 }\n\n# Let binding — uses `:=`, not `=`\nlet result := add(1, 2)\n\n# Algebraic data type\ntype Shape = Circle(Float) | Rect { w :: Float, h :: Float }\n\n# Match (must be exhaustive)\nmatch shape {\n Circle(r) => 3.14159 * r * r,\n Rect { w, h } => w * h,\n}\n\n# Result / Option idioms\nmatch http.get(url) {\n Ok(resp) => resp.body,\n Err(e) => str.concat(\"error: \", e),\n}\n\n# Import\nimport \"std.list\" as list\nimport \"./util\" as util\n```\n\n### Common syntax pitfalls\n| What you might write | Correct Lex |\n|---|---|\n| `x: Int` | `x :: Int` |\n| `let x = e` | `let x := e` |\n| `-> T` with no effects | `-> T` (omit `[]` entirely for pure fns) |\n| `-> [] T` | invalid — write `-> T` |\n| Exceptions / throw | use `Result[T, E]` — no exceptions in Lex |\n| `else if cond { }` | `else { if cond { } }` — **Lex has NO `else if`**. Always nest: `else { if ... { } else { } }` |\n| `True` / `False` | `true` / `false` — booleans are lowercase (unlike Haskell) |\n| `list.concat(a, b, c)` | `list.concat(a, list.concat(b, c))` — concat takes exactly 2 args |\n| `//` comment | `#` comment — Lex uses `#`, not `//` or `/*` |\n| write to `src/file.lex` | write to `file.lex` — use plain filenames, not subdirectory paths |\n| `list.concat(...)` without import | add `import \"std.list\" as list` — every stdlib module must be explicitly imported |\n| `fn f(x) examples{...} match t {` | function body MUST be `{ }` wrapped: `fn f(x) examples{...} { match t { ... } }` |\n| `type T = \\| A \\| B` | NO leading pipe: `type T = A \\| B` — first variant has no `\\|` prefix |\n| `pair.0` / `pair.1` | tuples have no field access — destructure: `match pair { (a, b) => a }` |\n| `while` / `for` loops | Lex is purely functional — use recursion or `list.fold` |\n| `let x := 1` then `let x := 2` | `let` bindings are immutable — cannot reassign; use a recursive helper or fold |\n| `a && b` / `a \\|\\| b` | use `a and b` / `a or b` — Lex uses keywords, not `&&`/`\\|\\|` |\n| `list.find(...)` | does not exist — use `list.filter` + `list.head`, or `list.fold` |\n| `list.zip(a, b)` | does not exist — use `list.enumerate(a)` to get `List[(Int, T)]`, or compare as strings: `str.join(a, \",\") == str.join(b, \",\")` |\n| `str.to_str(n)` | does not exist on `str` — convert Int to Str with `int.to_str(n)` from `std.int` |\n| `fn (kv :: (A, B)) -> T` in a call | tuple type in lambda param causes parse error; match inside body instead: `fn (kv :: (A, B)) -> T { match kv { (a, b) => ... } }` |\n| `match xs { [] => ..., [h, ..t] => ... }` | list pattern matching is NOT supported — use `if list.is_empty(xs) { ... } else { let h := list.head(xs); let t := list.tail(xs); ... }` |\n| `fn helper(...)` inside a function body | local named functions are NOT allowed — extract every helper to the top level |\n| `list.join(parts, sep)` | does not exist — use `str.join(parts, sep)` from `std.str` |\n| `VBool(bool)` / `x :: bool` | type names are uppercase: `Bool`, `Int`, `Str`, `Float` — never `bool`, `int`, `str` |\n| `let a := x,` (comma after let) | `let` bindings inside a block need NO separator — just newlines. Commas go between match arms only. |\n\n**No `else if` — this is the single most common mistake.** Lex only has `if/else`. Chain conditions by nesting:\n```lex\n# WRONG — will not parse:\nif a { x } else if b { y } else { z }\n\n# CORRECT — always nest else { if ... }:\nif a { x } else { if b { y } else { z } }\n```\n\n### Effect discipline (most important rule)\n- Declare the **narrowest** effect set that matches the body.\n- Pure functions: no effect annotation — `fn f(x :: T) -> T`.\n- Never add `[io]` \"just in case\"; the checker will reject unused effects.\n- Common effects: `io`, `net`, `llm`, `sql`, `fs_read`, `fs_write`, `env`, `proc`, `time`, `concurrent`.\n- Fine-grained path scopes: `[fs_write(\"/tmp/out/\")]`, `[net(\"api.example.com\")]`.\n\n### examples {} blocks\nPure functions SHOULD have an `examples {}` block — they're folded into the SigId and become free regression tests:\n```lex\nfn clamp(n :: Int, lo :: Int, hi :: Int) -> Int\n examples {\n clamp(5, 0, 10) => 5,\n clamp(-1, 0, 10) => 0,\n clamp(99, 0, 10) => 10,\n }\n{ if n < lo { lo } else { if n > hi { hi } else { n } } }\n```\n\n### Stdlib index\n| Module | Import | Key fns | Effect |\n|---|---|---|---|\n| list | `std.list` | map, filter, fold, head, tail, concat, cons, len, is_empty, enumerate, range, reverse, sort_by, par_map | pure |\n| str | `std.str` | concat, split, trim, join, contains, replace, is_empty | pure |\n| io | `std.io` | print, readline, argv, read, write | [io] |\n| http | `std.http` | get, post, send, stream_lines | [net] |\n| sql | `std.sql` | open, query, exec, close | [sql] |\n| fs | `std.fs` | read, write, exists, list_dir, walk | [fs_read/fs_write] |\n| env | `std.env` | get, set | [env] |\n| conc | `std.conc` | spawn, ask, tell | [concurrent] |\n| json | `std.json` | parse, stringify | pure |\n| regex | `std.regex` | match, split, replace | pure |\n| int | `std.int` | to_str, parse, abs, min, max | pure |\n| float | `std.float` | to_str, parse, floor, ceil, round | pure |\n\n### Iteration loop\n1. Write or edit the `.lex` file — the `write` tool auto-runs `lex check` and returns an error if it fails.\n2. **If `write` returns an error, fix the code and call `write` again immediately.** Keep iterating until write succeeds. Never stop after a write error.\n3. Read the structured error (`rule_tag` + `rule_explanation`) to understand what to fix.\n4. `lex_run fn_name` — run a specific function.\n5. `lex_test` — run the test suite.\n\n### List processing idioms\nLex has no list pattern matching syntax. Use these patterns instead:\n```lex\n# Recursive list processing (head/tail, NOT match [h,...t])\nfn sum(xs :: List[Int]) -> Int {\n if list.is_empty(xs) { 0 }\n else {\n let h := match list.head(xs) { Some(v) => v, None => 0 }\n let t := list.tail(xs)\n h + sum(t)\n }\n}\n\n# Accumulate with fold (preferred over manual recursion)\nfn sum_fold(xs :: List[Int]) -> Int {\n list.fold(xs, 0, fn (acc :: Int, x :: Int) -> Int { acc + x })\n}\n\n# Build a list with map (preferred — correct order, no reverse needed)\nfn double_all(xs :: List[Int]) -> List[Int] {\n list.map(xs, fn (x :: Int) -> Int { x * 2 })\n}\n\n# Build a list with range + map (generate n items in order)\nfn make_labels(n :: Int) -> List[Str] {\n list.map(list.range(1, n + 1), fn (i :: Int) -> Str { str.concat(\"item-\", int.to_str(i)) })\n}\n\n# CAUTION: fold + cons builds the list in REVERSE order.\n# Always wrap with list.reverse if you need the original order.\nfn double_all_fold(xs :: List[Int]) -> List[Int] {\n list.reverse(list.fold(xs, [], fn (acc :: List[Int], x :: Int) -> List[Int] {\n list.cons(x * 2, acc)\n }))\n}\n```\n\n### Searching a list of tuples (common pattern)\nTo find a key in `List[(Str, Int)]`, use `list.fold` — there is no `list.find`:\n```lex\nfn lookup(env :: List[(Str, Int)], name :: Str) -> Option[Int] {\n list.fold(env, None, fn (acc :: Option[Int], kv :: (Str, Int)) -> Option[Int] {\n match acc {\n Some(v) => Some(v),\n None => match kv { (k, v) => if k == name { Some(v) } else { None } },\n }\n })\n}\n```\nNote: the lambda `fn (acc :: Option[Int], kv :: (Str, Int)) -> Option[Int]` is valid — tuple types in lambda params ARE allowed, just not when the lambda itself is directly inside a function call that would make the `)` ambiguous. When in doubt, assign the lambda to a `let` binding first.\n\n### Anti-patterns to avoid\n- `_ =>` in match unless it's a genuine catch-all.\n- `while`/`for` loops — Lex has none; use tail recursion or `list.fold`.\n- Reassigning `let` bindings — they are immutable; restructure with a recursive function.\n- String-concat SQL — use `sql.query(db, q, [params])`.\n- Rolling your own crypto — use `std.crypto`.\n- Wide `[net, io, fs_read, fs_write]` when only one is used.\n- Renaming params after publish (changes the SigId).\n- Adding `[io]` to a pure function (the checker will reject it).\n\nFor the full reference: call `load_guidelines` tool or run `lex agent-guidelines`." }