Files
Lumotia/docs/architecture-map/04-llm-formatting-mcp/llm-decompose-task.md
Jake 26c7307607
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
agent: lumotia-rebrand — docs, scripts, root config, residuals
Phase 9 of the rebrand cascade. Sweep covers everything the Phase 8
frontend pass deliberately skipped: docs/, root markdown, scripts,
Cargo.toml descriptions, code comments that survived earlier
word-boundary sed, plus a handful of identifiers caught on the final
verify pass.

transcription-app changes:
- README.md, HANDOVER.md, KNOWN-ISSUES.md, run.sh — magnotia/Magnotia
  -> lumotia/Lumotia.
- docs/ — sweep across all subdirs except docs/handovers/ (preserved
  as immutable audit trail). Includes architecture-map references
  to magnotia_core::*, magnotia_storage::*, etc. now pointing at
  lumotia_*; dev-setup.md tracing output examples (lumotia_startup
  target); brief/ + superpowers/ + issues/ + whisper-ecosystem/ +
  audit/.
- Cargo.toml descriptions on 9 crates (core, audio, cloud-providers,
  hotkey, llm, mcp, plus referenced others).
- crates/core/src/{error,hardware,recommendation,paths}.rs +
  crates/audio/src/wav.rs + crates/llm/src/model_manager.rs +
  crates/cloud-providers/src/keystore.rs + crates/mcp/src/lib.rs —
  doc comments and a model-manager user-agent string.
- Caught on final pass: BroadcastChannel("magnotia_task_sync") -> ...
  ("lumotia_task_sync"); magnotia_locale i18n localStorage key
  renamed + migration shim added; CSS keyframe names
  magnotiaPulse / magnotiaBar / magnotiaFade renamed in the design-
  system kit; magnotia_viewer_item / magnotia_viewer_mode handoff
  keys renamed in HistoryPage + viewer/+page.svelte; src/assets/
  wordmark.svg text.
- src-tauri/src/lib.rs comment cleanup ("magnotia era" was sed'd
  to "lumotia era" earlier — restored).

Preserved (intentional):
- crates/core/src/paths.rs — keeps "magnotia" / "Magnotia" / ".magnotia"
  legacy detection strings in legacy_and_target_paths() so the
  migration shim can still find user data from the magnotia era.
- src/lib/stores/{page,focusTimer}.svelte.ts + src/lib/i18n/index.ts
  — migration call sites reference the legacy magnotia keys
  deliberately.
- docs/handovers/ — historical audit trail.

cargo build --workspace passes. npm run check: 0 errors / 0 warnings
(3958 files). cargo test --workspace: 339 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:38:03 +01:00

90 lines
5.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: LLM decompose_task surface
type: architecture-map-page
slice: 04-llm-formatting-mcp
last_verified: 2026/05/09
---
# LLM `decompose_task` surface
> **Where you are:** [Architecture map](../README.md) → [LLM, Formatting, MCP](README.md) → decompose_task
**Plain English summary.** `decompose_task` takes a task description and returns 3 to 7 short imperative micro-steps. The output is a JSON array of strings, constrained at the GBNF level so the model literally cannot emit fewer than three or more than seven. A feedback-conditioned variant adds few-shot examples from the user's HITL corrections.
## At a glance
- Crate: `lumotia-llm`
- Path: `crates/llm/src/lib.rs:254` (`decompose_task`) and `:267` (`decompose_task_with_feedback`)
- LOC: ~40 across both methods
- Public surface:
- `pub fn decompose_task(&self, task_text: &str) -> Result<Vec<String>, EngineError>`
- `pub fn decompose_task_with_feedback(&self, task_text: &str, examples: &[prompts::FeedbackExample]) -> Result<Vec<String>, EngineError>`
- Re-export not exposed at crate root: callers get `prompts::FeedbackExample` via `lumotia_llm::prompts::FeedbackExample` (the `prompts` module is `pub mod prompts;`).
- External deps that matter: GBNF sampler from llama-cpp-2; `serde_json` for the array parse.
- Tauri command that calls this (slice 2, best guess): the only call site is `src-tauri/src/commands/tasks.rs:322``engine.decompose_task_with_feedback(&parent_text, &examples)` — invoked from a `decompose_task_*_cmd` (the file's helper name; see slice 2's tasks page when written).
## What's in here
### `decompose_task` (`crates/llm/src/lib.rs:254`)
Convenience wrapper that calls `decompose_task_with_feedback(task_text, &[])`. Behaviour identical to the conditioned variant with no examples.
### `decompose_task_with_feedback` (`crates/llm/src/lib.rs:267`)
Steps:
1. Borrow the loaded model via `loaded_model_arc()`. `EngineError::NotLoaded` if no model is loaded.
2. Build the system prompt: `prompts::build_conditioned_system_prompt(prompts::DECOMPOSE_TASK_SYSTEM, examples)`. With an empty `examples` slice, the base prompt is returned unchanged. With non-empty examples, a few-shot block is appended (see [`llm-prompts-and-grammars.md`](llm-prompts-and-grammars.md) for the rendering rules).
3. Render a chat prompt with two messages — `("system", system)` and `("user", &format!("Task: {task_text}"))`.
4. Call `generate` with:
- `max_tokens: 512`
- `temperature: 0.0`
- `stop_sequences: ["<|im_end|>", "<|im_end_of_text|>"]`
- `grammar: Some(grammars::TASK_ARRAY_GRAMMAR.to_string())`
5. Parse the raw output via `parse_string_array` (`crates/llm/src/lib.rs:489`): `serde_json::from_str::<Vec<String>>` then trim, drop empties, dedupe case-insensitively while preserving first-seen ordering.
The 3-to-7 lower and upper bound is enforced by the GBNF, not by Rust code. The `parse_string_array` helper is happy to return arrays of any size; `TASK_ARRAY_GRAMMAR` makes that hypothetical impossible.
### Feedback examples
`prompts::FeedbackExample` carries `(input: String, original_output: Option<String>, corrected_output: Option<String>)`. The renderer prefers `corrected_output` over `original_output` so a user's edits beat a thumbs-up on the original. Empty inputs are skipped. Examples without any usable output (no original, no correction) are skipped. See `crates/llm/src/prompts.rs:69` for the renderer and `:93` for the prompt-builder.
## Data flow
```
(task_text: &str, examples: &[FeedbackExample])
→ loaded_model_arc()
→ build_conditioned_system_prompt(DECOMPOSE_TASK_SYSTEM, examples)
(returns base prompt unchanged when examples is empty)
→ render_chat_prompt([(system, system), (user, "Task: {task_text}")])
→ generate(prompt, GenerationConfig {
max_tokens: 512, temp: 0.0,
stops: [<|im_end|>, <|im_end_of_text|>],
grammar: Some(TASK_ARRAY_GRAMMAR),
})
→ parse_string_array(raw)
→ serde_json::from_str::<Vec<String>>
→ trim + drop empties + dedupe (case-insensitive, first-seen)
→ Vec<String>
```
## Prompts and grammars
- System prompt: `prompts::DECOMPOSE_TASK_SYSTEM` at `crates/llm/src/prompts.rs:1`. See [`llm-prompts-and-grammars.md`](llm-prompts-and-grammars.md) for the full text.
- GBNF: `grammars::TASK_ARRAY_GRAMMAR` at `crates/llm/src/grammars.rs:16`. Encodes "open bracket, exactly three strings, then up to four optional more strings, then close bracket". Recursive `rest3..rest6` chain is what bounds the array length to 37.
## Watch-outs
- **The GBNF is the source of truth for 37.** The system prompt also says "between 3 and 7", but the model's only actual constraint is the grammar. If the GBNF is ever loosened (e.g. for a free-text variant), the prompt will silently lose its size guarantee.
- **`parse_string_array` dedupe is case-insensitive.** Two micro-steps that differ only in casing collapse to one. This is desirable for the typical "Buy milk" / "buy milk" failure mode, but a niche prompt that legitimately asks for case variations would lose data.
- **`EngineError::InvalidJson` surfaces malformed grammar output.** In practice the GBNF prevents this, but a `LlamaSampler::grammar` runtime error or a tokenisation edge case can still produce a parse-able-by-llama but not-by-serde string. The error includes the raw output for debugging.
- **Stop sequences fire after detokenisation.** A token boundary that splits `<|im_end|>` is fine — the running buffer accumulates raw bytes via the UTF-8 decoder.
- **`max_tokens: 512` is the array's total budget, not per item.** Seven long imperative sentences will hit the cap. If a real-world task produces output near the ceiling, the JSON will be cut off and `serde_json::from_str` will return `EngineError::InvalidJson`.
## See also
- [LLM engine](llm-engine.md)
- [LLM extract_tasks (sibling array surface)](llm-extract-tasks.md)
- [Prompts and grammars catalogue](llm-prompts-and-grammars.md)
- [Slice README](README.md)