Skip to content

Web Dashboard: Tool results display raw Python repr() with escaped newlines instead of readable text #93

@franklixuefei

Description

@franklixuefei

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

  1. Run any workflow with the copilot provider and --web flag:
    conductor run workflow.yaml --web --provider copilot
  2. Open the web dashboard in a browser.
  3. Click on any agent node to expand its Activity panel, or look at the bottom Activity log tab.
  4. 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  Intent logged
✓ 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:

  1. Python repr instead of JSONstr() 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.

  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions