Skip to content

feat(loop): per-task model selection + per-model token spend (hew-1tq)#54

Merged
droidnoob merged 6 commits into
mainfrom
feat/loop-model-config
May 29, 2026
Merged

feat(loop): per-task model selection + per-model token spend (hew-1tq)#54
droidnoob merged 6 commits into
mainfrom
feat/loop-model-config

Conversation

@droidnoob
Copy link
Copy Markdown
Owner

Closes the hew-1tq epic: dynamic per-task model selection + per-model token spend in hew loop summary.

What lands

commit what
4aaf79a feat(config): loop.model.{default,by_priority,by_type} schema (hew-b1l)
3d287b2 feat(loop): resolve_model() pure fn with tag>label>config precedence (hew-d1j)
49bd2ab feat(loop): thread per-task resolve_model into SpawnOpts (hew-6et)
dd34f53 feat(loop): IterLog.model field with backward-compat parse (hew-2cq)
2fbfb30 feat(loop): per-model breakdown table in loop_summary (hew-cdn)
4477314 docs(loop): per-task model selection guide (hew-k7j)

How model is resolved per task

  1. Description-tag model:<name> in the task body — highest precedence.
  2. bd label model:<name> on the task.
  3. loop.model.by_type.<type> then loop.model.by_priority.<n> from config.
  4. loop.model.default.
  5. Built-in default.

Config surface

[loop.model]
default = "claude-opus-4-7"
by_priority = { "0" = "claude-opus-4-7", "2" = "claude-sonnet-4-6" }
by_type     = { "bug" = "claude-opus-4-7", "chore" = "claude-haiku-4-5-20251001" }

CLI:

hew config set loop.model.default "claude-opus-4-7"
hew config set "loop.model.by_priority.0" "claude-opus-4-7"
hew config get loop.model.default

Summary impact

When at least one iter records a non-null model, hew loop summary renders a new "by model" table (input/cached/output/total tokens, iter_count, tasks_closed). When no iter has a model label, the section is hidden — verified against a legacy run with all-null model fields.

Backward compat

  • IterLog.model: Option<String> — old iter logs without the field deserialize as None and render under "(default)" if any other iter in the run has a label.
  • Existing .hew/loop/<run-id>/run.json files unaffected.

Smoke (hew-diyp)

  • cargo build — clean.
  • hew config set loop.model.default "claude-opus-4-7" then get returns it.
  • hew config set "loop.model.by_priority.0/2" then get loop.model.by_priority returns the CSV.
  • hew loop summary --run-id <legacy-run> correctly hides the "by model" section when iter logs lack the field.

Stacking

Based on feat/parallel-loop-worktrees (PR #53). Both branches touch hew-core/src/loop_log.rs; landing PR #53 first avoids a textual conflict.

🤖 Generated with Claude Code

Base automatically changed from feat/parallel-loop-worktrees to main May 29, 2026 20:48
droidnoob added 6 commits May 30, 2026 02:20
- LoopModelConfig nested inside LoopConfig; BTreeMap for deterministic
  serialization, all-None / empty by default.
- hew config get/set: bulk paths (loop.model.by_priority) format/parse
  comma-separated KEY=VAL pairs; dotted paths (loop.model.by_priority.P0)
  read/insert individual entries; empty value clears.
- Model names unvalidated per task Craft — free-form strings passed to
  the spawner; runtime CLI rejects unknown ones at invocation.
- 7 new tests + probe values added to keys_includes_every_settable_path.
…(hew-d1j)

- new hew_core::loop_model module with TaskRecord<'_> + resolve_model()
- precedence: <!-- hew:model=X --> tag > model:X label > by_priority[P{n}]
  > by_type[issue_type] > cfg.default > None
- 13 unit tests cover malformed-tag fall-through, empty label values,
  non-model labels, trimmed tag, empty issue_type skips by_type
- RunConfig gains loop_model: LoopModelConfig; run_loop_with takes it
  as a new parameter, run_loop pipes it in from cfg.loop_cfg.model.
- Iter call site builds TaskRecord from the ready task and resolves
  the per-iter --model / -m override before spawning.
- New integration test loop_dynamic_model.rs asserts both the
  description-tag threading path (Some("opus")) and the empty-config
  no-op path (None) via MockSpawner.last_opts.

Closes hew-6et.
- Add model: Option<String> to IterLog with #[serde(default)]
- Populate from SpawnOpts.model_override alongside runtime_used
- Fixture hew-core/tests/fixtures/iter-log-pre-model.json proves
  legacy iter logs parse with model=None
- Add Summary::per_model: Vec<ModelBreakdown> populated from IterLog.model
- Group iters by model label (None -> "(default)"); sum input/cached/
  output/total tokens, iter_count, tasks_closed
- Render new "by model" table section when any iter recorded a model;
  hidden otherwise (no flag, no config)
- Cached column folds cache_read + cache_create
- 4 new tests: mixed-model run, no-model hides section, default-label
  for unlabeled iters, render columns present
- docs/LOOP.md gains 'Per-task model selection' section: tag > label
  > config precedence, TOML config example, by-model summary sample,
  per-model prompt-cache caveat.
- CHANGELOG [Unreleased] Added entry covering the precedence chain
  and the conditional 'by model' breakdown table.
- skills/core/hew-decompose.md Step 6 nudge: flag heavy tasks with a
  tag or label so hew loop routes them to a stronger model.
- skills/core/hew-plan.md craft-refinement tail nudge: surface
  per-task model needs during planning so decomposer can annotate.

Closes hew-k7j. Closes epic hew-1tq (6/6 children done).
@droidnoob droidnoob force-pushed the feat/loop-model-config branch from 4477314 to a09965b Compare May 29, 2026 21:06
@droidnoob droidnoob merged commit 96d0345 into main May 29, 2026
21 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant