Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"plugins": [
{
"name": "kbagent",
"version": "0.43.5",
"version": "0.43.6",
"source": "./plugins/kbagent",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"category": "development"
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ kbagent search QUERY [--project NAME] [--type table|bucket|config|flow|data-app|

kbagent job list [--project NAME] [--component-id ID] [--status STATUS] [--limit N]
kbagent job detail --project NAME --job-id ID
kbagent job run --project NAME --component-id ID --config-id ID [--row-id ID ...] [--wait] [--timeout N] [--branch ID] [--variable-values-id ID] [--no-variables] [--poll-strategy exponential|fixed] [--log-tail-lines N]
kbagent job run --project NAME --component-id ID --config-id ID [--row-id ID ...] [--wait] [--timeout N] [--branch ID] [--mode run|debug] [--variable-values-id ID] [--no-variables] [--poll-strategy exponential|fixed] [--log-tail-lines N]
kbagent job terminate --project NAME (--job-id ID [--job-id ID ...] | --status any|created|waiting|processing [--component-id ID] [--config-id ID] [--branch ID] [--limit N]) [--dry-run] [--yes]

kbagent storage buckets [--project NAME] [--branch ID]
Expand Down
2 changes: 1 addition & 1 deletion plugins/kbagent/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "kbagent",
"version": "0.43.5",
"version": "0.43.6",
"description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces",
"author": {
"name": "Keboola",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ All seven commands authenticate via `KBC_MANAGE_API_TOKEN` (Manage API), not the
## Job History
- `job list [--project NAME] [--component-id ID] [--config-id ID] [--status STATUS] [--limit N]` -- list jobs (default 50, max 500)
- `job detail --project NAME --job-id ID` -- full job detail with timing and result message
- `job run --project NAME --component-id ID --config-id ID [--row-id ID ...] [--wait] [--timeout N] [--branch ID] [--variable-values-id ID] [--no-variables] [--poll-strategy exponential|fixed] [--log-tail-lines N]` -- run a job, optionally wait for completion (branch-aware). For configs with linked `keboola.variables` (root-level `configuration.variables_id`), kbagent auto-resolves a `variableValuesId` so transformations bind to the deployed values row. `--variable-values-id` overrides; `--no-variables` skips resolution. `NO_VARIABLE_ROWS` when the linked variables config has zero rows -- fix via `kbagent config variables-set`. Under `--wait`, polls with an exponential curve (2s x 30 -> 5s x 48 -> 15s); `--poll-strategy fixed` keeps a constant 1s interval. On FAILED/WARNING/TERMINATED, the last `--log-tail-lines` events (default 200, **0 disables -- recommended for automation pipelines**) are attached as `logTail` in the JSON result (or `details.logTail` on errors). If `--timeout` expires, kbagent issues `kill_job` on the remote and exits **7** (`JOB_TIMEOUT_TERMINATED`) with the cancelled `details.job` + `details.logTail`; if the kill itself fails, exits **4** (`QUEUE_JOB_TIMEOUT`, `retryable=true`). Use jq pattern `.error.details.logTail? // .data.logTail? // []` to pick up the tail regardless of exit code.
- `job run --project NAME --component-id ID --config-id ID [--row-id ID ...] [--wait] [--timeout N] [--branch ID] [--mode run|debug] [--variable-values-id ID] [--no-variables] [--poll-strategy exponential|fixed] [--log-tail-lines N]` -- run a job, optionally wait for completion (branch-aware). For configs with linked `keboola.variables` (root-level `configuration.variables_id`), kbagent auto-resolves a `variableValuesId` so transformations bind to the deployed values row. `--variable-values-id` overrides; `--no-variables` skips resolution. `NO_VARIABLE_ROWS` when the linked variables config has zero rows -- fix via `kbagent config variables-set`. `--mode debug` (since v0.43.6) sets the Queue API job `mode` body field to `"debug"`: the component runs with the same configuration + inputs but the worker redirects output to a Storage File tagged `debug-<jobId>` instead of writing to destination buckets. Use for dry-runs, reproducing a failing job on a production config without touching downstream tables, or harvesting the worker's output bytes (download via `storage file-download --tag debug-<jobId>`) to feed into VCR fixtures or component test cases. Default `--mode run` is unchanged. Invalid values (`--mode anything-else`) exit 2 at the Click choice gate before any wire call. Under `--wait`, polls with an exponential curve (2s x 30 -> 5s x 48 -> 15s); `--poll-strategy fixed` keeps a constant 1s interval. On FAILED/WARNING/TERMINATED, the last `--log-tail-lines` events (default 200, **0 disables -- recommended for automation pipelines**) are attached as `logTail` in the JSON result (or `details.logTail` on errors). If `--timeout` expires, kbagent issues `kill_job` on the remote and exits **7** (`JOB_TIMEOUT_TERMINATED`) with the cancelled `details.job` + `details.logTail`; if the kill itself fails, exits **4** (`QUEUE_JOB_TIMEOUT`, `retryable=true`). Use jq pattern `.error.details.logTail? // .data.logTail? // []` to pick up the tail regardless of exit code.
- `job terminate --project NAME (--job-id ID [--job-id ...] | --status any|created|waiting|processing [--component-id ID] [--config-id ID] [--branch ID] [--limit N]) [--dry-run] [--yes]` -- kill running Queue API jobs. Use to stop runaway loops or clean up pile-ups from repeated `job run` calls. Two modes: by ID (single/batch) or by filter (`--status any` catches every killable state). Response partitions IDs into `killed / already_finished / not_found / failed`; safe to re-run idempotently. Kill is async -- poll `job detail` for `isFinished=true`.

## Storage
Expand Down
36 changes: 36 additions & 0 deletions plugins/kbagent/skills/kbagent/references/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,42 @@ limit, transient 5xx), the detail call still succeeds and
`storage_workspace_id` is set to `null` -- the annotation is UX, not a
contract.

## `kbagent job run --mode debug` redirects output to a Storage File, not the destination buckets (since v0.43.6)

`kbagent job run` accepts `--mode run|debug` (default `run`). The flag is
threaded straight into the Queue API job-creation body as `"mode": "..."`;
`run` is the unchanged historical wire shape. `--mode debug` flips the Queue
worker into debug execution: the component starts with the same configuration
and inputs as a normal run, but its output stream is **not** written to the
configured destination buckets. Instead the worker uploads the output bytes
to a Storage File tagged `debug-<jobId>` (you can pull it down with
`kbagent storage file-download --tag debug-<jobId>`). Useful for:

- **Dry-runs against production configs** -- reproduce a failing job exactly
as it happened in prod without touching downstream tables, and inspect the
worker's actual output bytes after the fact.
- **Seeding component test fixtures** -- harvest the debug output file as
ground-truth input for a new VCR recording or a component test case (the
intended use case that drove this flag).
- **Validating a config change before merging** -- run the new config in
debug mode, diff the output file against the previous debug file, only
promote once the diff is acceptable.

The CLI gates the flag with `click.Choice` so a typo like `--mode dry-run`
exits 2 with a Click usage error before any wire call -- it cannot reach the
Queue API and surface as an opaque 422. Service-layer also validates against
`VALID_JOB_MODES` so direct programmatic callers get a
`KeboolaApiError(INVALID_ARGUMENT)` rather than a silent passthrough.

The human-mode `Running ...` banner appends a bold-yellow `mode=debug` chip
when the flag is non-default, so operators see at a glance that a run is
diagnostic, not production. `--json` output shape is unchanged (the mode is
visible on the returned job dict via the Queue API's own response shape).

Before this release the `mode` parameter existed on `KeboolaClient.create_job`
but neither `JobService.run_job` nor `commands/job.py` exposed it, so every
job created via `kbagent` hard-coded `mode: "run"` on the wire.

## Metastore duplicate-name POST returns 409 OR 500 -- both map to `ALREADY_EXISTS` (since v0.43.5)

`MetastoreClient.post_item` normalises **both** the post-go-monorepo-PR#513
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "keboola-agent-cli"
version = "0.43.5"
version = "0.43.6"
description = "AI-friendly CLI for managing Keboola projects"
readme = "README.md"
requires-python = ">=3.12"
Expand Down
3 changes: 3 additions & 0 deletions src/keboola_agent_cli/changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

# Ordered newest-first. Each value is a list of brief one-line descriptions.
CHANGELOG: dict[str, list[str]] = {
"0.43.6": [
'New: `kbagent job run --mode run|debug` exposes the Queue API job `mode` body field, which was previously hard-coded to `"run"` inside `JobService.run_job` (the underlying `KeboolaClient.create_job` already accepted a `mode` kwarg but no service-layer or CLI path threaded it through). `--mode debug` flips the Queue worker into debug mode -- the component runs with the same configuration and inputs as a normal run, but its output stream is redirected into a Storage File tagged `debug-<jobId>` instead of into the destination buckets, so the run is safe to repeat on a production configuration for diagnostics (reproducing a failure, capturing the worker\'s actual output, A/B-comparing a flag change) without touching downstream tables. Default behaviour is unchanged: omit `--mode` and the body still carries `"mode": "run"`, the same wire shape every prior release sent. Validation lives at the service boundary (`KeboolaApiError` with `INVALID_ARGUMENT`) and the CLI also gates the flag with `click.Choice(sorted(VALID_JOB_MODES))`, so a typo (`--mode dry-run`) exits 2 with a Click usage error before any network round-trip -- it cannot reach the wire and surface as an opaque Queue API 422. The human-mode \'Running ...\' banner appends a bold-yellow `mode=debug` chip when the flag is non-default so operators see at a glance that a run is diagnostic, not production. The hint surface (`kbagent --hint client job run` / `--hint service`) emits the new `mode="..."` kwarg on both the `create_job` and `JobService.run_job` calls so AI agents that copy the rendered Python see the parameter inline. New constants `VALID_JOB_MODES = frozenset({"run", "debug"})` and `DEFAULT_JOB_MODE = "run"` in `constants.py`. Tests: 3 new service-layer tests in `test_services.py::TestJobServiceRunJobMode` (default lands as `mode="run"`, opt-in `mode="debug"` forwarded, unknown mode rejected at the service boundary and never reaches the wire), 3 new CLI tests in `test_cli.py::TestJobRun` (default, `--mode debug` forwarded, `--mode dry-run` exits 2 via the Click choice gate), and 2 new client-layer tests in `test_client.py::TestCreateJob` (`mode="run"` is in the POST /jobs body by default, `mode="debug"` reaches the body verbatim). Existing four `create_job.assert_called_once_with` assertions in `test_services.py` updated to include `mode="run"` since the call signature now always passes it. Plugin sync surfaces (silent-drift risks per convention #17): `CLAUDE.md ## All CLI Commands`, `commands/context.py::AGENT_CONTEXT`, `plugins/kbagent/skills/kbagent/references/commands-reference.md`, and `plugins/kbagent/skills/kbagent/references/gotchas.md` (new `(since v0.43.6)` entry) all updated so AI agents on the new version recommend `--mode debug` correctly.',
],
"0.43.5": [
"Fix: `MetastoreClient.post_item` now accepts both HTTP 409 (post go-monorepo PR #513) and the legacy HTTP 500 + `\"Failed to create meta object\"` body as the duplicate-name signal, normalising both into `ErrorCode.ALREADY_EXISTS` so command-layer error mapping (and the human-mode `'X with name Y already exists ...'` message) stays consistent across stacks during the metastore rollout. Before this release the workaround only matched the 500 shape; against a post-fix metastore the proper 409 Conflict would have bubbled up as a generic `API_ERROR` 'API error 409 ...' wrapper, blowing past the clean `ALREADY_EXISTS` path that command-layer error renderers special-case. Side benefit: 409 is not in `RETRYABLE_STATUS_CODES` (`constants.py`), so duplicate-name POSTs against a post-fix metastore stop being retried `MAX_RETRIES` times before the normalisation fires -- one round-trip instead of three. The 500 substring check is retained so unrelated 500s (DB outage, etc.) still surface as retryable `API_ERROR` rather than being miscategorised as a name collision. PATCH on a missing UUID is also fixed upstream (returns 404 now), but `BaseHttpClient._handle_error` already maps 404 -> `ErrorCode.NOT_FOUND`, so no client change is needed for that path. Docstring on `metastore_client.py` updated to describe both server-side shapes. New test in `test_metastore_client.py::TestDuplicateNameNormalization::test_duplicate_name_409_becomes_already_exists` registers a single 409 (asserting no retry happens) and verifies `error_code=ALREADY_EXISTS`, `status_code=409`, `retryable=False`, and the canonical user-facing message; the existing 500 + unrelated-500 tests stay green.",
"Plugin docs: `plugins/kbagent/skills/kbagent/references/semantic-layer-workflow.md` documents the dual 409+500 shape so AI agents recommending raw HTTP calls (vs the `kbagent semantic-layer ...` group) know what shape to expect during the metastore rollout. No CLI surface change.",
Expand Down
6 changes: 5 additions & 1 deletion src/keboola_agent_cli/commands/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@
kbagent job detail --project NAME --job-id ID
Full job detail including result message and timing.

kbagent job run --project NAME --component-id ID --config-id ID [--row-id ID ...] [--wait] [--timeout N] [--branch ID] [--variable-values-id ID] [--no-variables] [--poll-strategy exponential|fixed] [--log-tail-lines N]
kbagent job run --project NAME --component-id ID --config-id ID [--row-id ID ...] [--wait] [--timeout N] [--branch ID] [--mode run|debug] [--variable-values-id ID] [--no-variables] [--poll-strategy exponential|fixed] [--log-tail-lines N]
Run a Queue API job. --row-id selects specific config rows (repeatable; omit to run entire config).
--wait polls until job finishes. --timeout sets max wait in seconds (default 300). Branch-aware.
When the config has linked variables (configuration.variables_id), kbagent auto-resolves
Expand All @@ -274,6 +274,10 @@
--poll-strategy fixed keeps a constant 1s interval. On FAILED/WARNING/TERMINATED, the last
--log-tail-lines events (default 200, 0 disables -- recommended for automation pipelines) are
surfaced as `logTail` in --json output.
--mode run (default) writes to mapped output tables. --mode debug runs the component but
redirects the output to a Storage File tagged `debug-<jobId>` instead of into destination
buckets -- safe for dry-runs and for reproducing a failing run on a production configuration
without touching production data. Invalid values exit 2 via Click choice gate (since v0.43.6).
--json response shapes by exit code:
- exit 0 (success): {{status:"ok", data:{{..., logTail?:[...]}}}}
- exit 1 (QUEUE_JOB_FAILED, remote job status=error):
Expand Down
19 changes: 19 additions & 0 deletions src/keboola_agent_cli/commands/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
from ..config_store import ConfigStore
from ..constants import (
DEFAULT_JOB_LIMIT,
DEFAULT_JOB_MODE,
DEFAULT_JOB_RUN_TIMEOUT,
DEFAULT_LOG_TAIL_LINES,
DEFAULT_POLL_STRATEGY,
KILLABLE_JOB_STATUSES,
MAX_JOB_LIMIT,
MAX_LOG_TAIL_LINES,
VALID_JOB_MODES,
VALID_POLL_STRATEGIES,
VALID_STATUSES,
)
Expand Down Expand Up @@ -197,6 +199,19 @@ def job_run(
"--branch",
help="Dev branch ID (overrides active branch)",
),
mode: str = typer.Option(
DEFAULT_JOB_MODE,
"--mode",
click_type=click.Choice(sorted(VALID_JOB_MODES)),
help=(
"Queue API job mode. 'run' (default) executes the component "
"normally and writes to mapped output tables. 'debug' executes "
"the component but redirects its output to a Storage File tagged "
"'debug-<jobId>' instead of the destination buckets -- safe for "
"dry-runs and for reproducing a failing run on production "
"configuration without touching production data."
),
),
variable_values_id: str | None = typer.Option(
None,
"--variable-values-id",
Expand Down Expand Up @@ -271,6 +286,7 @@ def job_run(
wait=wait,
timeout=timeout,
branch=branch,
mode=mode,
variable_values_id=variable_values_id,
no_variables=no_variables,
poll_strategy=poll_strategy,
Expand Down Expand Up @@ -323,6 +339,8 @@ def job_run(
msg += f" (rows: {', '.join(row_id)})"
if effective_branch is not None:
msg += f" on branch [cyan]{effective_branch}[/cyan]"
if mode != DEFAULT_JOB_MODE:
msg += f" [bold yellow]mode={mode}[/bold yellow]"
if wait:
msg += f" [dim](waiting up to {timeout:.0f}s)[/dim]"
msg += "..."
Expand All @@ -343,6 +361,7 @@ def job_run(
no_variables=no_variables,
poll_strategy=poll_strategy,
log_tail_lines=log_tail_lines,
mode=mode,
)
except ConfigError as exc:
formatter.error(message=exc.message, error_code=ErrorCode.CONFIG_ERROR)
Expand Down
7 changes: 7 additions & 0 deletions src/keboola_agent_cli/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@
)
VALID_POLL_STRATEGIES: frozenset[str] = frozenset({"exponential", "fixed"})
DEFAULT_POLL_STRATEGY: str = "exponential"
# Queue API "mode" body field for job creation. "run" is the normal execution
# path (writes to mapped output tables); "debug" runs the component the same
# way but redirects the output to a Storage File tagged "debug-" + the job id
# instead of into destination buckets. Useful for dry-runs and reproducing
# failures without touching production tables.
VALID_JOB_MODES: frozenset[str] = frozenset({"run", "debug"})
DEFAULT_JOB_MODE: str = "run"
# Default log-tail length surfaced on FAILED/WARNING/TERMINATED jobs.
DEFAULT_LOG_TAIL_LINES: int = 200
# Upper bound to prevent accidentally pulling tens of thousands of events
Expand Down
2 changes: 2 additions & 0 deletions src/keboola_agent_cli/hints/definitions/job.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"config_row_ids": "{row_id}",
"branch_id": "{branch}",
"variable_values_id": 'var_rows[0]["id"] if var_rows else None',
"mode": "{mode}",
},
result_var="job",
result_hint="dict",
Expand All @@ -133,6 +134,7 @@
"no_variables": "{no_variables}",
"poll_strategy": "{poll_strategy}",
"log_tail_lines": "{log_tail_lines}",
"mode": "{mode}",
},
),
),
Expand Down
4 changes: 3 additions & 1 deletion src/keboola_agent_cli/server/routers/jobs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from pydantic import BaseModel
from sse_starlette.sse import EventSourceResponse

from ...constants import DEFAULT_LOG_TAIL_LINES, DEFAULT_POLL_STRATEGY
from ...constants import DEFAULT_JOB_MODE, DEFAULT_LOG_TAIL_LINES, DEFAULT_POLL_STRATEGY
from ..dependencies import ServiceRegistry, get_registry
from ..sse import json_event

Expand All @@ -24,6 +24,7 @@ class JobRun(BaseModel):
branch_id: int | None = None
variable_values_id: str | None = None
no_variables: bool = False
mode: str = DEFAULT_JOB_MODE


class JobTerminate(BaseModel):
Expand Down Expand Up @@ -86,6 +87,7 @@ def run(
no_variables=body.no_variables,
poll_strategy=poll_strategy,
log_tail_lines=log_tail_lines,
mode=body.mode,
)


Expand Down
Loading
Loading