Skip to content

feat(cli): emit reusable columnar JSON for paginated output - #322

Draft
V3RON wants to merge 12 commits into
mainfrom
codex/issue-318-columnar-output
Draft

feat(cli): emit reusable columnar JSON for paginated output#322
V3RON wants to merge 12 commits into
mainfrom
codex/issue-318-columnar-output

Conversation

@V3RON

@V3RON V3RON commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a public paginated tool declaration to @rozenite/agent-shared, re-exported from @rozenite/agent-bridge
  • propagate pagination field metadata through tool registration, multi-device aggregation, SDK discovery, descriptors, and schema inspection
  • replace the CLI's hard-coded domain/tool allowlist with metadata-driven shaping that works for built-in and third-party plugins
  • migrate built-in React, console, and network tools plus the Network Activity and TanStack Query plugins
  • document the external plugin contract and include the release changeset

Behavior

Declared paginated tools return the normal {items, page} contract. Their registered metadata defines the stable allowed/default field order; the CLI never infers columns from observed values.

  • two or more rows emit cols / rows
  • zero or one row remains row-keyed under items
  • absent optional values become null
  • tool-owned metadata is preserved
  • terminal pagination metadata is omitted
  • continued pages receive a shell-safe runnable next command
  • undeclared, malformed, and non-row results remain unchanged
  • --json remains a deprecated no-op with no legacy escape hatch

Why

Agent CLI row payloads repeated field names for every record, increasing byte and token usage. The original implementation solved this with a CLI-private allowlist, which excluded third-party plugins and duplicated knowledge of tool schemas. The corrected design makes pagination presentation an explicit tool contract owned by each producer.

Issue #320 remains responsible for the separate shared cursor/source pagination engine and migrations away from offset pagination.

Validation

  • 108 focused pagination, CLI, bridge, SDK, middleware, and plugin tests pass
  • affected package typechecks pass
  • affected package lint checks pass
  • @rozenite/agent-bridge production build passes
  • Prettier and git diff --check pass
  • changeset release plan validates
  • the broader CLI run retains five unrelated existing config-wrapper expectation failures caused by the generated enabled option

Closes #318

@V3RON V3RON changed the title Emit columnar JSON for agent row output Emit reusable columnar JSON for paginated agent output Jul 28, 2026
@V3RON V3RON changed the title Emit reusable columnar JSON for paginated agent output feat(cli): emit reusable columnar JSON for paginated output Jul 28, 2026
V3RON added 7 commits July 30, 2026 13:45
…-pagination

# Conflicts:
#	packages/middleware/src/__tests__/agent-local-domains.test.ts
…de on bad pagination metadata

- Add session.tools.resolve() to agent-sdk, resolving a domain/tool to its
  schema and a bound call() from a single getSessionTools fetch, and expose
  the canonical resolved domainId. The CLI's call-tool path now uses it
  instead of getSchema()+call(), cutting per-call-tool-invocation payload
  from two full tool listings to one.
- Reject non-object --args for declared paginated tools with a clear error
  instead of silently substituting {}; non-paginated tools keep their prior
  args passthrough behavior.
- Make getToolPagination degrade malformed pagination metadata to
  "non-paginated" (with a stderr warning) instead of throwing, matching the
  documented contract that malformed pagination leaves results unshaped.
`next` commands echoed the user-typed domain token verbatim, including
the deprecated legacy slug alias, and were built with the bare
`rozenite` binary despite the documented `npx rozenite` workflow.

- formatAgentCommand now emits `npx rozenite agent ...`.
- The call-tool continuation uses `resolvedTool.domainId` (already
  resolved by session.tools.resolve, no extra fetch).
- session.tools.list now attaches the resolved canonical `domainId` to
  each AgentDomainTool so the `agent <domain> tools` listing
  continuation can use it without a second getSessionTools round trip;
  the pagination cursor scope is keyed off the same canonical id so
  re-running the emitted command doesn't invalidate the cursor.
- Added a shell-safety test for slash-containing domain ids (e.g.
  avasapp/ably) and updated SKILL.md's next-command example to match.
…rk cursors

- projectRowKeyed omits absent fields instead of nulling them, so 0/1-row
  agent listings stay smaller than the columnar shape they exist to avoid
  (only the columnar branch, which needs positional cells, fills null).
- shapeToolResult now degrades to unshaped output instead of silently
  overwriting tool metadata that collides with cols/rows/next.
- register-agent-command no longer double-projects rows before handing
  them to shapePaginatedRows, which already projects internally.
- Network domain pagination stamps cursors with the capture generation and
  returns page.reset instead of resuming into a wiped buffer after an app
  relaunch invalidates a previously-issued cursor.
…ion fields

First-party paginated tools never declared defaultFields, so --verbose was a
no-op and heavy columns like console getMessages' argsPreview/context always
shipped. Add defaultFields to console, react, network, network-activity, and
tanstack-query tool declarations, and add a small cursorPagination helper
that ties each built-in's declared fields to keyof its actual row type,
turning schema drift into a build error instead of a silent null column.
Move parseFields, parseLimit, projectRows, shapePaginatedRows, and
shapeToolResult from packages/cli into @rozenite/agent-shared so MCP and
other agent-sdk consumers can reuse the same envelope, per issue #318's
maintainer comment. shapePaginatedRows/shapeToolResult now take a
frontend-neutral `nextAffordance` string instead of a CLI-specific
`nextCommand`, so the shared functions carry no shell-command assumptions.

paginateRows (cursor encode/decode) and formatAgentCommand/shellEscape
stay CLI-only: paginateRows is an offset pager scoped to CLI-owned
tools/domains listings, not the general shared pagination engine that
issue #320 already owns, and the shell-command builder is explicitly a
CLI-only rendering of the next-page affordance per the maintainer's
carve-out.

Adds a test script + vitest suite to agent-shared, which previously had
none.
@V3RON

V3RON commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Merged main into this branch and pushed fixes for a code review pass. Fast-forward from 1fc93326; no history rewritten.

Review findings addressed

Commit Fixes
29faa712 merge main (6 commits) — one trivial conflict in agent-local-domains.test.ts, an import this branch's new test needs
f7c2cd1f every paginated call made two full getSessionTools fetches (getSchema then call, both through the uncached resolveDomainContext) — replaced with a single session.tools.resolve(). Non-object --args was silently replaced with {}; now throws. Malformed third-party pagination metadata failed the whole call; now degrades to unshaped output, matching the documented contract
cd1f5c2c next echoed the user-typed domain token, so a deprecated legacy slug (at-rozenite__mmkv-plugin, kept "for one release cycle" per #328) could be baked into a generated command — now emits the canonical domain.id. next also emitted a bare rozenite ... while SKILL.md instructs npx rozenite
98e836e8 projectRows filled null in the row-keyed branch, making 0/1-row payloads larger than before — which is the whole reason that branch exists (#318 measures columnar at −7% for one row). Also: page.reset was handled by the CLI but no producer ever emitted it, so a stale cursor after an app relaunch returned {"items":[]}, indistinguishable from "no more rows"
987dd84a no first-party tool declared defaultFields, so --verbose was a no-op and console.getMessages always emitted argsPreview + context — the opposite of #318's "projection is the bigger lever". Built-in pagination.fields were unchecked strings; now coupled to their row types via keyof, so drift is a build error
12c802c7 moved the encoder, projection, and page envelope into @rozenite/agent-shared per the request on #318, leaving the CLI a thin renderer. formatAgentCommand/shellEscape stay CLI-side per carve-out 1, and the shared shaper takes a frontend-neutral affordance instead of assuming a shell command

Notes

Two findings from the review were split out rather than folded in here:

  • ci(cli): run the CLI test suite in CI #331packages/cli has no test script, so turbo run ... test --affected never ran this package's tests, including the ~600 lines this PR adds. It also explains the 5 config-wrapper.test.ts failures that had been sitting in the repo. Worth landing first so this PR's tests actually execute in CI.
  • fix(agent-sdk): stop maxItems from dropping rows and reporting a stale cursor #332autoPaginate.maxItems drops rows and returns a cursor that skips them. Pre-existing in agent-sdk, but this PR is what turns that cursor into a runnable next command. Once it lands, this branch should consume the new page.truncated signal and suppress next when it is set.

Carve-out 2 (token-based rather than row-based page cap) is not addressed here, deliberately — see my note on #320.

Also worth a reviewer's eye: 98e836e8 changes the network cursor scope from network:requests:${generation} to network:requests with generation as its own field, so a stale cursor now returns page.reset: true instead of a generic "Invalid cursor" error. And cd1f5c2c changes the tools-listing cursor scope from the typed token to the canonical id, without which a re-run of this PR's own emitted command would fail scope validation.

Validation

  • npx turbo run typecheck lint test --force across the whole repo: 100 successful, 100 total
  • packages/cli suite directly (turbo does not run it, see ci(cli): run the CLI test suite in CI #331): 10 files, 86 tests, all passing
  • New @rozenite/agent-shared test suite: 16 tests — that package previously had none
  • Verified the field-drift protection by introducing a typo: TS2820: Type '"nodeIdXX"' is not assignable to type 'keyof ReactTreeNode'
  • prettier --check fails on 1654 files, identically on main; the two files this branch had regressed are fixed in 4bbb7bcc

One caveat on the earlier tsc --noEmit -p tsconfig.json checks in this branch's history: packages/cli/tsconfig.json and packages/middleware/tsconfig.json are solution files with "include": [], so that command typechecks nothing. Everything above was re-verified through turbo, which builds workspace dependencies first.

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.

Emit columnar JSON for row-shaped agent output to cut token usage

1 participant