11 KiB
issue, issue_title
| issue | issue_title |
|---|---|
| 283 | Formatter extension seam for custom tool input previews |
Retro: #283 — Formatter extension seam for custom tool input previews
Stage: Planning (2026-05-31T00:00:00Z)
Session summary
Produced a numbered implementation plan for the tool input formatter seam.
Confirmed both prerequisites (#282 extract ToolPreviewFormatter, #266 configurable limits) are shipped/closed, then designed a persistent ToolInputFormatterRegistry, a seam-first dispatch in formatToolInputForPrompt, a registerToolInputFormatter method on PermissionsService, and a reference built-in MCP input summarizer registered through the public seam.
Observations
- Despite the dual
pkg:label, the user confirmed this is pi-permission-system only — pi-subagents would reach outward to register, violating its "arrows point inward" principle, so the plan is filed in the package'sdocs/plans/beside#266/#282rather than the repo-rootdocs/plans/. ToolPreviewFormatteris constructed fresh per tool call (fromthis.session.config), so the formatter registry cannot be instance state on it — it must be owned by the extension factory (index.ts) and threaded in. This shaped the whole design.- The seam convention follows pi-subagents'
registerWorkspaceProvider(provider): () => void(single provider, throws on duplicate, identity-guarded disposer). Adopted the same: one formatter per tool name, duplicateregisterthrows. - Reference built-in decision: user chose the MCP summarizer keyed to
mcpover a fictionalbatchtool. Important catch — MCP calls take an early-return branch informatAskPromptand never reachformatToolInputForPrompt, so the built-in needs a second integration point in the MCP branch (and changes existing MCP prompt tests). Captured as a dedicated TDD step. - Precedence: registered formatter checked first for any tool;
undefinedfalls through to the existing switch (user-selected). Lets extensions override even built-in tool previews. - Made the new
PermissionGateHandlerconstructor parameter optional somakeHandlerand the twoexternal-directory-*.test.tshandler constructions compile unchanged — onlyindex.tspasses the shared registry. Minimizes test churn. - Open questions deferred to implementation: whether to try/catch a throwing registrant, exact MCP summary wording, and whether to record this as a formal architecture roadmap phase.
Flagged writing a disposable exploratory check against a real MCP payload before finalizing
formatMcpInputForPrompt. - Next step:
/tdd-plan(this plan has red→green→commit cycles).
Stage: Implementation — TDD (2026-05-31T21:05:00Z)
Session summary
All five TDD steps completed across six commits (steps 1–4 plus a docs step plus a WARN fix).
Test count grew from 1628 to 1656 (+28), across 73 test files (up from 71).
Full suite, type check, lint, and fallow dead-code all pass.
Observations
ToolPreviewFormatteris constructed fresh per call, not held as instance state, so the formatter registry has to be owned by the extension factory (index.ts) and threaded in as an optional 4th constructor parameter onPermissionGateHandler. Making it optional leftmakeHandlerand the twoexternal-directory-*.test.tsconstructions untouched — onlyindex.tspasses the real registry.- MCP branch bypass was the main design surprise.
formatAskPrompt's MCP early-return never calledformatToolInputForPrompt, so the built-in MCP summarizer needed a deliberate second integration point there. Addingcase "mcp": return "";to the switch was also necessary — without it, when a custom formatter declines, the switch default serialises the raw MCP event to JSON and appends it to the prompt. - Truncation test correction: the initial test for "truncates the full summary when it exceeds the limit" used a single 200-char string value, but
renderArgValuecaps string values at 60 chars, so the total never reached the 160-char summary limit. Fixed by using three long-valued arguments so the joined summary exceeds 160 chars. service.test.tshad two inlinePermissionsServiceliterals that don't go throughmakeService;tsccaught them after adding the new interface method — both neededregisterToolInputFormatter: vi.fn()added.- Pre-completion reviewer verdict: WARN — one finding:
docs/architecture/architecture.mdstill said "exposes two methods" afterregisterToolInputFormatterwas added. Fixed in a follow-updocs:commit before closing. - Deferred (Open Questions from the plan): try/catch guard on misbehaving formatters and the exact wording of the MCP summary prefix — settled on
with key: value, ...format which reads naturally in the prompt. No follow-up issue needed for these; they are implementation details documented in the code. - Post-review docs pass: a thorough authoring guide for
registerToolInputFormatterwas added todocs/cross-extension-api.md(commit6d154a14). It covers the per-toolinputshapes, the must-not-throw contract,undefined-vs-""semantics, the grammatical-fragment guidance, an end-to-end register/dispose lifecycle example, and recommended practices. It also documents — and corrects an earlier misleading example about — the MCP keying limitation: the gate keys on the registered Pi tool name (getToolNameFromValue), so MCP calls all arrive under the"mcp"umbrella and cannot be keyed perserver:tool;"mcp"is already held by the built-in. A potential follow-up surfaced: a chained/per-server:toolMCP formatter model would need a richer seam than the current one-formatter-per-name registry.
Stage: Final Retrospective (2026-06-01T00:30:00Z)
Session summary
Shipped #283 end to end in one continuous session: planning, TDD (9 commits), /ship-issue (CI green, issue closed, release-please merged to pi-permission-system@8.3.0), a post-ship docs-thoroughness pass, and a courtesy follow-up to the original #266 requester.
The feature — a registerToolInputFormatter seam plus a built-in MCP argument summarizer — works as designed, but three of the session's friction points share one shape: the agent delivered the mechanical minimum and the user had to push for the thorough version.
Observations
What went well
- Incremental verification was disciplined:
pnpm run checkplus the targetedvitestfile ran after each TDD step, not just at the end, so type and test regressions surfaced one step at a time. - The plan flagged "confirm the real MCP input shape before writing
formatMcpInputForPrompt," and the agent followed through with agrepofmcp-targets.ts/input-normalizer.tsinstead of guessing the{ tool, server, arguments }shape. ask_userresolved genuine design ambiguity (precedence, reference-built-in target) cleanly in two focused calls without over-asking.- The
#266follow-up disclosed the MCP keying limitation honestly rather than overstating the feature.
What caused friction (agent side)
scope-drift(user-caught) — the first-pass docs for the public seam (docs: document tool input formatter seam,2fc9ff1d) were just the interface signature plus one example. The user had to ask "Did we thoroughly document how to create these formatters…?" which triggered a substantial authoring guide (6d154a14, +115/−19) covering per-toolinputshapes, the must-not-throw contract,undefined-vs-""semantics, lifecycle wiring, and limitations. Impact: one extra user prompt and one follow-up commit; every gap was knowable when the thin docs were written.premature-convergence(partly user-surfaced) — the agent committed to "MCP summarizer keyed tomcp" in planning without tracing the umbrella-keying constraint to its conclusion: because every MCP call arrives under the singlemcptool, the seam can never do bespoke per-MCP-tool rendering, which is literally what#266's title ("smart formatters for known MCP tools") and thectx_batch_executeexample asked for. The limitation only became explicit while writing the authoring guide and was disclosed post-ship. Impact: no rework — the generic summarizer is still valuable — but the shipped feature only partially fulfills the original#266ask, surfaced after release.missing-context(user-caught) — the/ship-issueclose comment mentioned#266, but the agent did not proactively notify the human requester (@kuba-4chain) on their issue; the user prompted "We should also get back to the submitter of#266, right?" Impact: one extra user prompt and one follow-up comment to close the loop.instruction-violation(self-identified) — the/plan-issueprompt says "multiplepkg:*labels → cross-package → rootdocs/plans/," but after the user confirmed the work waspi-permission-system-only, the agent filed the plan in the package directory instead. The override was correct (the determinant is which packages' code changes, not the labels), but the prompt's rule was mechanically wrong for this case. Impact: added deliberation, no rework.other(self-caught) — the first "truncates the full summary" test used a single 200-char value that could not exceed the 160-char summary cap, becauserenderArgValuecaps each value at 60 chars; fixed in the same red step with three long-valued arguments. Impact: negligible, caught before commit.
What caused friction (user side)
- Two of the friction points (docs thoroughness, notifying the
#266submitter) were corrections the user could instead have pre-empted by stating up front "this is a public API — write authoring docs and notify the original requester on ship." Framed as opportunity: a one-line "treat this as a third-party-facing API" cue at planning time would likely have produced the thorough docs and the courtesy follow-up without the two mid-stream nudges.
Diagnostic details
- Model-performance correlation — the only subagent dispatch was the
pre-completion-revieweronanthropic/claude-sonnet-4-6, judgment-heavy review work; appropriate task-model fit, and it caught a real staleness (architecture.mdsaid "exposes two methods" after a third was added). - Feedback-loop gap analysis — positive: verification ran incrementally (per-step
check+ targetedvitest), withlintandfallow dead-codereserved for the end where their cross-cutting scope belongs. No end-only-verification gap. - Escalation-delay and unused-tool lenses found nothing notable (no rabbit-holes, no >5-call error loops, no obviously-skipped tool).
Changes made
AGENTS.md(Code Style) — added a rule: public or cross-extension APIs must be documented for third-party authors (input/return contract, error/throw semantics, a minimal wiring example, known limitations), not just the type signature. Addresses friction #1..pi/prompts/plan-issue.md— clarified thatpkg:*labels are a hint, not the determinant: a plan is cross-package only if code in more than one package actually changes; a confirmed single-package scope files in that package's directory despite multiple labels. Addresses friction #4.
Not implemented (user declined): proposal B (a /ship-issue step to notify an external requester when shipping work deferred from their issue, friction #3).
The behavior was still performed manually this session via the #266 follow-up comment.