Web Dashboard: Tool results display raw Python repr() with escaped newlines instead of readable text
Version
- conductor-cli: v0.1.8
- github-copilot-sdk: v0.2.2
- OS: Windows 11 (also partially affects Linux/macOS — see below)
Summary
The Conductor web dashboard (--web) displays tool execution results as raw Python repr() output (e.g., Result(content='...', contents=None, detailed_content='...', kind=None)) instead of extracting and displaying the human-readable text content. Newline characters inside the repr() output appear as literal \n and \r\n escape sequences, making the activity panel very hard to read.
Reproduction Steps
- Run any workflow with the
copilot provider and --web flag:
conductor run workflow.yaml --web --provider copilot
- Open the web dashboard in a browser.
- Click on any agent node to expand its Activity panel, or look at the bottom Activity log tab.
- Observe tool results — they show raw Python object representations instead of clean text.
Actual Behavior
Activity entries for tool results display:
✓ result Result(content='Errors: MSB4276: The default SDK resolver failed to resolve SDK \n"Microsoft.NET.SDK.WorkloadAutoImportPropsLocator" because directory "C:\\Program \nFiles\\dotnet\\sdk\\10.0.104\\Sdks\\Microsoft.NET.SDK.WorkloadAutoImportPropsLocator\\Sdk" did not exist.\n C:\\az\\Chaos\\services\\GW\\build\\Strict.props(8,5): message : ...
✓ result Result(content='1. # Workspace Creation Passthrough\r\n2. \r\n3. | Field | Value |\r\n4. |-------|-------|\r\n5. | **Status** | DRAFT |\r\n...', contents=None, detailed_content='...', kind=None)
✓ result Result(content='Intent logged', contents=None, detailed_content='Reading implementation plan', kind=None)
✓ result Result(content='Failed Microsoft.Azure.Chaos.Arm.Gateway.Resources.OperationStatus.Tests.Api.IntegrationTests.OperationStatusIntegrat\nionTestsV2025_07_01_preview.GetOperationStatus [1 s]\n Failed Microsoft.Azure.Chaos.Arm.Gateway.Resources.OperationStatus.Tests.Api.IntegrationTests.OperationStatusIntegrat\nionTestsV2024_01_01.GetOperationStatus [1 s]\n...
Note: the \n in OperationStatusIntegrat\nionTests is a console-width line wrap baked into the tool output. Combined with the repr escaping, this makes multi-line output (stack traces, test results) nearly unreadable:
✓ result Result(content='at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Logged|17_1(ResourceInvoker invoker)\n at Microsoft.Azure.Squall.Telemetry.Auditing.AuditMiddleware.Invoke(HttpContext context)\n at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)\n...
Problems visible:
- The entire
Result(content='...', contents=None, detailed_content='...', kind=None) Python repr wrapper is shown to the user
- Newlines are displayed as literal
\n and \r\n text instead of actual line breaks
- Windows file path backslashes are doubled (
\\ instead of \), e.g.: Result(content='services/GW/src/ArmGatewayService\\ArmGatewayService.Resources.OperationStatus\\Api\\...')
- Multi-line output (stack traces, test results) is completely unreadable as a single escaped line
- On Windows,
\r\n (CRLF) appears because tool output uses Windows line endings — but even on Linux, \n would appear as escaped text due to the same root cause
Expected Behavior
Activity entries should display clean text:
✓ result 1. # Workspace Creation Passthrough
2.
3. | Field | Value |
...
✓ result Errors: MSB4276: The default SDK resolver failed to resolve SDK
"Microsoft.NET.SDK.WorkloadAutoImportPropsLocator" ...
Root Cause Analysis
Primary: str(result) on structured SDK objects (Copilot provider)
In conductor/providers/copilot.py, the _forward_event method (around line 1166–1177) calls str(result) on the Copilot SDK's Result object:
# copilot.py — _forward_event method
elif event_type == "tool.execution_complete":
tool_name = getattr(event.data, "tool_name", None) or getattr(event.data, "name", None)
result = getattr(event.data, "result", None) or getattr(event.data, "output", None)
callback(
"agent_tool_complete",
{
"tool_name": str(tool_name) if tool_name else None,
"result": str(result)[:500] if result else None, # ← BUG
},
)
The result here is a copilot.generated.session_events.Result dataclass:
# copilot/generated/session_events.py
class Result:
"""Tool execution result on success"""
content: str | None = None # Concise result text
contents: list[ContentElement] | None = None # Structured content blocks
detailed_content: str | None = None # Full untruncated content
kind: ResultKind | None = None
Calling str() on this object produces its Python repr: Result(content='...', contents=None, detailed_content='...', kind=None) — which escapes all special characters (newlines → \n, carriage returns → \r, backslashes → \\).
The same str(result) anti-pattern also exists in the console output handler (around line 1074–1085).
Note — Claude provider is NOT affected: The claude.py provider calls MCPManager.call_tool() which already returns -> str (it extracts TextContent.text from the MCP result and joins with \n). So str(result) on an already-string value is a no-op. This bug is specific to the Copilot provider where the SDK event stream returns a structured Result object rather than a plain string.
Secondary: str(arguments) produces Python repr, not JSON, and truncates unsafely
The arguments field (line 1162) uses str(arguments)[:500], which has two sub-issues:
-
Python repr instead of JSON — str() on a dict produces Python repr syntax ({'key': 'value'} with single quotes, \\ for backslashes) rather than valid JSON ({"key": "value"}). Since arguments are inherently structured data, JSON is the expected format. Note: within JSON, \n in string values is correct encoding — the display concern here is the Python repr wrapper, not the escaped characters themselves.
-
Unsafe truncation — the [:500] slice can cut in the middle of a JSON/dict structure, dropping the closing } and producing visually broken output. Example:
🔧 tool {'command': 'cd C:\\az\\Chaos\\services\\GW\n$msbuild = "C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\MSBuild\\Current\\Bin\\amd64\\MSBuild.exe"\n$devEnvDir = "C:\\Program Files\\Microsoft Visual Studio\\18\\Enterprise\\Common7\\IDE\\"\n& $msbuild src\\ArmGatewayService\\ArmGatewayService.Resources.OperationStatus.Tests\\ArmGatewayService.Resources.OperationStatus.Tests.csproj -restore -t:Build -p:Configuration=Debug "-p:DevEnvDir=$devEnvDir" -v:minimal 2>&1 | Select-Object -Last
The } is missing because the 500-char limit cuts mid-structure. A better approach would be json.dumps(arguments, indent=2)[:500] + "…" (or truncate at a structural boundary).
Proposed Fix
Option A: Extract .content from the Result object (recommended)
# In copilot.py — _forward_event, for tool.execution_complete:
elif event_type == "tool.execution_complete":
tool_name = getattr(event.data, "tool_name", None) or getattr(event.data, "name", None)
raw_result = getattr(event.data, "result", None) or getattr(event.data, "output", None)
# Extract text content from structured Result objects
result_text = None
if raw_result is not None:
result_text = (
getattr(raw_result, "content", None)
or getattr(raw_result, "detailed_content", None)
or str(raw_result)
)
callback(
"agent_tool_complete",
{
"tool_name": str(tool_name) if tool_name else None,
"result": str(result_text)[:500] if result_text else None,
},
)
Apply the same pattern for:
tool.execution_start arguments: use json.dumps(arguments) if it's a dict, else str(arguments). Consider truncating at a structural boundary (e.g., append …} or … after truncation) rather than slicing mid-structure.
- Console output handler (around line 1074): same
.content extraction
Option B (frontend, complementary): Normalize CRLF in the frontend
Even after fixing the Python side, tool output from Windows may contain \r\n. While CSS white-space: pre-wrap handles \r\n correctly in most browsers, a defensive normalization step in the Yu or io helper would be good:
// In the Yu() display helper:
function Yu(e) {
if (e == null) return "";
if (typeof e === "string") return e.replace(/\r\n/g, "\n");
try { return JSON.stringify(e, null, 2); }
catch { return String(e); }
}
This is a minor improvement and is not required if the primary fix is applied, since real \r\n characters render correctly with pre-wrap. However, it prevents any edge cases with \r alone (which some browsers may not render as a line break).
Cross-Platform Impact Assessment
| Platform |
Current Behavior (Copilot provider) |
After Fix |
| Windows |
Result(content='...\r\n...') — repr wrapper, \r\n visible, \\ doubled paths |
Clean text with proper line breaks and single \ paths |
| Linux/macOS |
Result(content='...\n...') — repr wrapper AND \n visible |
Clean text with proper line breaks |
The Claude provider is not affected on any platform — MCPManager.call_tool() already returns a plain string.
The fix is platform-agnostic — it addresses the core issue (Python repr) regardless of OS line-ending conventions.
Affected Files
| File |
Lines |
Issue |
conductor/providers/copilot.py |
~1162 |
str(arguments) on tool start event |
conductor/providers/copilot.py |
~1175 |
str(result) on tool complete event — primary bug |
conductor/providers/copilot.py |
~1076 |
str(result) in console output handler |
frontend/ (React source) |
Yu() helper |
Optional: normalize \r\n → \n |
conductor/providers/claude.py is not affected — its MCPManager.call_tool() already returns a plain string.
Workarounds
None currently. Users must mentally parse Result(content='...', contents=None, ...) and mentally unescape \n/\r\n when reading the web dashboard.
Web Dashboard: Tool results display raw Python
repr()with escaped newlines instead of readable textVersion
Summary
The Conductor web dashboard (
--web) displays tool execution results as raw Pythonrepr()output (e.g.,Result(content='...', contents=None, detailed_content='...', kind=None)) instead of extracting and displaying the human-readable text content. Newline characters inside therepr()output appear as literal\nand\r\nescape sequences, making the activity panel very hard to read.Reproduction Steps
copilotprovider and--webflag:Actual Behavior
Activity entries for tool results display:
Note: the
\ninOperationStatusIntegrat\nionTestsis a console-width line wrap baked into the tool output. Combined with the repr escaping, this makes multi-line output (stack traces, test results) nearly unreadable:Problems visible:
Result(content='...', contents=None, detailed_content='...', kind=None)Python repr wrapper is shown to the user\nand\r\ntext instead of actual line breaks\\instead of\), e.g.:Result(content='services/GW/src/ArmGatewayService\\ArmGatewayService.Resources.OperationStatus\\Api\\...')\r\n(CRLF) appears because tool output uses Windows line endings — but even on Linux,\nwould appear as escaped text due to the same root causeExpected Behavior
Activity entries should display clean text:
Root Cause Analysis
Primary:
str(result)on structured SDK objects (Copilot provider)In
conductor/providers/copilot.py, the_forward_eventmethod (around line 1166–1177) callsstr(result)on the Copilot SDK'sResultobject:The
resulthere is acopilot.generated.session_events.Resultdataclass:Calling
str()on this object produces its Python repr:Result(content='...', contents=None, detailed_content='...', kind=None)— which escapes all special characters (newlines →\n, carriage returns →\r, backslashes →\\).The same
str(result)anti-pattern also exists in the console output handler (around line 1074–1085).Secondary:
str(arguments)produces Python repr, not JSON, and truncates unsafelyThe
argumentsfield (line 1162) usesstr(arguments)[:500], which has two sub-issues:Python repr instead of JSON —
str()on a dict produces Python repr syntax ({'key': 'value'}with single quotes,\\for backslashes) rather than valid JSON ({"key": "value"}). Since arguments are inherently structured data, JSON is the expected format. Note: within JSON,\nin string values is correct encoding — the display concern here is the Python repr wrapper, not the escaped characters themselves.Unsafe truncation — the
[:500]slice can cut in the middle of a JSON/dict structure, dropping the closing}and producing visually broken output. Example:The
}is missing because the 500-char limit cuts mid-structure. A better approach would bejson.dumps(arguments, indent=2)[:500] + "…"(or truncate at a structural boundary).Proposed Fix
Option A: Extract
.contentfrom the Result object (recommended)Apply the same pattern for:
tool.execution_startarguments: usejson.dumps(arguments)if it's a dict, elsestr(arguments). Consider truncating at a structural boundary (e.g., append…}or…after truncation) rather than slicing mid-structure..contentextractionOption B (frontend, complementary): Normalize CRLF in the frontend
Even after fixing the Python side, tool output from Windows may contain
\r\n. While CSSwhite-space: pre-wraphandles\r\ncorrectly in most browsers, a defensive normalization step in theYuoriohelper would be good:This is a minor improvement and is not required if the primary fix is applied, since real
\r\ncharacters render correctly withpre-wrap. However, it prevents any edge cases with\ralone (which some browsers may not render as a line break).Cross-Platform Impact Assessment
Result(content='...\r\n...')— repr wrapper,\r\nvisible,\\doubled paths\pathsResult(content='...\n...')— repr wrapper AND\nvisibleThe fix is platform-agnostic — it addresses the core issue (Python repr) regardless of OS line-ending conventions.
Affected Files
conductor/providers/copilot.pystr(arguments)on tool start eventconductor/providers/copilot.pystr(result)on tool complete event — primary bugconductor/providers/copilot.pystr(result)in console output handlerfrontend/(React source)Yu()helper\r\n→\nWorkarounds
None currently. Users must mentally parse
Result(content='...', contents=None, ...)and mentally unescape\n/\r\nwhen reading the web dashboard.