Uh oh!
There was an error while loading. Please reload this page.
fix(cli): show non-ASCII workflow inputs literally in verbose output - #391
Conversation
Jason Robert (jrob5756)
left a comment
There was a problem hiding this comment.
Nice find here. I reproduced #371 on main (175 KB of link-heavy prompt: 63s, 993 MB) and this branch renders the same input in about 0.3s. The file-log half is a real improvement the issue never even asked for, since Text(content) makes the --log-file artifact a faithful copy for the first time.
My one substantive worry is Markdown on the console. Rich's Markdown renderer rewrites content rather than escaping it, and some of what it discards matters here. A prompt wrapped in XML-ish tags with no blank lines renders as an empty panel. That is not hypothetical: skills/loader.py wraps injected skills in <skills>/<skill> tags and executor/agent.py prepends that before this logs it, so on claude and hermes the console shows a prompt with every injected skill missing. Link URLs vanish too, which is the exact content class from the issue's own reproduction.
rich.text.Text closes #371 just as completely, measured about 5x faster and 3x lighter, and it keeps the panel honest. It also makes the ```json fence unnecessary. That was option 1 in the issue, and I think it is the better trade. Details inline.
| _verbose_console.print( | ||
| Panel(Markdown(content), title=f"[cyan]{title}[/cyan]", border_style="dim") | ||
| ) |
There was a problem hiding this comment.
Rich's Markdown renderer does not just neutralise the brackets, it rewrites the content. Confirmed to disappear from the panel: HTML/XML blocks, link URLs ([text](url) renders as bare text), leading indentation, and _underscores_. A tag-structured prompt with no blank lines renders as a completely empty box.
That last one bites in practice. skills/loader.py:147 wraps injected skills in <skills>/<skill name=...>, and executor/agent.py prepends that to the prompt before this line logs it, so on claude and hermes the console silently shows the prompt minus every injected skill.
Text avoids markup parsing just as completely (0.22s / 2.9 MB against 1.15s / 9.0 MB for a 175 KB prompt) and shows what was actually sent. If you take this, also drop the now-unused Markdown import on line 370 and the fence on line 1713.
If you would rather keep Markdown, then the content: entry in the docstring needs to say so, because content has quietly become Markdown source and executor/agent.py passes arbitrary prompt text straight into it.
| _verbose_console.print( | |
| Panel(Markdown(content), title=f"[cyan]{title}[/cyan]", border_style="dim") | |
| ) | |
| _verbose_console.print( | |
| Panel(Text(content), title=f"[cyan]{title}[/cyan]", border_style="dim") | |
| ) |
| Panel(Markdown(content), title=f"[cyan]{title}[/cyan]", border_style="dim") | ||
| ) | ||
| # File always gets full untruncated content |
There was a problem hiding this comment.
Minor archaeology, worth fixing while you are in here. Commit 87476b7 deleted the truncate parameter and added this comment in the same change, so the "untruncated" contrast has never had anything to contrast against. Nothing in this function truncates the console.
What is true now is a fidelity difference, and it is exactly what the Text on the next line buys you.
| # File always gets full untruncated content | |
| # File sink is written even when the console sink is gated off, and records | |
| # content verbatim: Text parses no markup, so the log is a faithful copy. |
| if inputs: | ||
| verbose_log_section("Workflow Inputs", json.dumps(inputs, indent=2)) | ||
| verbose_log_section("Workflow Inputs", f"```json\n{json.dumps(inputs, indent=2)}\n```") |
There was a problem hiding this comment.
This fence is load-bearing and nothing says so. Drop it and Markdown reflows the payload onto one line, losing indent=2 entirely.
Two side effects worth knowing. The backticks reach the file sink as well, so the plain-text log now carries markdown syntax that exists only to satisfy the console. And nothing covers it: I reverted this hunk on its own and all 4968 tests still passed.
Moving the console to Text removes the problem rather than working around it. While you are on this line, it is also the last JSON display path in the repo still on the default ensure_ascii, so Cyrillic and CJK inputs render as \uXXXX. Compare gates/dialog.py:616 and engine/workflow.py:3113, which both carry a comment explaining the flag.
| verbose_log_section("Workflow Inputs", f"```json\n{json.dumps(inputs, indent=2)}\n```") | |
| verbose_log_section("Workflow Inputs", json.dumps(inputs, indent=2, ensure_ascii=False)) |
| from unittest.mock import MagicMock | ||
| from rich.markdown import Markdown | ||
| from rich.panel import Panel | ||
| from rich.text import Text | ||
| from conductor.cli import run as run_module | ||
| console = MagicMock() | ||
| file_console = MagicMock() | ||
| token_verbose = verbose_mode.set(True) | ||
| token_full = full_mode.set(True) | ||
| try: | ||
| with ( | ||
| patch("conductor.cli.run._verbose_console", console), | ||
| patch("conductor.cli.run._file_console", file_console), | ||
| ): | ||
| # Requirement: bracket-heavy agent prompts render through the Markdown parser. | ||
| content = "**Release** [[`abc123`](https://example.com/commit/abc123)]" | ||
| run_module.verbose_log_section("Prompt", content) | ||
| finally: | ||
| full_mode.reset(token_full) | ||
| verbose_mode.reset(token_verbose) | ||
| console_panel = console.print.call_args.args[0] | ||
| file_panel = file_console.print.call_args.args[0] | ||
| assert isinstance(console_panel, Panel) | ||
| assert isinstance(console_panel.renderable, Markdown) | ||
| assert isinstance(file_panel, Panel) | ||
| assert isinstance(file_panel.renderable, Text) | ||
| assert file_panel.renderable.plain == content |
There was a problem hiding this comment.
This asserts the renderer class rather than the behaviour, and that has a real cost. I swapped the console to Panel(Text(content)), which fixes #371 identically and loses no content, and this test fails on it. It rejects a correct fix.
It also never touches the brackets. Re-run it with content = "hello" and it still passes, so the witness string is currently decorative.
Below is a version I validated three ways: passes on this branch, passes with the Text alternative, and fails against pre-fix code on assert "[bold]literal[/bold]" in rendered. It uses the real-Console-into-StringIO pattern the neighbouring tests already use, and keeps your .plain == content assertion, which is the one genuinely behavioural check in the original.
| fromunittest.mockimportMagicMock | |
| fromrich.markdownimportMarkdown | |
| fromrich.panelimportPanel | |
| fromrich.textimportText | |
| fromconductor.cliimportrunasrun_module | |
| console=MagicMock() | |
| file_console=MagicMock() | |
| token_verbose=verbose_mode.set(True) | |
| token_full=full_mode.set(True) | |
| try: | |
| with ( | |
| patch("conductor.cli.run._verbose_console", console), | |
| patch("conductor.cli.run._file_console", file_console), | |
| ): | |
| # Requirement: bracket-heavy agent prompts render through the Markdown parser. | |
| content="**Release** [[`abc123`](https://example.com/commit/abc123)]" | |
| run_module.verbose_log_section("Prompt", content) | |
| finally: | |
| full_mode.reset(token_full) | |
| verbose_mode.reset(token_verbose) | |
| console_panel=console.print.call_args.args[0] | |
| file_panel=file_console.print.call_args.args[0] | |
| assertisinstance(console_panel, Panel) | |
| assertisinstance(console_panel.renderable, Markdown) | |
| assertisinstance(file_panel, Panel) | |
| assertisinstance(file_panel.renderable, Text) | |
| assertfile_panel.renderable.plain==content | |
| importre | |
| fromioimportStringIO | |
| fromunittest.mockimportMagicMock | |
| fromrich.consoleimportConsole | |
| fromrich.panelimportPanel | |
| fromrich.textimportText | |
| fromconductor.cliimportrunasrun_module | |
| output=StringIO() | |
| file_console=MagicMock() | |
| token_verbose=verbose_mode.set(True) | |
| token_full=full_mode.set(True) | |
| try: | |
| with ( | |
| patch( | |
| "conductor.cli.run._verbose_console", | |
| Console(file=output, force_terminal=True, no_color=True, width=200), | |
| ), | |
| patch("conductor.cli.run._file_console", file_console), | |
| ): | |
| # Issue #371 regression witness: keep the brackets. Drop them and every | |
| # assertion below still passes on any string at all. | |
| content="[bold]literal[/bold] [#63111] [/] tail" | |
| run_module.verbose_log_section("Prompt", content) | |
| finally: | |
| full_mode.reset(token_full) | |
| verbose_mode.reset(token_verbose) | |
| rendered=re.sub(r"\x1b\[[0-9;]*m", "", output.getvalue()) | |
| assert"[bold]literal[/bold]"inrendered | |
| assert"[#63111]"inrendered | |
| assert"tail"inrendered | |
| file_panel=file_console.print.call_args.args[0] | |
| assertisinstance(file_panel, Panel) | |
| assertisinstance(file_panel.renderable, Text) | |
| assertfile_panel.renderable.plain==content |
hertznsk
commented
Aug 10, 2026
Thanks, Jason Robert (@jrob5756) — I switched the verbose console path to I also removed the workflow-input Markdown fence, added |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@## main #391 +/- ##
=======================================
Coverage ? 91.32% =======================================
Files ? 108 Lines ? 17507 Branches ? 0 =======================================
Hits ? 15989 Misses ? 1518 Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
The "Workflow Inputs" verbose panel serialised inputs with the default ensure_ascii=True, so a Cyrillic, CJK or emoji input value was displayed as \uXXXX escapes rather than the text the user typed. Every other JSON display path in the repo already passes ensure_ascii=False (issue microsoft#356); this was the last one that did not. This branch previously also carried a fix for Rich parsing bracketed prompt text as console markup (microsoft#371). That landed independently on main in microsoft#387, which arrived at the same Panel(Text(content), ...) console fix and additionally set markup=False on the file-log console. The rest of this branch is therefore rebased away as redundant, leaving only the encoding change and its regression test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
56458c4 to
314ec95CompareJason Robert (jrob5756)
commented
Aug 11, 2026
Heads up hertznsk, I rebased this branch and force-pushed, so please Your v2 was correct. It got overtaken. While it was in review, #387 merged and fixed the same root cause in the same function, landing on I checked what was still unique after #387 and it was one thing: Worth recording since the issue is still open: #371 is fixed on Thanks for the work here, and for the thorough follow-up on the review comments. The diagnosis in #371 was accurate and the benchmarks in it were genuinely useful for verifying #387 too. |
Jason Robert (jrob5756)
left a comment
There was a problem hiding this comment.
LGTM. Approved!
Uh oh!
There was an error while loading. Please reload this page.
Summary
Serialise the verbose "Workflow Inputs" panel with
ensure_ascii=Falseso non-ASCII input values are shown as the text the user typed rather than\uXXXXescapes.Why
json.dumps(inputs, indent=2)used the defaultensure_ascii=True, so running a workflow with a Cyrillic, CJK or emoji input rendered the panel as:Every other JSON display path in the repo already passes
ensure_ascii=False(issue #356, PR #359) with a comment explaining why:gates/dialog.py,engine/workflow.py,engine/validator.py,engine/dialog_evaluator.py. This call site was the last one that did not.Scope change
This branch originally fixed Rich parsing bracketed prompt text as console markup (#371). That problem was fixed independently on
mainby #387, which reached the samePanel(Text(content), ...)console fix and additionally setmarkup=Falseon the file-log console.Measured on current
main: the #371 reproduction (175 KB of link-heavy prompt) renders in 388 ms and 2.7 MB, down from 63 s and 993 MB, with the file log preserving content verbatim. The rest of this branch is therefore rebased away as redundant, and the duplicate test is dropped sincetests/test_cli/test_markup_safety.pyfrom #387 already covers both sinks more thoroughly.Validation
test_workflow_inputs_section_serializes_non_ascii_unescaped, which fails without the change onassert 'план 你好' in ...ruff check,ruff format --checkandty checkall clean