Skip to content

fix(storage): surface the table definition on table-detail (#621) - #629

Merged
padak merged 4 commits into
mainfrom
claude/issue-621-table-definition
Aug 21, 2026
Merged

fix(storage): surface the table definition on table-detail (#621)#629
padak merged 4 commits into
mainfrom
claude/issue-621-table-definition

Conversation

@padak

@padak padak commented Aug 21, 2026

Copy link
Copy Markdown
Member

Closes #621.

The gap

storage create-table --source-table-id ... --time-partitioning-field ... followed by storage swap-tables is the documented way to repartition a populated BigQuery table. Nothing in kbagent could read the result back.

StorageService.get_table_detail() built its response from an explicit field allowlist and definition was never on it, so the one object carrying the registered timePartitioning / rangePartitioning / clustering was dropped. The write half of the flow was supported, the verify half was not.

That matters more than "a field is missing":

  • The table ID is identical whether or not the swap happened. The layout is the only thing that tells the two apart.
  • create-table is not a second opinion. Its --json echoes the layout you requested (storage_service.py passes the input args straight into the result), and its --if-not-exists skip path sets all three layout keys to null on purpose — the existing table's layout is never re-derived.
  • The workaround is unavailable to the reporter. In a Keboola-managed BigQuery project without bigquery.jobs.create, the Storage definition is the only reachable view of the registered layout.

The data was already in hand: StorageTablesClient.get_table_detail() returns GET /v2/storage/tables/{id} unfiltered, and that response carries definition with no include= required.

Three things the issue got wrong or did not know

Verified against connection's TableDetailResponseProvider::getResponseArray() and BigqueryDriverConfig::extendTableDefinitionResponse(), then confirmed live.

1. definition is never null for an untyped table. The issue proposed table.get("definition") on the grounds that it "returns None for untyped tables, which is the expected shape". It does not — connection sets the key on every table-detail response, building one via createUntypedTableDefinitionResponseFromMetadata() when the table is untyped. So a null means the stack omitted the key, never "this table is untyped", and the human-mode guard keys off the layout fields rather than off definition's presence. Confirmed live on a Snowflake project: definition present ({columns, primaryKeysNames}), human output byte-identical to before.

2. The response carries more than the issue lists. With partitioning set it also returns requirePartitionFilter and partitions[] — one entry per physical partition, read from INFORMATION_SCHEMA.PARTITIONS. That list is unbounded; a DAY-partitioned table with three years of history returns ~1,100 entries. Human mode therefore prints the count, never the contents. --json passes the whole thing through: re-dropping an API field is the bug this PR fixes, so it is not repeated one level down. The size caveat is documented in gotchas.md and the workflow example uses jq to select keys rather than dumping the object.

3. There is a wire shape that would crash the proposed render. tests/test_storage_empty_definition.py records a real support incident where the Storage API served "definition": [] instead of an object and broke the Go CLI's strict decoder. Its docstring warns that a future x.get("definition", {}).get(...) would reintroduce the bug — which is exactly the shape the issue proposed. Every access here is isinstance-guarded, with a test for it.

What changed

Servicedefinition passes through verbatim.

Human mode — between the existing Primary key and Last import lines, and only when there is a layout:

Table: out.c-my-bucket.my-table
  Rows: 6,290,737
  Primary key: id
  Time partitioning: DAY on created_at
  Clustering: tenant_id, country
  Partition filter required: yes
  Partitions: 1,096
  Last import: 2026-08-19T17:26:18+0200

Time partitioning: / Range partitioning: rather than the issue's Partitioning: — those are the labels storage create-table already prints, and this PR makes both commands share the formatters. Verifying a repartition means diffing "what create-table said it applied" against "what table-detail reads back"; that comparison is only trustworthy if both render the same layout the same way.

JSON modedefinition verbatim, partitions[] included. GET /storage/table-detail/... on kbagent serve picks it up for free through the same service.

Not changed: storage tables. The list endpoint's include= accepts no definition value (TablesListRequest::ALLOWED_INCLUDE is attributes, buckets, bucketsWithoutSourceTables, columns, metadata, columnMetadata, sourceMetadata, sourceColumnMetadata), so a layout in the listing costs one detail request per table. The issue scoped that out and it was right to.

Why this is a refactor and not a one-liner

commands/storage.py and services/storage_service.py are both grandfathered over their file-size ceiling in scripts/file_size_baseline.json and may only shrinkloc-check fails on a single added code line. Per CONTRIBUTING's "extract pure helpers into a sibling module" guidance:

before after
services/storage_service.py 1733 1684
commands/storage.py 2246 2221
services/_table_detail.py (new, pure, client-free) 64
commands/_storage_table_detail.py (new) 63

Both baselines are re-recorded down. Only the two files this PR shrank are touched — make loc-baseline would also have ratcheted commands/config.py and commands/lineage.py, which this PR never opened, and tightening those would create merge friction for in-flight work for no benefit here.

Prompt budget (second commit)

keboola-expert.md was at 61 960 B against a 62 000 B ceiling — under 100 bytes free. At that margin the budget is a tripwire rather than a budget: this PR's one-line verify hint could not land without first finding an unrelated trim to pay for it. Raised to 70 000 B, still ~12% under the ~80 kB / ~20k-token reference the number came from. The standing guidance is restated in the constant's comment, not weakened: AGENT_CONTEXT remains the place for per-command detail, and splitting into per-domain specialists remains the answer to sustained growth.

The three prose sites still said "60 KB", stale since v0.48.0 moved the ceiling to 62 000 B — so an author trimming to the documented figure trimmed ~2 kB more than CI required. They now quote the enforced number and point at the test.

Overlap with #586: that open PR (issue #585) adds a test gating exactly this doc-vs-enforced drift. The prose here is written in the 70 000 B form its assertion expects, so it satisfies that gate on either merge order; the merge itself will still need a trivial textual resolution on CONTRIBUTING.md and kbagent-pr-reviewer.md.

Testing

  • tests/test_storage_table_definition.py (new, 12 tests, TDD — all watched failing first): passthrough for the BigQuery layout, for an untyped definition, for an absent key, and for the [] wire shape; human rendering of time and range partitioning, clustering, partition-filter and partition count; the negative case that a table without a layout prints nothing new; and JSON passthrough.
  • E2E: _test_table_listing asserts the key is present on any backend; _test_create_table_from_source now asserts that after the BigQuery repartition + swap, table-detail on the production name reports the clustering create-table applied. That is the issue's exact scenario, end to end.
  • make check green (lint, format, skill, version, changelog, error-codes, sentinel-guards, loc, 5,724 tests). make typecheck clean — the single hatchling diagnostic is pre-existing on main and unrelated.
  • Live-verified against a Snowflake project: definition present, human output unchanged.

Doc surfaces

AGENT_CONTEXT (commands/context.py), CLAUDE.md, commands-reference.md, gotchas.md (tagged since v0.88.0), storage-types-workflow.md (the repartition example already said "inspect with table-detail" — a step that could not show the layout until now; it gains an explicit verify step), keboola-expert.md, docs/e2e-scenarios.md, the --help docstring, and the regenerated SKILL.md table.


Open in Devin Review

padak added 3 commits August 21, 2026 23:01
`keboola-expert.md` sat at 61 960 B against a 62 000 B ceiling -- under 100
bytes of headroom. At that margin the budget stopped doing its job and became
a tripwire: any PR needing to add a line to the prompt first had to find and
justify an unrelated trim, which is review cost with no reviewer benefit.

Raise it to 70 000 B. That is still ~12% under the ~80 kB (~20k token)
reference point the budget was originally derived from, and the standing
guidance is unchanged and restated in the constant's comment: exhaustive
per-command detail belongs in `AGENT_CONTEXT`, which loads on demand, and the
real answer to sustained growth is splitting the prompt into per-domain
specialists rather than another bump.

The three prose sites still said "60 KB" -- stale since v0.48.0 moved the
ceiling to 62 000 B -- so an author trimming to the documented figure trimmed
~2 kB more than CI required. They now quote the enforced number in the
`70 000 B` form and point at the test that asserts it. This overlaps with the
open PR #586 (issue #585), which adds a test gating exactly this doc/enforced
drift; the wording here is written to satisfy that gate on either merge order.
`storage create-table --source-table-id ... --time-partitioning-field ...`
followed by `storage swap-tables` is the documented way to repartition a
populated BigQuery table. Nothing in kbagent could read the result back.

`StorageService.get_table_detail()` assembled its response from an explicit
field allowlist and `definition` was never on it, so the one object carrying
the registered `timePartitioning` / `rangePartitioning` / `clustering` was
dropped on the floor. The write half of the flow was supported, the verify
half was not -- confirming a swap meant leaving kbagent for raw Storage API
calls or BigQuery metadata access, which is not available at all in a
Keboola-managed BigQuery project without `bigquery.jobs.create`. The table ID
is identical whether or not the swap happened; the layout is the only thing
that tells the two apart. `create-table` is no substitute: its JSON echoes
the layout that was REQUESTED, and its `--if-not-exists` skip path nulls the
layout keys outright rather than re-deriving the existing table's.

The data was already in hand -- `StorageTablesClient.get_table_detail()`
returns `GET /v2/storage/tables/{id}` unfiltered, and that response carries
`definition` with no `include=` needed.

Two upstream details, read off connection's `TableDetailResponseProvider` and
`BigqueryDriverConfig::extendTableDefinitionResponse`, shape the rendering:

* `definition` is set on EVERY table-detail response, untyped and Snowflake
  tables included. A null therefore means the stack omitted the key -- never
  "this table is untyped" -- so the human-mode block keys off the layout
  fields themselves. Verified live against a Snowflake project: output is
  byte-identical to before.
* When partitioning is set the response also carries `requirePartitionFilter`
  and `partitions[]`, one entry per physical partition from
  INFORMATION_SCHEMA.PARTITIONS. That list is unbounded -- thousands of
  entries on a long-lived daily table -- so human mode prints its length and
  never its contents. `--json` passes the whole thing through: re-dropping an
  API field is the bug being fixed here, so it is not repeated one level down.

The value is also type-checked before any `.get()` reaches it. The Storage API
has really served `definition` as `[]` rather than an object (SUPPORT-16581,
pinned in tests/test_storage_empty_definition.py) -- that shape broke the Go
CLI's decoder, and `definition.get(...)` would raise on it.

`GET /storage/table-detail/...` on `kbagent serve` picks the field up for free
via the same service. `storage tables` (the LIST endpoint) is deliberately
untouched: the Storage API's `include=` there accepts no `definition` value
(`TablesListRequest::ALLOWED_INCLUDE`), so surfacing the layout in a listing
would cost one detail request per table -- a different change with a different
cost profile.

Both files this touches are grandfathered over their file-size ceiling and may
only shrink, so the change is made by extracting rather than appending:
response assembly moves to the pure, client-free
`services/_table_detail.py`, and human rendering to
`commands/_storage_table_detail.py`. Net effect is -49 code lines in
`storage_service.py` and -25 in `commands/storage.py`, both re-recorded in the
size baseline. `create-table` now shares the layout formatters, so both
commands print the same string for the same layout -- which is what makes
diffing "what I asked for" against "what landed" trustworthy.

E2E gains the assertion that matters: after the BigQuery repartition + swap,
`table-detail` on the production name must report the clustering that
`create-table` applied.
`kbagent changelog` and the "What's new" banner show only the first sentence,
capped at 160 chars; the original opener was cut off mid-clause.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

@padak padak left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Review of #629 — fix(storage): surface the table definition on table-detail (#621)

Generated by kbagent-pr-reviewer subagent. Verdict and findings below
are advisory; the human author retains every veto. CI-coverable issues
(lint, format, tests) are confirmed via make check, not duplicated here.

Summary

This PR fixes storage table-detail dropping the Storage API's definition object — on BigQuery the only readable record of a table's registered timePartitioning/rangePartitioning/clustering layout — which left the "verify a repartition" half of the documented create-table --source-table-id + swap-tables flow unsupported. The service now returns definition verbatim (extracted into a new pure services/_table_detail.py to stay inside the grandfathered file-size ceiling), human mode renders the layout via a new commands/_storage_table_detail.py, and the whole Plugin synchronization map (context.py, CLAUDE.md, keboola-expert.md matrix + gotchas budget bump, SKILL.md, commands-reference.md, gotchas.md with a (since v0.88.0, #621) tag, storage-types-workflow.md) plus service/CLI/E2E tests were all updated in the same PR. Verdict: COMMENT — no blocking findings, one small non-blocking style nit on a newly-extracted tuple return.

Verdict

  • Verdict: COMMENT
  • Blocking findings: 0
  • Non-blocking findings: 1
  • Nits: 1

Blocking findings

(none)

Non-blocking findings

[NB-1] src/keboola_agent_cli/services/_table_detail.py:538 — newly-extracted _split_descriptions returns a bare 2-tuple of semantically distinct values

_split_descriptions(raw_metadata: list[dict]) -> tuple[str, dict[str, str]] is a new function created by this PR's services/storage_service.py -> services/_table_detail.py extraction (it did not exist as a named function before; the logic was inline). It returns (description, col_descriptions) — a scalar table description and a per-column description map, which is exactly the "semantically distinct values" case CONTRIBUTING.md's "Return values -- name them with dataclasses, not tuples" section calls out (resolve_project example). The call site description, col_descriptions = _split_descriptions(raw_metadata) already relies on positional order to disambiguate.

Fix: wrap in a small @dataclass(frozen=True) (e.g. class DescriptionSplit: table: str; columns: dict[str, str]) or simply return a dict with those two keys. Small surface, no urgency, but flagging so it doesn't get grandfathered by accident.

Nits

  • [NIT-1] PR bundles a genuinely unrelated internal change (raising PROMPT_BYTE_BUDGET 62 000 B -> 70 000 B in tests/test_agent_prompt.py, plus matching CONTRIBUTING.md/kbagent-pr-reviewer.md edits) into a fix:-scoped PR as a second commit. It's well justified and clearly disclosed in the PR description ("Prompt budget (second commit)"), and was evidently necessary here (keboola-expert.md was at 61 960/62 000 B before this PR, i.e. ~40 bytes of headroom — not enough for the new matrix row + gotcha), so this is not a real scope objection, just noting it for the record since strictly it's a separate logical change from the #621 fix.

Verification log

  • git rev-parse --abbrev-ref HEAD -> claude/issue-621-table-definition (already on PR branch, matches <branch> input) ✓
  • gh auth status -> authenticated as padak
  • gh pr view 629 --json title,body,files,additions,deletions,baseRefName,headRefName,labels,state -> OPEN, main <- claude/issue-621-table-definition, 23 files, +663/-? (mergeable) ✓
  • Read CONTRIBUTING.md (Checklist / Plugin synchronization map / Releasing), CLAUDE.md conv. #17 + ## All CLI Commands, plugins/kbagent/agents/keboola-expert.md §1/§2/§3 ✓
  • gh pr diff 629 (1086 lines) reviewed in full ✓
  • Layer-violation greps (typer/console in services; httpx in commands) -> empty, no violations ✓
  • Magic-number / raw error-code / bare-except / print() / token-leak greps on the diff -> empty (only a doc line mentioning "budget" matched the token regex, false positive) ✓
  • New -> tuple[...] grep on the diff -> found _split_descriptions (see NB-1); no other new tuple returns ✓
  • grep -n '"storage.table-detail"' src/keboola_agent_cli/permissions.py -> "read", already registered — this PR adds a field, not a new command, so OPERATION_REGISTRY correctly needs no change ✓
  • grep -n "table-detail" src/keboola_agent_cli/server/routers/storage.py -> route exists, delegates to registry.storage.get_table_detail(...), so kbagent serve picks up definition automatically as the PR description claims ✓
  • uv run python scripts/check_command_sync.py -> OK: all 261 CLI commands are registered (OPERATION_REGISTRY) and documented (CLAUDE.md, context.py, commands-reference.md).
  • uv run python scripts/check_sentinel_guards.py -> OK: ... all 10 guards covered by SESSION_UNSUPPORTED_FEATURES
  • make loc-check -> OK: 214 modules within budget (7 over soft, 6 grandfathered); new files _table_detail.py (125 lines) / _storage_table_detail.py (113 lines) well under ceilings; grandfathered storage_service.py/commands/storage.py both shrank per scripts/file_size_baseline.json diff (1733->1684, 2246->2221) ✓
  • make check (background, 133s) -> 5712 passed, 12 skipped, 161 deselected exit 0 ✓
  • uv run ruff check / ruff format --check / ty check on the touched/new files -> all clean ✓
  • make changelog-check -> All 45 stable releases have changelog entries. (0.88.0 entry present) ✓
  • uv run pytest tests/test_storage_table_definition.py tests/test_storage_empty_definition.py -v -> 14/14 passed, including the definition: [] defensive-shape regression test referenced by the PR description ✓
  • Behavior claim reproduction: could not hit a live BigQuery project with a partitioned table (no credentials in this environment / per repo convention AI agents never handle tokens) — relied on the unit/CLI-layer tests above, which directly assert the claimed human-mode strings (Time partitioning: DAY on created_at, Partitions: 2 not the full list, Partition filter required: yes) and the --json passthrough shape. Marking this NON-VERIFIED-LIVE rather than a finding, since the test coverage is a faithful proxy and the PR also adds a real E2E assertion in tests/test_e2e.py (_test_create_table_from_source, _test_table_listing) that will run against a live project on the next make test-e2e / nightly E2E workflow.
  • Plugin synchronization map walk: commands/context.py (AGENT_CONTEXT) ✓, CLAUDE.md ✓, keboola-expert.md §2 matrix row updated + §3 gotchas not needed (existing "Storage" gotcha section extended in gotchas.md instead, appropriately) ✓, SKILL.md row updated ✓, commands-reference.md bullet updated with (since 0.88.0) ✓, gotchas.md new entry tagged (since v0.88.0, #621) ✓, storage-types-workflow.md extended with a verification step ✓ — every applicable "NO" row from the table was addressed.

Open questions for the author

(none)

…w NB-1)

`_split_descriptions` was extracted in the previous commit and returned a bare
`tuple[str, dict[str, str]]` -- a scalar table description and a per-column
description map, disambiguated only by position. CONTRIBUTING.md's "Return
values -- name them with dataclasses, not tuples" calls out exactly this case
("even two-element tuples should use a dataclass when the values are
semantically distinct"), and the rule applies since the function is new here,
not grandfathered inline code.

Returns a frozen `Descriptions(table=..., columns=...)` instead. No behavior
change; covered by the existing table-detail and describe-service tests.
@padak

padak commented Aug 21, 2026

Copy link
Copy Markdown
Member Author

Review addressed.

NB-1 (bare tuple return) — fixed in 4th commit. _split_descriptions now returns a frozen Descriptions(table=..., columns=...) instead of tuple[str, dict[str, str]]. The finding was right: the function is new in this PR (the logic was previously inline and therefore never a named return), so CONTRIBUTING.md:127 applies — "even two-element tuples should use a dataclass when the values are semantically distinct". No behavior change; covered by the existing table-detail and describe-service tests. (Minor: the citation read _table_detail.py:538; the file is 125 lines. The finding itself was accurate.)

NIT-1 (bundled prompt-budget change) — acknowledged, keeping it. It is a separate logical change and it is deliberately a separate commit, but it is not separable in practice: keboola-expert.md was at 61 960 B against the 62 000 B ceiling, so this PR's verify hint could not land without it (the file is 62 147 B now). Splitting it into its own PR would only mean this one blocks on that one.

make check green after the fix (5,712 passed).

@padak
padak merged commit 839e4e1 into main Aug 21, 2026
4 checks passed
@padak
padak deleted the claude/issue-621-table-definition branch August 21, 2026 22:15
padak added a commit that referenced this pull request Aug 21, 2026
padak added a commit that referenced this pull request Aug 21, 2026
The agent prompt sat at 61999 B against what was then a 62000 B
PROMPT_BYTE_BUDGET -- 1 byte free, so the next command group that genuinely
needed a tool-matrix row could not get one. v0.88.0 has since raised the cap
to 70000 B, and the comment introducing that bump says the quiet part out
loud: "It is NOT a licence to grow the file... Trim before you add." This is
that trim. The budget is a real runtime cost -- the prompt loads into every
subagent invocation -- so headroom should come from removing duplication,
not from the ceiling.

The bloat was not the version tags: ~95 (since vX.Y.Z) notes are only ~1 KB
in total. It was section 2, the tool selection matrix, at 32 KB -- 52% of
the file -- because its cells had grown into a second manual. The 37 files
in skills/kbagent/references/ already carry that prose; semantic-layer-
workflow.md even opens with a "When to use what" table in the same
intent-to-command shape, making the 11 semantic-layer rows (7.3 KB) near
verbatim duplication.

- section 2: 32044 -> 21092 B. Cells now carry the decision only (command,
  outcome-changing flags, the NEVER list) and point at the matching
  *-workflow.md for the rationale. The semantic-layer block collapses from
  11 rows to 2 (read, and "any write: export first, then read the workflow").
- section 3: 14768 -> 10978 B. Entries whose prose duplicated gotchas.md are
  back to one-line triggers, per the section's own stated design rule.
- Retired version tags whose floor no longer bites. Kept every gate where an
  older version is silently wrong: 0.54.0 plaintext #-secrets, 0.66.1 dormant
  cron, 0.86.0 Azure cipher, 0.87.0 data-app workspace flag.
- Dropped the hand-copied session_unsupported_features list, which the
  surrounding text already tells the agent not to reconstruct from memory --
  auth login --json ships it. Kept the dev-portal and flow list/detail
  carve-outs so the agent does not pre-emptively refuse working commands.

Rule 6 is reworded in the same pass. It used to say every command carries
its own since-tag and to treat those as the authoritative floor; under that
wording a stripped tag would silently read as "safe on any version". It now
states that a tag marks a floor that still bites, that its absence is not a
promise, and that a No such command error is a failed version gate.

The 0.88.0 verification guidance added to the BigQuery repartition row in
#629 is preserved through the rebase: table-detail --json ->
.definition.timePartitioning / .clustering, because create-table only echoes
the layout you requested.

Nothing moved into gotchas.md -- every trimmed block was already documented
there or in a topical workflow file, so no reference file changed.

The freed headroom immediately absorbs the two features that had to skip
this file: config clone (#587) and config state-get/state-set (#593) now
have matrix rows instead of living only in gotchas.md.

47830 B against the 70000 B budget, 22170 free. PROMPT_BYTE_BUDGET itself is
untouched by this PR. All 41 tests in tests/test_agent_prompt.py pass.
padak added a commit that referenced this pull request Aug 21, 2026
[B-1] Wire all five new flags through the `kbagent serve` routers. Each
router docstring claims it "Mirrors" its CLI command, and `search` already
exposed `--regex`, so the 1:1 contract demonstrably covers flags and not just
commands -- leaving these out made those docstrings false and left scheduled
agent tasks (which reach kbagent over REST, not argv) without the parity this
work is about:

- `GET /jobs` gains `offset`, `sort_by`, `sort_order`
- `GET /jobs/{project}/{job_id}` gains `log_tail_lines`
- `GET /search` gains repeatable `scope`
- `GET /storage/tables` gains `include_usage`
- `POST /sharing/{project}/link` body gains `stage`

[NB-1] `_fetch_tables` no longer conflates two meanings in its third tuple
slot. It was `True` without a usage scan and the component listing with one,
forcing the consumer to sniff `len(result) > 2` and widening the annotation to
`Any`. `_run_parallel` tells success from error by tuple LENGTH (base.py:253),
never by that element, so the slot now always carries the component listing
(empty when no scan was asked for) and the consumer destructures directly.

[NB-2] Added E2E coverage for all five flags in `TestE2EMcpParityCommands`:
the live-stack contracts the mocked suite cannot check (Queue API accepting
`sortBy`/`sortOrder`, `logTail` on a real finished job, `used_by` shape from a
real component listing) plus the three exit-2 validation paths. NOT RUN here
-- `make test-e2e` needs E2E_API_TOKEN against a real project.

Rebased onto #629. This branch carries no version bump: main is already at an
unreleased 0.88.0, so the changelog bullets join that entry and the version is
bumped once for the whole batch at release time. The byte-budget trims to
`keboola-expert.md` are reverted -- #629 raised that ceiling to 70 000 B, so
the prose they paid for is affordable again.

make check green against the rebased base: 5761 passed.
padak added a commit that referenced this pull request Aug 21, 2026
The agent prompt sat at 61999 B against what was then a 62000 B
PROMPT_BYTE_BUDGET -- 1 byte free, so the next command group that genuinely
needed a tool-matrix row could not get one. v0.88.0 has since raised the cap
to 70000 B, and the comment introducing that bump says the quiet part out
loud: "It is NOT a licence to grow the file... Trim before you add." This is
that trim. The budget is a real runtime cost -- the prompt loads into every
subagent invocation -- so headroom should come from removing duplication,
not from the ceiling.

The bloat was not the version tags: ~95 (since vX.Y.Z) notes are only ~1 KB
in total. It was section 2, the tool selection matrix, at 32 KB -- 52% of
the file -- because its cells had grown into a second manual. The 37 files
in skills/kbagent/references/ already carry that prose; semantic-layer-
workflow.md even opens with a "When to use what" table in the same
intent-to-command shape, making the 11 semantic-layer rows (7.3 KB) near
verbatim duplication.

- section 2: 32044 -> 21092 B. Cells now carry the decision only (command,
  outcome-changing flags, the NEVER list) and point at the matching
  *-workflow.md for the rationale. The semantic-layer block collapses from
  11 rows to 2 (read, and "any write: export first, then read the workflow").
- section 3: 14768 -> 10978 B. Entries whose prose duplicated gotchas.md are
  back to one-line triggers, per the section's own stated design rule.
- Retired version tags whose floor no longer bites. Kept every gate where an
  older version is silently wrong: 0.54.0 plaintext #-secrets, 0.66.1 dormant
  cron, 0.86.0 Azure cipher, 0.87.0 data-app workspace flag.
- Dropped the hand-copied session_unsupported_features list, which the
  surrounding text already tells the agent not to reconstruct from memory --
  auth login --json ships it. Kept the dev-portal and flow list/detail
  carve-outs so the agent does not pre-emptively refuse working commands.

Rule 6 is reworded in the same pass. It used to say every command carries
its own since-tag and to treat those as the authoritative floor; under that
wording a stripped tag would silently read as "safe on any version". It now
states that a tag marks a floor that still bites, that its absence is not a
promise, and that a No such command error is a failed version gate.

The 0.88.0 verification guidance added to the BigQuery repartition row in
#629 is preserved through the rebase: table-detail --json ->
.definition.timePartitioning / .clustering, because create-table only echoes
the layout you requested.

Nothing moved into gotchas.md -- every trimmed block was already documented
there or in a topical workflow file, so no reference file changed.

The freed headroom immediately absorbs the two features that had to skip
this file: config clone (#587) and config state-get/state-set (#593) now
have matrix rows instead of living only in gotchas.md.

47830 B against the 70000 B budget, 22170 free. PROMPT_BYTE_BUDGET itself is
untouched by this PR. All 41 tests in tests/test_agent_prompt.py pass.
padak added a commit that referenced this pull request Aug 21, 2026
[B-1] Wire all five new flags through the `kbagent serve` routers. Each
router docstring claims it "Mirrors" its CLI command, and `search` already
exposed `--regex`, so the 1:1 contract demonstrably covers flags and not just
commands -- leaving these out made those docstrings false and left scheduled
agent tasks (which reach kbagent over REST, not argv) without the parity this
work is about:

- `GET /jobs` gains `offset`, `sort_by`, `sort_order`
- `GET /jobs/{project}/{job_id}` gains `log_tail_lines`
- `GET /search` gains repeatable `scope`
- `GET /storage/tables` gains `include_usage`
- `POST /sharing/{project}/link` body gains `stage`

[NB-1] `_fetch_tables` no longer conflates two meanings in its third tuple
slot. It was `True` without a usage scan and the component listing with one,
forcing the consumer to sniff `len(result) > 2` and widening the annotation to
`Any`. `_run_parallel` tells success from error by tuple LENGTH (base.py:253),
never by that element, so the slot now always carries the component listing
(empty when no scan was asked for) and the consumer destructures directly.

[NB-2] Added E2E coverage for all five flags in `TestE2EMcpParityCommands`:
the live-stack contracts the mocked suite cannot check (Queue API accepting
`sortBy`/`sortOrder`, `logTail` on a real finished job, `used_by` shape from a
real component listing) plus the three exit-2 validation paths. NOT RUN here
-- `make test-e2e` needs E2E_API_TOKEN against a real project.

Rebased onto #629. This branch carries no version bump: main is already at an
unreleased 0.88.0, so the changelog bullets join that entry and the version is
bumped once for the whole batch at release time. The byte-budget trims to
`keboola-expert.md` are reverted -- #629 raised that ceiling to 70 000 B, so
the prose they paid for is affordable again.

make check green against the rebased base: 5761 passed.
padak added a commit that referenced this pull request Aug 21, 2026
[B-1] Wire all five new flags through the `kbagent serve` routers. Each
router docstring claims it "Mirrors" its CLI command, and `search` already
exposed `--regex`, so the 1:1 contract demonstrably covers flags and not just
commands -- leaving these out made those docstrings false and left scheduled
agent tasks (which reach kbagent over REST, not argv) without the parity this
work is about:

- `GET /jobs` gains `offset`, `sort_by`, `sort_order`
- `GET /jobs/{project}/{job_id}` gains `log_tail_lines`
- `GET /search` gains repeatable `scope`
- `GET /storage/tables` gains `include_usage`
- `POST /sharing/{project}/link` body gains `stage`

[NB-1] `_fetch_tables` no longer conflates two meanings in its third tuple
slot. It was `True` without a usage scan and the component listing with one,
forcing the consumer to sniff `len(result) > 2` and widening the annotation to
`Any`. `_run_parallel` tells success from error by tuple LENGTH (base.py:253),
never by that element, so the slot now always carries the component listing
(empty when no scan was asked for) and the consumer destructures directly.

[NB-2] Added E2E coverage for all five flags in `TestE2EMcpParityCommands`:
the live-stack contracts the mocked suite cannot check (Queue API accepting
`sortBy`/`sortOrder`, `logTail` on a real finished job, `used_by` shape from a
real component listing) plus the three exit-2 validation paths. NOT RUN here
-- `make test-e2e` needs E2E_API_TOKEN against a real project.

Rebased onto #629. This branch carries no version bump: main is already at an
unreleased 0.88.0, so the changelog bullets join that entry and the version is
bumped once for the whole batch at release time. The byte-budget trims to
`keboola-expert.md` are reverted -- #629 raised that ceiling to 70 000 B, so
the prose they paid for is affordable again.

make check green against the rebased base: 5761 passed.
padak added a commit that referenced this pull request Aug 21, 2026
[B-1] Wire all five new flags through the `kbagent serve` routers. Each
router docstring claims it "Mirrors" its CLI command, and `search` already
exposed `--regex`, so the 1:1 contract demonstrably covers flags and not just
commands -- leaving these out made those docstrings false and left scheduled
agent tasks (which reach kbagent over REST, not argv) without the parity this
work is about:

- `GET /jobs` gains `offset`, `sort_by`, `sort_order`
- `GET /jobs/{project}/{job_id}` gains `log_tail_lines`
- `GET /search` gains repeatable `scope`
- `GET /storage/tables` gains `include_usage`
- `POST /sharing/{project}/link` body gains `stage`

[NB-1] `_fetch_tables` no longer conflates two meanings in its third tuple
slot. It was `True` without a usage scan and the component listing with one,
forcing the consumer to sniff `len(result) > 2` and widening the annotation to
`Any`. `_run_parallel` tells success from error by tuple LENGTH (base.py:253),
never by that element, so the slot now always carries the component listing
(empty when no scan was asked for) and the consumer destructures directly.

[NB-2] Added E2E coverage for all five flags in `TestE2EMcpParityCommands`:
the live-stack contracts the mocked suite cannot check (Queue API accepting
`sortBy`/`sortOrder`, `logTail` on a real finished job, `used_by` shape from a
real component listing) plus the three exit-2 validation paths. NOT RUN here
-- `make test-e2e` needs E2E_API_TOKEN against a real project.

Rebased onto #629. This branch carries no version bump: main is already at an
unreleased 0.88.0, so the changelog bullets join that entry and the version is
bumped once for the whole batch at release time. The byte-budget trims to
`keboola-expert.md` are reverted -- #629 raised that ceiling to 70 000 B, so
the prose they paid for is affordable again.

make check green against the rebased base: 5761 passed.
padak added a commit that referenced this pull request Aug 21, 2026
[B-1] Wire all five new flags through the `kbagent serve` routers. Each
router docstring claims it "Mirrors" its CLI command, and `search` already
exposed `--regex`, so the 1:1 contract demonstrably covers flags and not just
commands -- leaving these out made those docstrings false and left scheduled
agent tasks (which reach kbagent over REST, not argv) without the parity this
work is about:

- `GET /jobs` gains `offset`, `sort_by`, `sort_order`
- `GET /jobs/{project}/{job_id}` gains `log_tail_lines`
- `GET /search` gains repeatable `scope`
- `GET /storage/tables` gains `include_usage`
- `POST /sharing/{project}/link` body gains `stage`

[NB-1] `_fetch_tables` no longer conflates two meanings in its third tuple
slot. It was `True` without a usage scan and the component listing with one,
forcing the consumer to sniff `len(result) > 2` and widening the annotation to
`Any`. `_run_parallel` tells success from error by tuple LENGTH (base.py:253),
never by that element, so the slot now always carries the component listing
(empty when no scan was asked for) and the consumer destructures directly.

[NB-2] Added E2E coverage for all five flags in `TestE2EMcpParityCommands`:
the live-stack contracts the mocked suite cannot check (Queue API accepting
`sortBy`/`sortOrder`, `logTail` on a real finished job, `used_by` shape from a
real component listing) plus the three exit-2 validation paths. NOT RUN here
-- `make test-e2e` needs E2E_API_TOKEN against a real project.

Rebased onto #629. This branch carries no version bump: main is already at an
unreleased 0.88.0, so the changelog bullets join that entry and the version is
bumped once for the whole batch at release time. The byte-budget trims to
`keboola-expert.md` are reverted -- #629 raised that ceiling to 70 000 B, so
the prose they paid for is affordable again.

make check green against the rebased base: 5761 passed.
padak added a commit that referenced this pull request Aug 21, 2026
* feat: close the remaining keboola-mcp-server parity gaps (0.88.0)

Five read-side flags, each covering a capability the MCP server had and
kbagent did not. Verified against keboola/mcp-server v1.76.2: 41 registered
tools, all now mapped in docs/mcp-migration.md.

- `storage tables --include-usage` -- MCP `get_tables(include_usage=True)`.
  Only storage input/output mappings count as a reference; a table id inside
  a transformation's SQL is text that happens to match, and counting it would
  make "what breaks if I drop this table?" answer with false positives. One
  extra component listing per project, not per table.
- `job detail --log-tail-lines N` -- MCP `get_jobs(include_logs=True)`. The
  log tail previously existed only on the `job run --wait` path, so reading
  the logs of an already-finished job had no CLI route.
- `job list --offset/--sort-by/--sort-order` -- MCP `get_jobs` paging.
- `search --scope PATH` -- MCP `search(scopes=...)`. Scopes are written
  relative to the configuration body; the `configuration.` /
  `rows[N].configuration.` wrapper is normalised away.
- `sharing link --stage in|out` -- MCP `link_shared_bucket(target_stage=...)`.
  The default stays `in` rather than deriving from the source bucket the way
  MCP does: silently relocating where existing scripts' buckets land is the
  worse surprise.

docs/mcp-migration.md gains the two Data Catalog tools it was missing
(`get_shared_buckets`, `link_shared_bucket`) and records the mcp-server
version the map was verified against.

`commands/storage.py` and `services/storage_service.py` are both over their
file-size budget, so the code that would have grown them moved out instead:
`commands/_storage_tables_render.py`, `services/_storage_tables.py` and
`services/table_usage.py`. Both files end up smaller than before.

* fix: address PR #632 review -- REST parity, tuple typing, E2E coverage

[B-1] Wire all five new flags through the `kbagent serve` routers. Each
router docstring claims it "Mirrors" its CLI command, and `search` already
exposed `--regex`, so the 1:1 contract demonstrably covers flags and not just
commands -- leaving these out made those docstrings false and left scheduled
agent tasks (which reach kbagent over REST, not argv) without the parity this
work is about:

- `GET /jobs` gains `offset`, `sort_by`, `sort_order`
- `GET /jobs/{project}/{job_id}` gains `log_tail_lines`
- `GET /search` gains repeatable `scope`
- `GET /storage/tables` gains `include_usage`
- `POST /sharing/{project}/link` body gains `stage`

[NB-1] `_fetch_tables` no longer conflates two meanings in its third tuple
slot. It was `True` without a usage scan and the component listing with one,
forcing the consumer to sniff `len(result) > 2` and widening the annotation to
`Any`. `_run_parallel` tells success from error by tuple LENGTH (base.py:253),
never by that element, so the slot now always carries the component listing
(empty when no scan was asked for) and the consumer destructures directly.

[NB-2] Added E2E coverage for all five flags in `TestE2EMcpParityCommands`:
the live-stack contracts the mocked suite cannot check (Queue API accepting
`sortBy`/`sortOrder`, `logTail` on a real finished job, `used_by` shape from a
real component listing) plus the three exit-2 validation paths. NOT RUN here
-- `make test-e2e` needs E2E_API_TOKEN against a real project.

Rebased onto #629. This branch carries no version bump: main is already at an
unreleased 0.88.0, so the changelog bullets join that entry and the version is
bumped once for the whole batch at release time. The byte-budget trims to
`keboola-expert.md` are reverted -- #629 raised that ceiling to 70 000 B, so
the prose they paid for is affordable again.

make check green against the rebased base: 5761 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.

storage table-detail drops the table definition (BigQuery partitioning/clustering) from the API response

1 participant