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).
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).
Directive Lines
TASK: <id> | deps=<comma-separated task ids, or empty> | kind=config|workload|unit_test|rtl | <short instruction>
CACHES: <task id> | <name>=<size>,<associativity> ...
CONFIG_RTL: <task id> | <FLAG1> <FLAG2> ...
For example:
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).
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).
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).
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).
Task Kinds
Kind |
Instruction |
Execution |
|---|---|---|
|
A change to the build configuration |
Build the task’s configuration, run each gate workload |
|
Running or fixing a gate workload |
Same as |
|
One module’s RTL path relative to the checkout root, such as |
Scaffold a unit-test environment, adapt its testbench, build it |
|
What to change in the design’s RTL, and why |
Edit files under |
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.