% Copyright (c) 2026 Rana Umar Nadeem, Samrah Mumtaz, Muhammad Imran # Planner `mace.planner.plan(spec, llm, tools=(), feedback="", rtl_edits=False, rejection="", phase="plan")` turns a `MaceSpec` into a task DAG with one LLM call. It sends the prompt from `build_prompt()`, parses the response with `mace.agents.parse_tasks()`, and checks the result with `mace.integrator.topological_levels()`. ## Prompt The prompt names the core, objective, target mesh, and gate workloads, and specifies the directive lines below. On a replan, `build_prompt()` appends the triage feedback under the line `Feedback from a previous attempt, to inform this plan:` (see [Failure Analysis](failure_analysis.md)). When the plan check turned the previous plan away before any build, `rejection` adds the reasons after the feedback, and the call is recorded in the `plan_retry` phase (see [Fixed Checks](fixed_checks.md)). ## Directive Lines ```text TASK: | deps= | kind=config|workload|unit_test|rtl | CACHES: | =, ... CONFIG_RTL: | ... ``` For example: ```text TASK: base | deps= | kind=workload | Run the gate workload on the default caches TASK: l1d_8way | deps=base | kind=config | Rebuild with an 8-way L1D and run it again CACHES: l1d_8way | l1d=8192,8 ``` The parsers in `mace/agents.py` share three rules. A tag starts its line, after optional whitespace and an optional list marker (`-`, `*`, `•`, `1.`, `1)`), and matches case-insensitively. A value ends at the end of its line or before another known tag on the same line. A malformed line is dropped without an error. Triage and post-mortem directives follow the same rules, and each keeps its last match (see [Failure Analysis](failure_analysis.md)). `TASK:` lines accumulate in response order. Each needs four `|`-separated fields: a non-empty id, a `deps=` field, a `kind=` field naming a known kind, and the instruction. A third field that is a bare known kind, such as `| rtl |`, reads as `kind=rtl`: planners drop the prefix, and it was the most common reply that held no plan. `CACHES:` accepts `l1i`, `l1d`, `l15`, and `l2`, each with a positive integer size and associativity, and drops malformed entries. Several lines for one task merge, and a later value for the same cache wins. The prompt lists the defaults: `l1i=16384,4 l1d=8192,4 l15=8192,4 l2=65536,4`. `CONFIG_RTL:` accepts upper-snake-case identifiers such as `CONFIG_DISABLE_BIST_CLEAR`. The flags from all of a task's lines form a sorted union. The flags add to the default RTL defines and never replace them. No allowlist applies, so any well-formed name reaches the build. An override line applies to the task it names. `plan()` then adds to each task the `CONFIG_RTL:` flags of every task it depends on, directly or through other tasks, so a workload task that depends on a config task builds with that task's defines. `CACHES:` overrides stay with the task they name (see [Parallel Task Execution](task_execution.md)). ## Validation `Task.__post_init__` in `mace/spec.py` rejects an empty id, a kind outside `TASK_KINDS`, an empty dependency id, an unknown or duplicated cache name, a non-positive cache size or associativity, and a malformed or duplicated `config_rtl` flag. `parse_tasks()` drops any line whose `Task` fails these checks. `plan()` raises `LLMCallFailed` when the backend gave no reply at all, as when the network is down; the run then ends as `error`, which a batch runs again, rather than as a failed plan. The retry agent treats a failed call the same way. `plan()` raises `PlanningError` when no task remains, or when `topological_levels()` finds a duplicate id, a dependency on an unknown id, or a cycle. The orchestrator then asks the planner again, up to twice, with the error and the line format (see [Fixed Checks](fixed_checks.md)). When the retries are spent, it logs the error, which quotes a reply with no `TASK:` lines or names the graph problem, records the planner's calls with their replies, and ends the run with status `planning_failed`. A valid DAG can still be a plan the loop cannot carry out, such as a task that asks for a source edit in a run without RTL edits. The orchestrator checks each plan for those before it builds anything, and sends one back to the planner with the reasons (see [Fixed Checks](fixed_checks.md)). ## Task Kinds | Kind | Instruction | Execution | |---|---|---| | `config` | A change to the build configuration | Build the task's configuration, run each gate workload | | `workload` | Running or fixing a gate workload | Same as `config` | | `unit_test` | One module's RTL path relative to the checkout root, such as `piton/design/chip/tile/pico/rtl/picorv32.v` | Scaffold a unit-test environment, adapt its testbench, build it | | `rtl` | What to change in the design's RTL, and why | Edit files under `piton/design/` through an edit tool, build, run each gate workload (see [Parallel Task Execution](task_execution.md)) | `config` and `workload` share one execution path; the label records intent. Without `LoopOptions(rtl_edits=True)`, the prompt states that no task can edit a source file, that a `config` or `workload` task changes only what its `CACHES:` and `CONFIG_RTL:` lines set, and that a build failing with changes from the defaults may fail because of one of them, so a change the objective does not ask for should be left out (`NO_RTL_RULE`). Without it, the planner turned triage's suggested source edits into `config` tasks that could not make them. The prompt offers the `rtl` kind only under `LoopOptions(rtl_edits=True)`. Offered on tasks that pass at their defaults, it made the planner write RTL tasks about files that do not exist, so the default leaves it out; without it, an `rtl` line still parses and runs as a `config` task. ## Thinking Cap Gemini bills thinking tokens at the output price, and they were most of a planner call's cost. `LoopOptions.planner_thinking` caps the thinking tokens of each planner call at `PLANNER_THINKING_BUDGET`, which is 512 tokens. The orchestrator sets the cap with `mace.llm.thinking_budget()` around every planner call, first plans and retries alike, and `mace.vertex` puts it on the Vertex request as a thinking budget. A value of `None` leaves the amount to the model, and a backend that cannot cap thinking ignores the cap. Only the planner is capped: triage, the task calls, the post-mortem, and the retry agent think as the model chooses.