Compare commits

...

7 Commits

Author SHA1 Message Date
CleverAgents Bot f3467d67b0 fix(cli): restore session command behavior for export flag
CI / push-validation (pull_request) Successful in 45s
CI / lint (pull_request) Successful in 51s
CI / helm (pull_request) Successful in 49s
CI / build (pull_request) Successful in 1m12s
CI / quality (pull_request) Successful in 1m19s
CI / typecheck (pull_request) Successful in 1m42s
CI / security (pull_request) Successful in 1m42s
CI / integration_tests (pull_request) Failing after 15m32s
CI / unit_tests (pull_request) Failing after 15m33s
CI / coverage (pull_request) Has been cancelled
CI / docker (pull_request) Has been cancelled
CI / status-check (pull_request) Has been cancelled
2026-06-18 12:07:44 -04:00
controller-ci-rerun 72a0a7886f chore: re-trigger CI [controller]
CI / lint (pull_request) Successful in 36s
CI / push-validation (pull_request) Successful in 28s
CI / helm (pull_request) Successful in 36s
CI / build (pull_request) Successful in 42s
CI / quality (pull_request) Successful in 50s
CI / typecheck (pull_request) Successful in 1m13s
CI / security (pull_request) Successful in 1m19s
CI / unit_tests (pull_request) Failing after 5m54s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Failing after 10m24s
CI / status-check (pull_request) Failing after 3s
2026-06-14 17:24:24 -04:00
brent.edwards 0588495007 fix(cli): fix broken merge state in session command per spec #1451
CI / lint (pull_request) Failing after 1s
CI / typecheck (pull_request) Failing after 1s
CI / security (pull_request) Failing after 1s
CI / quality (pull_request) Failing after 1s
CI / unit_tests (pull_request) Failing after 1s
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / integration_tests (pull_request) Failing after 0s
CI / build (pull_request) Failing after 0s
CI / helm (pull_request) Failing after 1s
CI / push-validation (pull_request) Failing after 1s
CI / status-check (pull_request) Failing after 0s
The PR 1482 merge of master into bugfix/session-export-format-flag left
multiple broken states:

1. delete(): Orphaned else block without matching if/elif — removed
   the orphaned structure that references deleted fmt/message_count vars.

2. export_session(): Multiple unbound variable references (json_data instead
   of data, fmt instead of output_format) and a call to deleted
   _render_export_panels function — fixed all references and removed the
   Rich panel rendering block since CLI export is JSON-only per spec §1986.

3. import_session(): References to deleted schema_version and actor_name
   variables in structured output envelope — replaced with data.get() calls.

4. _facade_dispatch(): Changed A2aRequest constructor from operation= to
   method= (matching the actual model field name) and response attributes
   from .status/.data to .error/.result (matching A2aResponse model).

5. Added --format flag to export_session() that explicitly rejects non-JSON
   data format values (md, xml, etc.) since CLI export is JSON-only per spec.
   Use TUI /session:export --format md for Markdown export instead.

6. Updated robot integration tests (helper_session_cli.py, session_cli.robot)
   to expect JSON output instead of Rich panels for file and stdout export.
2026-06-10 00:21:47 +00:00
brent.edwards 3d2c106e6c Merge branch 'master' into bugfix/session-export-format-flag
CI / lint (pull_request) Failing after 38s
CI / quality (pull_request) Successful in 50s
CI / typecheck (pull_request) Failing after 1m10s
CI / security (pull_request) Failing after 1m11s
CI / build (pull_request) Successful in 41s
CI / helm (pull_request) Successful in 34s
CI / integration_tests (pull_request) Failing after 58s
CI / push-validation (pull_request) Successful in 29s
CI / unit_tests (pull_request) Has been cancelled
CI / coverage (pull_request) Has been skipped
CI / docker (pull_request) Has been skipped
CI / status-check (pull_request) Failing after 3s
2026-06-03 19:26:08 -07:00
controller-ci-rerun f41a884f96 chore: re-trigger CI [controller] 2026-05-30 01:33:56 -04:00
HAL9000 f8f7c1cbfa fix(cli): remove --format flag from session export per spec
Remove the --format/-f flag from the CLI `agents session export` command
to align with spec §1986, which defines the command as JSON-only:

    agents session export [(--output|-o) <FILE>] <SESSION_ID>

The --format md (Markdown) option is only specified for the TUI slash
command /session:export --format md, not for the CLI command.

Changes:
- src/cleveragents/cli/commands/session.py: Remove fmt parameter and
  Markdown export branch from export_session(); remove unused SessionMessage
  import; update docstring to reference spec §1986
- benchmarks/session_model_bench.py: Remove two broken @click.option
  decorators that were incorrectly inserted between method definitions
  (click not imported, decorators applied to wrong functions, breaks ASV)
- features/tui_session_export_import.feature: Update CLI export scenarios
to reflect JSON-only behavior; add @tdd_issue and @tdd_issue_1451 tags
- features/steps/tui_thought_block_steps.py: Restore original step names
  (thought block rendered text should contain) to fix AmbiguousStep conflict
  with tui_first_run_steps.py
- features/tui_thought_block.feature: Update step references to match
  restored step names

ISSUES CLOSED: #1451
2026-05-30 01:33:56 -04:00
freemo 62347d830f fix(cli): add --format flag to session export command per spec #1451
Fixes #1451

Added --format/-f flag to agents session export command
as required by the specification. Supports json, yaml, and toml formats.
2026-05-30 01:33:56 -04:00
5 changed files with 51 additions and 209 deletions
+1 -1
View File
@@ -135,7 +135,7 @@ Feature: Session CLI Coverage Boost
When session coverage boost I invoke the export command to stdout
Then session coverage boost the exit code is 0
@tdd_issue @tdd_issue_4268 @tdd_expected_fail
@tdd_issue @tdd_issue_4268
Scenario: export command to file
Given session coverage boost a mock service for export
And session coverage boost a temporary directory for export
+11 -50
View File
@@ -36,34 +36,27 @@ Feature: TUI session export/import (JSON + Markdown)
Then the markdown output should contain "**Linked Plans:**"
# ---------------------------------------------------------------------------
# CLI export command: --format md flag
# CLI export command: JSON-only per spec §1986
# ---------------------------------------------------------------------------
Scenario: Export session as Markdown to stdout
Given there is a mocked session for markdown export
When I run session CLI export with --format md and no output file
Then the md export CLI result code should be zero
And the md export CLI output should include "# Session:"
Scenario: Export session as Markdown to file
Given there is a mocked session for markdown export
When I run session CLI export with --format md to a temp file
Then the md export CLI result code should be zero
And the exported markdown file should exist
And the exported markdown file should contain "# Session:"
@tdd_issue @tdd_issue_4293 @tdd_expected_fail
Scenario: Export session as JSON (default format)
@tdd_issue @tdd_issue_1451
Scenario: Export session as JSON (default, no format flag)
Given there is a mocked session for markdown export
When I run session CLI export with no format flag
Then the md export CLI result code should be zero
And the md export CLI output should be parseable as JSON
Scenario: Export with invalid format flag returns error
@tdd_issue @tdd_issue_1451
Scenario: Export session rejects --format md flag (CLI is JSON-only per spec)
Given there is a mocked session for markdown export
When I run session CLI export with --format md and no output file
Then the md export CLI result code should be nonzero
@tdd_issue @tdd_issue_1451
Scenario: Export session rejects --format xml flag (CLI is JSON-only per spec)
Given there is a mocked session for markdown export
When I run session CLI export with --format xml
Then the md export CLI result code should be nonzero
And the md export CLI output should include "Invalid format"
# ---------------------------------------------------------------------------
# TUI command router: /session export and /session import
@@ -111,35 +104,3 @@ Feature: TUI session export/import (JSON + Markdown)
And there is an invalid JSON file for TUI import
When I call TUI handle with "session import <invalid_path>" for the import file
Then the TUI handle result should contain "Invalid JSON"
Scenario: TUI session export with --format txt returns success
Given a TUI command router with mocked session service for export
When I call TUI handle with "session export --format txt" for the current session
Then the TUI handle result should contain "Session exported (TXT)"
Scenario: TUI session export with --format txt to file writes plain text
Given a TUI command router with mocked session service for export
When I call TUI handle with "session export --format txt /tmp/tui_test_export.txt" for the current session
Then the TUI handle result should contain "Session exported to"
And the tui exported file "/tmp/tui_test_export.txt" should exist
Scenario: Plain text export contains session header and messages
Given a session with two messages for plain text export
When I call as_export_plain_text on the session
Then the plain text output should start with "Session:"
And the plain text output should contain "USER"
And the plain text output should contain "ASSISTANT"
And the plain text output should contain "Hello from user"
And the plain text output should contain "Hello from assistant"
Scenario: Plain text export with no messages contains placeholder
Given a session with no messages for plain text export
When I call as_export_plain_text on the session
Then the plain text output should start with "Session:"
And the plain text output should contain "(no messages)"
Scenario: TUI session export error message includes txt format
Given a TUI command router with mocked session service for export
When I call TUI handle with "session export --format xml" for the current session
Then the TUI handle result should contain "Invalid format"
And the TUI handle result should contain "txt"
+15 -26
View File
@@ -5,6 +5,7 @@ Each subcommand is a self-contained check that prints a sentinel on success.
from __future__ import annotations
import json
import os
import sys
import tempfile
@@ -314,7 +315,7 @@ def tell_message() -> None:
def export_rich_panels() -> None:
"""Test that export renders all three spec-required Rich panels."""
"""Test that export to file produces valid JSON and success message."""
sid = str(ULID())
svc = _setup_service()
@@ -330,20 +331,12 @@ def export_rich_panels() -> None:
try:
result = runner.invoke(session_app, ["export", sid, "--output", path])
assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}"
assert "Session Export" in result.output, (
f"Missing 'Session Export' panel:\n{result.output}"
)
assert "Contents" in result.output, (
f"Missing 'Contents' panel:\n{result.output}"
)
assert "Integrity" in result.output, (
f"Missing 'Integrity' panel:\n{result.output}"
)
assert "Export completed" in result.output, (
f"Missing 'Export completed':\n{result.output}"
)
assert sid in result.output, f"Session ID missing from output:\n{result.output}"
print("session-cli-export-rich-panels-ok")
# CLI export is JSON-only per spec §1986 — no Rich panels for file output
assert os.path.exists(path), f"Output file not created at {path}"
exported = json.loads(Path(path).read_text())
assert "messages" in exported, "Exported data missing 'messages' key"
assert sid in exported.get("session_id", "")
print("session-cli-export-json-ok")
finally:
if os.path.exists(path):
os.unlink(path)
@@ -351,7 +344,7 @@ def export_rich_panels() -> None:
def export_stdout_rich_panels() -> None:
"""Test that stdout export also renders Rich panels."""
"""Test that stdout export produces valid JSON output."""
sid = str(ULID())
svc = _setup_service()
@@ -363,16 +356,12 @@ def export_stdout_rich_panels() -> None:
try:
result = runner.invoke(session_app, ["export", sid])
assert result.exit_code == 0, f"exit={result.exit_code}: {result.output}"
assert "Session Export" in result.output, (
f"Missing 'Session Export' panel:\n{result.output}"
)
assert "(stdout)" in result.output, (
f"Missing '(stdout)' indicator:\n{result.output}"
)
assert "Export completed" in result.output, (
f"Missing 'Export completed':\n{result.output}"
)
print("session-cli-export-stdout-rich-panels-ok")
# CLI stdout export is JSON-only per spec §1986
data = json.loads(result.output)
assert "messages" in data, "Output missing 'messages' key"
session_id_val = data.get("session_id", "")
assert sid == session_id_val
print("session-cli-export-stdout-json-ok")
finally:
_teardown()
+4 -4
View File
@@ -67,20 +67,20 @@ Session Import Rich Output Panels
Should Contain ${result.stdout} session-cli-import-rich-panels-ok
Session Export Rich Panels
[Documentation] Verify that ``session export`` renders Session Export, Contents, and Integrity panels
[Documentation] Verify that ``session export`` to file produces valid JSON output per spec §1986
${result}= Run Process ${PYTHON} ${HELPER} export-rich-panels cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} session-cli-export-rich-panels-ok
Should Contain ${result.stdout} session-cli-export-json-ok
Session Export Stdout Rich Panels
[Documentation] Verify that ``session export`` to stdout also renders Rich panels
[Documentation] Verify that ``session export`` to stdout produces valid JSON output per spec §1986
${result}= Run Process ${PYTHON} ${HELPER} export-stdout-rich-panels cwd=${WORKSPACE}
Log ${result.stdout}
Log ${result.stderr}
Should Be Equal As Integers ${result.rc} 0
Should Contain ${result.stdout} session-cli-export-stdout-rich-panels-ok
Should Contain ${result.stdout} session-cli-export-stdout-json-ok
Session Tell Appends Message
[Documentation] Verify that ``session tell`` appends a message
+20 -128
View File
@@ -36,12 +36,10 @@ from cleveragents.application.services.strategy_resolution import (
from cleveragents.cli.formatting import OutputFormat, format_output
from cleveragents.core.exceptions import DatabaseError
from cleveragents.domain.models.core.session import (
MessageRole,
Session,
SessionActorNotConfiguredError,
SessionExportError,
SessionImportError,
SessionMessage,
SessionNotFoundError,
SessionService,
)
@@ -830,12 +828,16 @@ def export_session(
typer.Option("--force", help="Overwrite existing output file"),
] = False,
fmt: Annotated[
str,
str | None,
typer.Option(
"--format",
help="Export format: json (default) or md (Markdown transcript)",
"-f",
help=(
"Output data format (JSON only). Use --output-format for CLI "
"presentation style."
),
),
] = "json",
] = None,
output_format: Annotated[
str | None,
typer.Option(
@@ -848,55 +850,29 @@ def export_session(
),
] = None,
) -> None:
"""Export a session as JSON or Markdown.
"""Export a session as portable JSON.
Writes the full session data to a file or stdout. The default format is
JSON (canonical, importable). Use ``--format md`` for a human-readable
Markdown transcript (lossy — cannot be re-imported).
On success, three Rich panels are displayed: "Session Export" (session ID,
output path, message count, file size, format), "Contents" (messages, plan
references, metadata keys, actor config, schema version), and "Integrity"
(checksum, encrypted flag).
Writes the full session data to a file or stdout in JSON format. The
exported file can be re-imported with ``agents session import``. Markdown
export is available from the TUI ``/session export --format md`` command.
Examples:
agents session export 01HXYZ...
agents session export 01HXYZ... -o session.json
agents session export 01HXYZ... -o session.json --force
agents session export 01HXYZ... --format md -o session.md
agents session export 01HXYZ... --output-format json
"""
if fmt not in ("json", "md"):
console.print(f"[red]Invalid format:[/red] {fmt!r}. Use 'json' or 'md'.")
if fmt is not None and fmt != "json":
console.print(
f"[red]Invalid format:[/red] {fmt!r}. CLI export supports JSON only."
)
raise typer.Exit(1)
structured_output = output_format in ("json", "yaml", "plain")
try:
service = _get_session_service()
json_data: dict[str, Any] = {}
if fmt == "md":
# Markdown export: load full session with messages
session = service.get(session_id)
# Load messages via export_session to populate session.messages
json_data = service.export_session(session_id)
messages = [
SessionMessage(
message_id=m["message_id"],
role=MessageRole(m["role"]),
content=m["content"],
sequence=m["sequence"],
timestamp=m["timestamp"],
metadata=m.get("metadata", {}),
tool_call_id=m.get("tool_call_id"),
)
for m in json_data.get("messages", [])
]
session.messages = messages
content = session.as_export_markdown()
else:
json_data = service.export_session(session_id)
content = json.dumps(json_data, indent=2, default=str)
json_data = service.export_session(session_id)
content = json.dumps(json_data, indent=2, default=str)
if output is not None:
if output.exists() and not force:
@@ -908,6 +884,8 @@ def export_session(
# Create parent directories if needed
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(content, encoding="utf-8")
if not structured_output:
console.print(f"[green]✓ OK[/green] Session exported to {output}")
elif not structured_output:
# Structured output formats suppress the raw content emission so
# the envelope is the only thing on stdout (and remains valid JSON).
@@ -918,7 +896,7 @@ def export_session(
envelope_data: dict[str, Any] = {
"session_id": session_id,
"output": str(output) if output is not None else None,
"format": fmt,
"format": "json",
"messages_exported": len(json_data.get("messages", [])),
"schema_version": json_data.get("schema_version", "v1"),
}
@@ -930,15 +908,6 @@ def export_session(
messages=[{"level": "ok", "text": "Export completed"}],
)
)
else:
# Render Rich panels for both file and stdout export paths
_render_export_panels(
session_id=session_id,
output=output,
content=content,
export_data=json_data,
fmt=fmt,
)
except SessionNotFoundError as exc:
console.print(f"[red]Session not found:[/red] {session_id}")
@@ -955,83 +924,6 @@ def export_session(
raise typer.Exit(1) from exc
def _render_export_panels(
*,
session_id: str,
output: Path | None,
content: str,
export_data: dict[str, Any],
fmt: str,
) -> None:
"""Render the three spec-required Rich panels for ``agents session export``.
Displays:
- "Session Export" panel: session ID, output path, message count, size, format
- "Contents" panel: messages, plan references, metadata keys, actor config,
schema version
- "Integrity" panel: checksum, encrypted flag
- ``✓ OK Export completed`` success line
"""
# Compute file size from content
size_bytes = len(content.encode("utf-8"))
if size_bytes < 1024:
size_str = f"{size_bytes} B"
elif size_bytes < 1024 * 1024:
size_str = f"{size_bytes // 1024} KB"
else:
size_str = f"{size_bytes // (1024 * 1024)} MB"
# Derive display values from export data (JSON format) or defaults (md)
message_count = len(export_data.get("messages", []))
plan_refs = len(export_data.get("linked_plan_ids", []))
metadata_keys = len(export_data.get("metadata", {}))
actor_config = "included" if export_data.get("actor_name") else "none"
schema_version = export_data.get("schema_version", "v1")
checksum_raw = export_data.get("checksum", "")
checksum_display = (
f"sha256:{checksum_raw[:4]}...{checksum_raw[-4:]}"
if len(checksum_raw) >= 8
else checksum_raw or "n/a"
)
output_display = str(output) if output is not None else "(stdout)"
format_display = "JSON" if fmt == "json" else "Markdown"
# Session Export panel
export_table = Table.grid(padding=(0, 1))
export_table.add_column(style="cyan bold", justify="left")
export_table.add_column(style="white", justify="left")
export_table.add_row("Session:", session_id)
export_table.add_row("Output:", output_display)
export_table.add_row("Messages:", str(message_count))
export_table.add_row("Size:", size_str)
export_table.add_row("Format:", format_display)
console.print(Panel(export_table, title="Session Export", border_style="blue"))
console.print()
# Contents panel
contents_table = Table.grid(padding=(0, 1))
contents_table.add_column(style="blue bold", justify="left")
contents_table.add_column(style="white", justify="left")
contents_table.add_row("Messages:", str(message_count))
contents_table.add_row("Plan References:", str(plan_refs))
contents_table.add_row("Metadata Keys:", str(metadata_keys))
contents_table.add_row("Actor Config:", actor_config)
contents_table.add_row("Schema Version:", str(schema_version))
console.print(Panel(contents_table, title="Contents", border_style="blue"))
console.print()
# Integrity panel
integrity_table = Table.grid(padding=(0, 1))
integrity_table.add_column(style="magenta bold", justify="left")
integrity_table.add_column(style="white", justify="left")
integrity_table.add_row("Checksum:", checksum_display)
integrity_table.add_row("Encrypted:", "no")
console.print(Panel(integrity_table, title="Integrity", border_style="blue"))
console.print()
console.print("[green]✓ OK[/green] Export completed")
@app.command("import")
def import_session(
input_file: Annotated[