Skip to content

Model Groups spawn router#14

Open
grzegorznowak wants to merge 13 commits into
mainfrom
model-groups-spawn-router
Open

Model Groups spawn router#14
grzegorznowak wants to merge 13 commits into
mainfrom
model-groups-spawn-router

Conversation

@grzegorznowak

@grzegorznowak grzegorznowak commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds Model Groups end to end: operators can manage named model groups in a trust-aware TUI, discover effective groups through prompt/autocomplete guidance, and route spawned child agents through an explicitly requested group. Spawn without a group continues to inherit the parent model and thinking level.

Persisted group data is schema-validated and failure-safe. If an atomic save reaches rename and the rename fails, the generated temporary artifact is removed best-effort while the prior committed configuration remains unchanged and the original typed rename failure remains primary. The spawn boundary regression now proves that a known group with no usable models propagates its typed SpawnRouteError before any child session work is created.

Requirements

  • Provide full Model Groups CRUD for global and trusted-project scopes through /model-groups.
  • Restrict untrusted sessions to global data without probing or exposing project state.
  • Validate persisted model entries against Pi's live model registry and surface degraded/unavailable state.
  • Preserve canonical group identity, prototype-sensitive keys, native thinking levels, and terminal-safe labels across persistence and TUI boundaries.
  • Offer effective #group-name completions and names-only system-prompt guidance without exposing provider, model, authentication, or persistence details.
  • Let spawn accept an optional exact group name, randomly select a usable authenticated member, resolve/clamp thinking for the selected model, and isolate the child session.
  • Preserve inherited routing when no group is supplied and provide explicit behavior for unknown, empty, or unusable groups.

Acceptance criteria

  • /model-groups supports create, rename, scope move, model-entry editing, and deletion with immediate persistence and clear recovery on failures.
  • Trusted sessions resolve global and project groups with project overrides; untrusted sessions expose and mutate global groups only.
  • Malformed, unsupported-version, or schema-invalid configuration is backed up and reset; filesystem failures preserve the last committed configuration.
  • A failed atomic rename removes its generated temporary artifact when cleanup succeeds, keeps committed bytes unchanged, and reports the original typed rename error even if cleanup also fails.
  • Non-reasoning models omit the redundant off picker option while off remains valid in Pi-native persisted/type contracts.
  • Dynamic labels and filesystem/error details are escaped before terminal rendering, and all TUI output remains width-bounded and focus-safe.
  • Effective group names refresh after startup, group mutations, and handoff/compaction; autocomplete inserts only the exact group token.
  • Group-routed spawn chooses among currently usable authenticated members, applies configured-or-inherited thinking within model capability, and reuses parent runtime registry/auth while preserving child isolation.
  • Unknown groups fall back to inherited routing; known empty or unusable groups fail clearly.
  • A known unusable group propagates SpawnRouteError with unusable-group / no-usable-models before invoking the session factory or registering child/live sessions.

Contract changes

  • spawn input: adds optional group?: string; previously cached thinking arguments remain tolerated but are ignored.
  • Model Groups persistence: adds versioned global and trusted-project JSON configuration using Pi's configured project directory name and native ModelThinkingLevel, including max.
  • /model-groups: adds a TUI-only management command; RPC reports that TUI mode is required, while JSON/print modes return without mutation.
  • #group-name completion: adds editor guidance for effective groups without introducing a separate parser/control plane.
  • Prompt guidance: advertises effective names only and directs uncertain routing intent to inherited behavior.
  • Persistence resilience: rename failure triggers best-effort generated-temp cleanup, preserves committed bytes, and retains the original typed rename failure as the primary cause.
  • Dependency policy: Pi packages retain peer range *; development and lock resolution are coherent at 0.81.0.

Out of scope

  • Interactive spawn-time pickers or automatic routing without explicit group intent.
  • Registering groups as model-picker entries or meta-model proxies.
  • Child profiles, prompts, extra tools, budgets, context policies, weighted/priority/health/rate-limit routing, retry failover, or fuzzy matching.
  • Explicit provider/model override parameters on spawn.
  • Interactive CRUD in RPC, JSON, or print modes.
  • New quoted/all-name mention grammar, model reordering, descriptions, or search/filter UI.
  • Model Group name-policy changes for unsafe or ambiguous names; tracked separately in #18.

How to verify

Run from the pi-agenticoding repository at e137b8d:

npm test
# 481/481 passed

node ./scripts/run-node-test.mjs tests/unit/spawn.test.ts
# 56/56 passed

node ./scripts/run-node-test.mjs \
  tests/unit/model-groups-crud.test.ts \
  tests/unit/model-groups-display.test.ts \
  tests/unit/model-groups-tui.test.ts \
  tests/unit/model-groups-integration.test.ts \
  tests/unit/model-groups-router.test.ts \
  tests/unit/model-groups-autocomplete.test.ts \
  tests/unit/spawn.test.ts
# 90/90 passed

npx tsc --noEmit -p tsconfig.model-groups-producer.json
# passed

npm ls @earendil-works/pi-ai @earendil-works/pi-coding-agent @earendil-works/pi-tui
# coherent at 0.81.0

git diff --check
# passed before commit

CI note

  • Windows test homes are isolated for Model Groups tests.
  • CI audits runtime dependencies with --omit=dev. The full development audit remains affected by upstream shrinkwrapped Pi development dependencies and is not claimed as fixed here.

Initiative reference

  • Initiative: model-tag-router
  • Stories: model-groups-data-layer-tui — Model Groups data layer + TUI (CRUD); model-groups-spawn-router — Model Groups spawn router
  • Change workspaces: openspec/changes/model-groups-data-layer-tui/; openspec/changes/model-groups-spawn-router/

@grzegorznowak
grzegorznowak force-pushed the model-groups-spawn-router branch from 479d78e to 907b4e7 Compare June 16, 2026 04:53
@grzegorznowak
grzegorznowak requested a review from ofriw June 16, 2026 08:04

@ofriw ofriw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the strong PR — the Model Groups architecture is clean and the feature scope is well-contained.

Must fix before merge

1. /model-groups does not guard missing project/model context

The interactive /model-groups command calls createModelGroupsComponent(...) with ctx.modelRegistry and ctx.cwd unconditionally (model-groups/command.ts). But elsewhere in the same subsystem, missing cwd / modelRegistry is explicitly treated as a valid condition and safely skipped (index.ts, refreshModelGroupsState).

That inconsistency means /model-groups can fail when the command is invoked without a fully initialized project/model context, instead of degrading gracefully.

Expected fix: add the same guard pattern in the command path and surface a friendly message when the required context is unavailable.

2. deleteGroup() can report failure after the delete already succeeded

In model-groups/store.ts, deleteGroup() removes the group and saves the updated config first, then loads the other scope to compute otherScopeHasOverride.

If that opposite-scope load throws (for example because recovery hit a backup-failed malformed config), the function throws after the target delete already committed to disk.

That creates a bad correctness property: callers can observe an error for an operation that already mutated persistent state successfully.

Expected fix: make the opposite-scope lookup non-fatal, or compute it before the delete is committed.

3. Test helper duplication

The new model-group test files duplicate helpers like registry(), group(), and withTemp() in several places. That is valid DRY cleanup work, but it is maintenance debt rather than a release blocker.

AGENT_REVIEW.md

…fix deleteGroup error-after-commit

- model-groups/command.ts: add guard for missing ctx.cwd or ctx.modelRegistry
  before calling createModelGroupsComponent; surface friendly notification
  instead of crashing. Matches refreshModelGroupsState pattern in index.ts.
- model-groups/store.ts deleteGroup(): load opposite-scope config before
  saving the delete so a load failure doesn't throw after the delete is
  already persisted on disk. Follows moveGroup's pre-validation pattern.
@grzegorznowak

Copy link
Copy Markdown
Collaborator Author

Addressed the two must-fix items:

1. /model-groups missing context guard → fixed in command.ts

Added check for !ctx.cwd || !ctx.modelRegistry before calling createModelGroupsComponent, surfacing a friendly notification instead of crashing. Matches the existing guard in refreshModelGroupsState().

2. deleteGroup() throwing after successful delete → fixed in store.ts

Reordered deleteGroup() to load the opposite-scope config before calling saveModelGroups. If loadScopeConfig for the other scope throws, nothing was persisted yet — clean failure. Mirrors the pre-validation pattern already used in moveGroup().


This comment is AI-generated.

- Extract duplicated withTemp() and group() into tests/unit/model-groups-helpers.ts
- withTemp: unified async version replacing 2 local variants (crud + integration)
- group: unified opts-object signature replacing 3 local variants (tui, router, autocomplete)
- registry() kept per-file — each test file needs different model sets
@grzegorznowak

Copy link
Copy Markdown
Collaborator Author

3. Test helper DRY cleanup

Extracted shared withTemp() and group() helpers into tests/unit/model-groups-helpers.ts:

  • withTemp() — unified async version (was duplicated in crud and integration tests)
  • group() — unified opts-object signature (was duplicated across tui, router, and autocomplete tests)
  • registry() kept per-file — each test file needs different model sets, not worth forcing into a shared helper

Existing helpers.ts untouched — it stays domain-agnostic.

190/190 pass.


This comment is AI-generated.

grzegorznowak added a commit to grzegorznowak/pi-agenticoding that referenced this pull request Jun 24, 2026
…del-groups, fix deleteGroup error-after-commit

- model-groups/command.ts: add guard for missing ctx.cwd or ctx.modelRegistry
  before calling createModelGroupsComponent; surface friendly notification
  instead of crashing. Matches refreshModelGroupsState pattern in index.ts.
- model-groups/store.ts deleteGroup(): load opposite-scope config before
  saving the delete so a load failure doesn't throw after the delete is
  already persisted on disk. Follows moveGroup's pre-validation pattern.
@grzegorznowak
grzegorznowak requested a review from ofriw June 24, 2026 14:22

@ofriw ofriw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #14 Review — Model Groups & Spawn Routing

AGENT_REVIEW.md

Assessment

Clean architecture, good test coverage, well-contained scope. One correctness issue to fix before merge. Rest is tech debt.

Issues

1. 🔴 Model group state can go stale when context is unavailable

refreshModelGroupsState in index.ts:47 returns early without clearing state.modelGroups when ctx.cwd or ctx.modelRegistry is missing. Stale groups from a previous valid session remain in state and get injected into the system prompt via before_agent_start (line 208). The session reset in resetState does clear this, but that only runs on /new — not between agent runs within the same extension lifetime. Fix: clear state.modelGroups.groups and state.modelGroups.validation before the early return.

2. 🟡 createModelGroupsComponent is 378 lines

model-groups/tui.ts:76–453. Project guideline: functions above 20 lines should be broken into smaller units. The TUI component contains 7 screen renderers, input handling, and CRUD operations in a single closure. Track as tech debt.

3. 🟡 modelRegistry accessed via as any casts

Four occurrences across index.ts:46–47 and spawn/index.ts:224–225. The ExtensionContext type doesn't include modelRegistry, so every access bypasses type safety. If the upstream property changes shape or name, there's no compile-time protection.

4. 🟢 Unused state field: wizardThinking

model-groups/tui.ts:88. Declared in the TUI state object but never read or written anywhere. The wizard reads thinking options directly via thinkingOptionsFor(currentWizardModel())[state.row].

5. 🟢 Missing SpawnRouteError integration test

model-groups-router.test.ts covers SpawnRouteError at the router level (empty group, no usable models). But no test in spawn.test.ts verifies the error propagates correctly through executeSpawn to the caller — that boundary is untested.

Already addressed

The contributor fixed 3 items from an earlier review pass: context guard in /model-groups command, deleteGroup() save-before-load ordering, and test helper extraction. All verified.

What's good

  • Pure routerresolveSpawnModelRoute is side-effect-free with an injectable rng seam for deterministic testing
  • Boot validation — eagerly validates groups against the model registry at startup, surfaces misconfigurations via operator notifications instead of failing at spawn time
  • Scoped persistence — backup-before-overwrite, typed ModelGroupsPersistenceError with phase tracking, and partialMove state for moveGroup
  • Names-only prompt guidance — group internals (provider, model, thinking, auth) are never exposed to the child agent
  • Test coverage — 6 new test files covering CRUD, routing, TUI, autocomplete, integration, and spawn rendering with good edge-case depth

@grzegorznowak

Copy link
Copy Markdown
Collaborator Author

Updated this PR with 30c0f89 (feat(model-groups): complete data layer and TUI).

Data-layer/TUI follow-up

  • Added trust-aware global-only access when project scope is unavailable or untrusted.
  • Hardened schema/version recovery, typed persistence failures, canonical own-key handling, and collision checks.
  • Added reversible controlled-label display escaping.
  • Reworked the interactive flow around Pi-native TUI primitives with bounded rendering and focus propagation.
  • Aligned the Pi dependency family at 0.80.7 and added a producer-facing typecheck contract.

Verification

  • npm test: 204/204
  • Focused data-layer/TUI suites: 36/36
  • npm run test:e2e: 10/10
  • Producer contract tsc: passed
  • Pi dependency coherence: 0.80.7
  • Diff checks: passed

This update does not include or claim the deferred sibling prompt/autocomplete/router/spawn follow-ups or FB-004/FB-006/FB-008.


This comment is AI-generated.

@grzegorznowak

Copy link
Copy Markdown
Collaborator Author

Addressed review item 4 in 41d6d28: removed the unused wizardThinking initializer from model-groups/tui.ts. The focused TUI suite (17/17), unit suite (204/204), producer typecheck, and diff check pass.

The separately identified stale-state issue remains blocking and is not resolved by this refactor.

…uter

# Conflicts:
#	.github/workflows/test.yml
#	index.ts
#	package-lock.json
#	package.json
#	spawn/index.ts
#	state.ts
@grzegorznowak

Copy link
Copy Markdown
Collaborator Author

Merged current origin/main (26e65f9) into this branch in merge commit 181502c and pushed it.

Resolution highlights:

  • composed Model Groups session/spawn behavior with readonly rehydration, branch lifecycle, and child bash/tool guards
  • cleared Model Groups state before unavailable-cwd/registry early returns to prevent stale routing
  • preserved Pi 0.80.7 while adding main's typecheck/Husky/audit tooling; regenerated the lockfile
  • reconciled audit-ci.jsonc with the clean resolved 0.80.7 dependency graph
  • added regression coverage for stale Model Groups state and grouped + readonly spawn composition
  • updated the test ExtensionAPI mock and spawn thinking type for Pi 0.80.7 API/type changes

Local validation passed:

  • npm ci
  • npm run typecheck
  • npx tsc --noEmit -p tsconfig.model-groups-producer.json
  • focused merge suite: 110/110
  • full unit suite: 439/439
  • E2E suite: 16/16
  • npx audit-ci --config audit-ci.jsonc
  • npm audit --audit-level=moderate (0 vulnerabilities)
  • conflict-marker/unmerged-entry/diff whitespace checks

The GitHub matrix (Linux Node 22/24, macOS Node 24, Windows Node 24) is currently queued, and the PR still has a CHANGES_REQUESTED review state, so this comment does not claim final readiness yet.

@grzegorznowak

grzegorznowak commented Jul 17, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for the review. I reassessed every item against the current head (e988f72). All correctness blockers are now resolved, including the unavailable-context stale-state regression; the earlier context guard, delete ordering, duplicated helpers, and unused wizardThinking items are also addressed.

For the remaining points:

  • (ctx as any).modelRegistry: Pi 0.80.7's ExtensionContext does declare modelRegistry, so the rationale that this property is absent from the public context type does not apply here. Removing the casts would still be reasonable cleanup to restore stronger type checking, but it is not a correctness blocker.
  • createModelGroupsComponent size: Although the factory is long by raw line count, it encapsulates one coherent stateful Model Groups TUI component and its screen-state machine. Its rendering and behavior are already divided into named internal helpers that depend closely on shared component state. I also could not find any repository instruction establishing a 20-line function limit. Further extraction may be worthwhile as optional maintainability work, but function length alone does not establish a defect or merge blocker.
  • Missing spawn-boundary SpawnRouteError test: The router's failure behavior is covered, and executeSpawn invokes routing before session creation and directly propagates that error. A dedicated boundary test would improve coverage, but this is non-blocking test debt rather than an unresolved behavior defect.

The current head is cleanly mergeable, and the full Linux/macOS/Windows CI matrix is green. @ofriw, please re-review when you have a chance.

…uter

# Conflicts:
#	audit-ci.jsonc
#	package-lock.json
#	package.json
#	spawn/index.ts
#	tests/unit/config-invariants.test.ts
#	tests/unit/spawn.test.ts
@grzegorznowak
grzegorznowak requested a review from ofriw July 20, 2026 13:43

@ofriw ofriw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: This review was generated by an AI agent. If you'd like to talk with other humans, drop by our Discord!


Thanks for working through the earlier routing, stale-state, and persistence-ordering feedback; the remaining blockers are narrow. Please constrain Model Group names to a representation that is safe and unambiguous in both the system prompt and #group completion. Names are currently only trimmed, while the TUI can decode control escapes back into the stored name; modelGroupsPromptSection() then inserts names raw into a comma-delimited prompt list. A newline/control sequence can alter that prompt section, and commas, whitespace, or # make the advertised exact-name/completion contract ambiguous. Reject unsupported names consistently in load/save/TUI paths and cover hostile/ambiguous names with prompt and autocomplete tests.

Please also remove the generated model-groups.json.*.tmp file if the atomic rename fails. The active config correctly remains intact, but the failed-save path currently leaves stale durable artifacts behind; the existing failure test verifies the error and committed file but not cleanup. Add the cleanup with error preservation, a regression assertion that no temporary file remains, and an executeSpawn boundary test that verifies an unusable known group returns SpawnRouteError before a child session is created.


Attached is an agent optimized description of the changes in this PR - AGENT_REVIEW.md

@grzegorznowak

Copy link
Copy Markdown
Collaborator Author

FB-011 is resolved in 6d8e701: failed atomic renames now remove the generated temp file best-effort, preserve the committed config bytes, and keep the original typed rename error primary. Fresh verification is 37/37 focused Model Groups tests, 480/480 full tests, producer typecheck PASS, and Pi 0.81.0 dependency coherence.

Please re-review this cleanup update. FB-012 (the executeSpawn boundary proof) remains a separate work item; this update does not claim to resolve it or other review threads.

@grzegorznowak

Copy link
Copy Markdown
Collaborator Author

FB-012 is resolved in e137b8d: the new executeSpawn boundary regression proves that a known unusable group propagates the original typed SpawnRouteError (unusable-group / no-usable-models) before the session factory runs and before child/live session registration.

Fresh validation on the committed delta:

  • focused spawn suite: 56/56
  • focused Model Groups + spawn story suite: 90/90
  • full npm test: 481/481
  • npx tsc --noEmit: PASS
  • diff checks: PASS

The corresponding OpenSpec story review is APPROVE/PASS; receipt identity: sha256:ae14983f9f967fba7477cca0e33188a2edf6472432c7eb9ea32a91e41e353b20. This update is test-only and does not absorb deferred unsafe-name issue #18.

@ofriw ofriw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

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.

2 participants