Show partial trace on run failure instead of generic error#1352
Show partial trace on run failure instead of generic error#1352chiang-daniel wants to merge 9 commits intomainfrom
Conversation
WalkthroughAdds structured error tracing: a new ErrorWithTrace model and KilnRunError, adapter changes to capture partial message traces on failures, server handler returning ErrorWithTrace for 500s, frontend detection/UI to display traces, and extensive tests across adapters, server, and UI. Changes
Sequence DiagramsequenceDiagram
participant Client as Client/UI
participant Server as FastAPI Server
participant Adapter as Model Adapter
participant Model as AI Model
Client->>Server: POST /run (task request)
Server->>Adapter: adapter.invoke(input)
Adapter->>Adapter: create caller-owned messages list
Adapter->>Adapter: _run(input, messages) -- mutates messages
Adapter->>Model: send chat completion / tool calls
Note over Adapter,Model: messages accumulate system/user/assistant/tool entries
alt Success
Model-->>Adapter: response
Adapter->>Server: RunOutput (+ optional trace)
Server-->>Client: 200 OK (result)
else Failure
Model-->>Adapter: raises exception
Adapter->>Adapter: catch exception, convert messages -> partial_trace
Adapter->>Adapter: raise KilnRunError(partial_trace, original)
Server->>Server: custom handler catches KilnRunError
Server->>Client: 500 ErrorWithTrace JSON (message, error_type, trace)
Client->>Client: detect ErrorWithTrace shape -> render error_with_trace UI
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a structured error handling system for adapter runs, enabling the UI to display partial conversation traces when a failure occurs. It adds an ErrorWithTrace schema, a KilnRunError exception wrapper, and a new Svelte component for rendering these traces. The BaseAdapter and its implementations (LiteLLM, MCP) were updated to capture and propagate partial traces during runtime exceptions. Feedback suggests refining the error message mapping in format_user_message to avoid losing specific debugging context (like tool names) and troubleshooting links, as well as ensuring consistency in adapter method signatures within test mocks.
📊 Coverage ReportOverall Coverage: 45% Diff: origin/main...HEADNo lines with coverage information in this diff.
|
There was a problem hiding this comment.
Actionable comments posted: 5
🧹 Nitpick comments (7)
app/web_ui/src/routes/(app)/run/+page.svelte (1)
259-284: Optional: consider scrolling to the error block on failure.When
error_with_traceis set,output_sectionis not bound, soscroll_to_output_if_needed()is skipped (thefinallyblock only scrolls whenresponseis truthy). For long input forms, the run-failed UI may render below the fold without auto-scroll, requiring users to manually scroll to discover the error. Consider bindingoutput_section(or a similar ref) on the error branch and callingscroll_to_output_if_needed()infinallywhen eitherresponseorerror_with_traceis set.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/web_ui/src/routes/`(app)/run/+page.svelte around lines 259 - 284, The error branch doesn't bind output_section so scroll_to_output_if_needed() isn't invoked when error_with_trace is set; update the template so the error branch (where error_with_trace is truthy) also binds the same output_section (or a new element ref) and ensure the finally block that calls scroll_to_output_if_needed() runs when either response or error_with_trace is set (check response || error_with_trace) so the page auto-scrolls to the error block; refer to error_with_trace, output_section, scroll_to_output_if_needed(), and response to locate and change the relevant template and finally logic.app/web_ui/src/lib/ui/error_with_trace.test.ts (1)
40-122: LGTM!Good coverage across the visible behaviour: trace render gating (present/null/empty), troubleshooting steps pass-through, default vs. custom
error_title, anderror.messagerendering. TheResizeObserverpolyfill inbeforeAllis a sensible workaround for jsdom's missing API used by the underlyingoutput.svelte.Optional nit: the
as ErrorWithTraceType["trace"]cast at line 43 hints at a small type drift betweenTraceandErrorWithTraceType["trace"]. If they're meant to be the same shape, aligning the types in$lib/typeswould let the assignment work without the cast.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/web_ui/src/lib/ui/error_with_trace.test.ts` around lines 40 - 122, The test uses a type cast `as ErrorWithTraceType["trace"]` for SAMPLE_TRACE indicating a type mismatch between the sample fixture and the declared error trace type; update the definitions in $lib/types so the trace shape used by SAMPLE_TRACE (and by makeError) and ErrorWithTraceType["trace"] are identical (e.g., unify/export the same Trace interface and reference it from ErrorWithTraceType), then remove the unnecessary cast in the test (line with SAMPLE_TRACE) and ensure imports use the unified Trace/ErrorWithTraceType symbols.libs/core/kiln_ai/adapters/errors.py (1)
53-66: Minor: the non-string guard is effectively unreachable.In CPython,
str(exc)always returns astror raises (which the outertryalready handles). Theif not isinstance(result, str)branch is defensive paranoia that won't be exercised. Harmless, but worth a comment if you want to keep it; otherwise it can be dropped to simplify.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/core/kiln_ai/adapters/errors.py` around lines 53 - 66, The non-string guard in _safe_str is unreachable in CPython because str(exc) either returns a str or raises; remove the redundant `if not isinstance(result, str): return _GENERIC_FALLBACK_MESSAGE` branch to simplify the function while keeping the existing try/except and empty-string handling; update any tests if they relied on that defensive branch and ensure _GENERIC_FALLBACK_MESSAGE remains used only in the exception-from-str case.libs/server/kiln_server/test_run_api_errors.py (1)
44-49: Passpathasstrfor consistency.
Projectis constructed with apathlib.Pathhere, whiletest_litellm_adapter_errors.py(and most existing fixtures in this repo) usestr(project_path). Pydantic v2 will likely coerce, but the inconsistency is unnecessary.♻️ Proposed change
- project = Project(name="Test Project", path=project_path) + project = Project(name="Test Project", path=str(project_path))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/server/kiln_server/test_run_api_errors.py` around lines 44 - 49, The fixture task_setup constructs project_path as a pathlib.Path but passes it directly to Project; change the Project instantiation (Project(name="Test Project", path=...)) to pass path=str(project_path) for consistency with other tests (e.g., test_litellm_adapter_errors.py) and then call project.save_to_file() as before so the path is a string rather than a Path object.libs/core/kiln_ai/adapters/model_adapters/test_base_adapter_errors.py (1)
111-233: LGTM — comprehensive coverage of the new wrapping contract.The suite exercises the important invariants: identity passthrough for already-wrapped errors,
__cause__preservation,partial_tracepopulated/None timing, and the absorption of_messages_to_tracefailures so the original exception still surfaces. Note that no@pytest.mark.asynciomarkers are used here whiletest_litellm_adapter_errors.pyuses them explicitly — this only works ifasyncio_mode = "auto"is set in pytest config; consider aligning the style across the two new files.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/core/kiln_ai/adapters/model_adapters/test_base_adapter_errors.py` around lines 111 - 233, The tests in this file rely on pytest's asyncio auto-detection but other test files use explicit `@pytest.mark.asyncio`; make the style consistent by adding `@pytest.mark.asyncio` to each async test function (e.g., test_raises_kiln_run_error_when_run_throws, test_partial_trace_populated_when_messages_extended, test_partial_trace_none_when_failure_before_any_messages, test_existing_kiln_run_error_passes_through, test_cause_chain_preserved, test_format_error_message_applied_for_known_exception, test_format_error_message_applied_for_litellm_rate_limit, test_messages_to_trace_failure_does_not_swallow_original_exception, test_messages_to_trace_hook_called) or alternatively ensure pytest.ini / pyproject.toml explicitly sets asyncio_mode = "auto"; pick one approach and apply it consistently across this test file to match test_litellm_adapter_errors.py.libs/core/kiln_ai/adapters/model_adapters/base_adapter.py (1)
319-330:save_to_file()failures get wrapped with a misleading "successful" trace.If
run.save_to_file()raises (disk full, permission error, validation error in the saved model, etc.), it falls into the genericexcept Exceptionarm at line 334. The user then sees:
message = "An unexpected error occurred."(becauseformat_error_messagedoesn't recognizeOSError/PermissionError)partial_trace =the complete trace from a fully successful model runThat's confusing — the run itself succeeded; only persistence failed. Consider letting persistence errors escape unwrapped (they're not "run failures with a trace to preserve"), e.g.:
♻️ Suggested split
- run = self.generate_run( - input, - input_source, - run_output, - usage, - run_output.trace, - parent_task_run, - ) - - # Save the run if configured to do so, and we have a path to save to - if ( - self.base_adapter_config.allow_saving - and Config.shared().autosave_runs - and self.task.path is not None - ): - run.save_to_file() - else: - # Clear the ID to indicate it's not persisted - run.id = None - - return run, run_output + run = self.generate_run( + input, + input_source, + run_output, + usage, + run_output.trace, + parent_task_run, + ) except KilnRunError: raise except Exception as e: ... + + # Persistence errors are not run failures — let them surface unwrapped + # so callers don't see a "successful" trace alongside a generic error. + if ( + self.base_adapter_config.allow_saving + and Config.shared().autosave_runs + and self.task.path is not None + ): + run.save_to_file() + else: + run.id = None + + return run, run_output🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/core/kiln_ai/adapters/model_adapters/base_adapter.py` around lines 319 - 330, The persistence step (run.save_to_file()) is currently inside the same exception-handling flow that treats any error as a run failure and returns the full run trace; instead, isolate persistence errors so they don't get wrapped as "run failures." Wrap only the save step in its own try/except: when base_adapter_config.allow_saving && Config.shared().autosave_runs && self.task.path is not None, call run.save_to_file() inside a small try block that either (a) lets IO/Permission/validation exceptions propagate (preferred) or (b) catches them and raises a more specific persistence exception/log entry (not the generic format_error_message path). Ensure run.id is only cleared when you intentionally treat the run as non-persisted (keep the existing run/id and run_output return semantics on a successful run; do not replace the run trace with an unrelated "unexpected error" message from format_error_message).libs/core/kiln_ai/adapters/model_adapters/litellm_adapter.py (1)
240-248: Type-alias trick is pragmatic but worth a brief alias-safety note.Aliasing
messages_internal = messagesand using# type: ignore[assignment]to widen the element type is fine because the mutations after this only ever go throughmessages_internal— and crucially, you never callmessages_internal = ...(rebinding). If a future change ever doesmessages_internal = something_new, the caller's reference goes stale and the partial trace is lost on exception. The docstring on the abstract_run(base_adapter.pyline 531-535) already calls this out, but consider mirroring a one-liner here as a local reminder, since this is the only file that performs the widening.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/core/kiln_ai/adapters/model_adapters/litellm_adapter.py` around lines 240 - 248, Add a short alias-safety comment above the widening assignment so future maintainers know why we do messages_internal: list[ChatCompletionMessageIncludingLiteLLM] = messages # type: ignore[assignment] and must not rebind messages_internal; explain that mutations are safe because we never reassign the variable (rebinding would detach the caller's reference and lose the partial trace on exception) and reference the abstract _run behavior in base_adapter.py for context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@libs/core/kiln_ai/adapters/model_adapters/test_litellm_adapter_errors.py`:
- Around line 41-51: The test uses a hyphenated model name which doesn't match
the registry; update the KilnAgentRunConfigProperties in LiteLlmConfig so
model_name uses the underscored registry name "gpt_4o_mini" (replace
"gpt-4o-mini" with "gpt_4o_mini") so that builtin_model_from resolves to the
OpenAI provider and the adapter's self.model_provider() path and rate-limit
error handling are exercised.
In `@libs/core/kiln_ai/adapters/model_adapters/test_saving_adapter_results.py`:
- Around line 13-15: The test double for the async adapter method _run currently
declares messages=None which weakens the test contract; change the signature of
the mock methods (the async def _run functions referenced by _run and the second
occurrence) to require messages as a positional/keyword argument (remove the
default None) so it matches the production adapter signature (async def
_run(self, input: InputType, messages, **kwargs) -> tuple[RunOutput, Usage |
None]) and update any test callers to pass messages explicitly.
In `@libs/core/kiln_ai/adapters/model_adapters/test_structured_output.py`:
- Around line 100-102: The mock _run method currently declares messages=None
which relaxes the adapter contract; change the signature of async def _run(self,
input: str, messages, **kwargs) -> tuple[RunOutput, Usage | None]: (remove the
default None) so messages is required, and update any local test
instantiations/calls to pass a messages argument consistent with the adapter
(e.g., a list of Message objects or the expected type); ensure the _run
implementation and any type hints still match RunOutput/Usage return types.
In `@libs/core/kiln_ai/adapters/test_prompt_builders.py`:
- Around line 63-65: The test mock's async method _run currently declares
messages=None which weakens the signature; update the mock method _run in
test_prompt_builders.py to match BaseAdapter._run by making the messages
parameter required (remove the default) and ensure its type annotation matches
the original (keep InputType, Return type tuple[RunOutput, Usage | None] and any
kwarg passthrough), so test signature drift is caught early; locate and edit the
async def _run(...) function to remove the messages default and align the
parameter types with BaseAdapter._run.
In `@libs/server/kiln_server/run_api.py`:
- Around line 382-388: The OpenAPI 500 response currently documents only
ErrorWithTrace but the global fallback in custom_errors.py (the handler that
returns {"message": message, "raw_error": str(exc)} for
non-KilnRunError/httpx.TimeoutException) can return a different shape; update
the 500 response declaration in run_api.py to document both shapes (use an
OpenAPI oneOf/union): keep ErrorWithTrace and add a small schema/model (e.g.,
FallbackError or GenericError with "message": str and "raw_error": str) and
reference both in the responses mapping for 500 so generated clients match
runtime responses.
---
Nitpick comments:
In `@app/web_ui/src/lib/ui/error_with_trace.test.ts`:
- Around line 40-122: The test uses a type cast `as ErrorWithTraceType["trace"]`
for SAMPLE_TRACE indicating a type mismatch between the sample fixture and the
declared error trace type; update the definitions in $lib/types so the trace
shape used by SAMPLE_TRACE (and by makeError) and ErrorWithTraceType["trace"]
are identical (e.g., unify/export the same Trace interface and reference it from
ErrorWithTraceType), then remove the unnecessary cast in the test (line with
SAMPLE_TRACE) and ensure imports use the unified Trace/ErrorWithTraceType
symbols.
In `@app/web_ui/src/routes/`(app)/run/+page.svelte:
- Around line 259-284: The error branch doesn't bind output_section so
scroll_to_output_if_needed() isn't invoked when error_with_trace is set; update
the template so the error branch (where error_with_trace is truthy) also binds
the same output_section (or a new element ref) and ensure the finally block that
calls scroll_to_output_if_needed() runs when either response or error_with_trace
is set (check response || error_with_trace) so the page auto-scrolls to the
error block; refer to error_with_trace, output_section,
scroll_to_output_if_needed(), and response to locate and change the relevant
template and finally logic.
In `@libs/core/kiln_ai/adapters/errors.py`:
- Around line 53-66: The non-string guard in _safe_str is unreachable in CPython
because str(exc) either returns a str or raises; remove the redundant `if not
isinstance(result, str): return _GENERIC_FALLBACK_MESSAGE` branch to simplify
the function while keeping the existing try/except and empty-string handling;
update any tests if they relied on that defensive branch and ensure
_GENERIC_FALLBACK_MESSAGE remains used only in the exception-from-str case.
In `@libs/core/kiln_ai/adapters/model_adapters/base_adapter.py`:
- Around line 319-330: The persistence step (run.save_to_file()) is currently
inside the same exception-handling flow that treats any error as a run failure
and returns the full run trace; instead, isolate persistence errors so they
don't get wrapped as "run failures." Wrap only the save step in its own
try/except: when base_adapter_config.allow_saving &&
Config.shared().autosave_runs && self.task.path is not None, call
run.save_to_file() inside a small try block that either (a) lets
IO/Permission/validation exceptions propagate (preferred) or (b) catches them
and raises a more specific persistence exception/log entry (not the generic
format_error_message path). Ensure run.id is only cleared when you intentionally
treat the run as non-persisted (keep the existing run/id and run_output return
semantics on a successful run; do not replace the run trace with an unrelated
"unexpected error" message from format_error_message).
In `@libs/core/kiln_ai/adapters/model_adapters/litellm_adapter.py`:
- Around line 240-248: Add a short alias-safety comment above the widening
assignment so future maintainers know why we do messages_internal:
list[ChatCompletionMessageIncludingLiteLLM] = messages # type:
ignore[assignment] and must not rebind messages_internal; explain that mutations
are safe because we never reassign the variable (rebinding would detach the
caller's reference and lose the partial trace on exception) and reference the
abstract _run behavior in base_adapter.py for context.
In `@libs/core/kiln_ai/adapters/model_adapters/test_base_adapter_errors.py`:
- Around line 111-233: The tests in this file rely on pytest's asyncio
auto-detection but other test files use explicit `@pytest.mark.asyncio`; make the
style consistent by adding `@pytest.mark.asyncio` to each async test function
(e.g., test_raises_kiln_run_error_when_run_throws,
test_partial_trace_populated_when_messages_extended,
test_partial_trace_none_when_failure_before_any_messages,
test_existing_kiln_run_error_passes_through, test_cause_chain_preserved,
test_format_error_message_applied_for_known_exception,
test_format_error_message_applied_for_litellm_rate_limit,
test_messages_to_trace_failure_does_not_swallow_original_exception,
test_messages_to_trace_hook_called) or alternatively ensure pytest.ini /
pyproject.toml explicitly sets asyncio_mode = "auto"; pick one approach and
apply it consistently across this test file to match
test_litellm_adapter_errors.py.
In `@libs/server/kiln_server/test_run_api_errors.py`:
- Around line 44-49: The fixture task_setup constructs project_path as a
pathlib.Path but passes it directly to Project; change the Project instantiation
(Project(name="Test Project", path=...)) to pass path=str(project_path) for
consistency with other tests (e.g., test_litellm_adapter_errors.py) and then
call project.save_to_file() as before so the path is a string rather than a Path
object.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: ce058e69-4c43-4496-9596-9e81d597cb66
📒 Files selected for processing (25)
app/web_ui/src/lib/api_schema.d.tsapp/web_ui/src/lib/types.tsapp/web_ui/src/lib/ui/error_with_trace.svelteapp/web_ui/src/lib/ui/error_with_trace.test.tsapp/web_ui/src/routes/(app)/run/+page.svelteapp/web_ui/src/routes/(app)/run/error_with_trace_detection.tsapp/web_ui/src/routes/(app)/run/run_page_errors.test.tslibs/core/kiln_ai/adapters/errors.pylibs/core/kiln_ai/adapters/model_adapters/base_adapter.pylibs/core/kiln_ai/adapters/model_adapters/litellm_adapter.pylibs/core/kiln_ai/adapters/model_adapters/mcp_adapter.pylibs/core/kiln_ai/adapters/model_adapters/test_base_adapter.pylibs/core/kiln_ai/adapters/model_adapters/test_base_adapter_errors.pylibs/core/kiln_ai/adapters/model_adapters/test_litellm_adapter.pylibs/core/kiln_ai/adapters/model_adapters/test_litellm_adapter_errors.pylibs/core/kiln_ai/adapters/model_adapters/test_mcp_adapter.pylibs/core/kiln_ai/adapters/model_adapters/test_saving_adapter_results.pylibs/core/kiln_ai/adapters/model_adapters/test_structured_output.pylibs/core/kiln_ai/adapters/test_errors.pylibs/core/kiln_ai/adapters/test_prompt_builders.pylibs/core/kiln_ai/datamodel/test_basemodel.pylibs/server/kiln_server/custom_errors.pylibs/server/kiln_server/run_api.pylibs/server/kiln_server/test_custom_error.pylibs/server/kiln_server/test_run_api_errors.py
| # exception thrown from inside `_run` (or the post-processing that | ||
| # follows). `_run` must mutate this list in place (extend/append, | ||
| # or `list[:] = ...`) and never rebind the local name. | ||
| messages: list[ChatCompletionMessageParam] = [] |
There was a problem hiding this comment.
Allocate the messages list in the caller and pass it into _run() by reference. _run() appends to it as the conversation builds up.
If _run() throws, we still have the list with whatever was added before the crash. That's how we preserve the partial trace in case of failure.
| run.id = None | ||
|
|
||
| return run, run_output | ||
| except KilnRunError: |
There was a problem hiding this comment.
Wraps any failure from _run() in KilnRunError with the trace attached.
| trace: list[ChatCompletionMessageParam] | None = None | ||
|
|
||
|
|
||
| class KilnRunError(Exception): |
There was a problem hiding this comment.
New error type.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
libs/core/kiln_ai/adapters/model_adapters/test_structured_output.py (1)
121-138:⚠️ Potential issue | 🟡 MinorNarrow this branch to the wrapped error type.
pytest.raises(Exception)lets the newKilnRunErrorpath regress silently. Hoist the existingKilnRunErrorimport above this branch and assert it here instead of accepting any failure.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@libs/core/kiln_ai/adapters/model_adapters/test_structured_output.py` around lines 121 - 138, The test currently uses pytest.raises(Exception) for the first failing adapter case which can hide the new KilnRunError path; import KilnRunError (from kiln_ai.adapters.errors) at the top of this test block and change the pytest.raises(Exception) around adapter = MockAdapter(task, response={"setup": "asdf"}) / await adapter.invoke(...) to pytest.raises(KilnRunError) so the test explicitly expects the wrapped KilnRunError raised by MockAdapter.invoke and fails if a different exception type is produced; locate the assertions around MockAdapter, task, and adapter.invoke to make this replacement.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@libs/core/kiln_ai/adapters/model_adapters/test_structured_output.py`:
- Around line 121-138: The test currently uses pytest.raises(Exception) for the
first failing adapter case which can hide the new KilnRunError path; import
KilnRunError (from kiln_ai.adapters.errors) at the top of this test block and
change the pytest.raises(Exception) around adapter = MockAdapter(task,
response={"setup": "asdf"}) / await adapter.invoke(...) to
pytest.raises(KilnRunError) so the test explicitly expects the wrapped
KilnRunError raised by MockAdapter.invoke and fails if a different exception
type is produced; locate the assertions around MockAdapter, task, and
adapter.invoke to make this replacement.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: cbe57152-2c34-40bb-812d-79b77115100a
📒 Files selected for processing (4)
libs/core/kiln_ai/adapters/model_adapters/test_litellm_adapter_errors.pylibs/core/kiln_ai/adapters/model_adapters/test_saving_adapter_results.pylibs/core/kiln_ai/adapters/model_adapters/test_structured_output.pylibs/core/kiln_ai/adapters/test_prompt_builders.py
✅ Files skipped from review due to trivial changes (1)
- libs/core/kiln_ai/adapters/model_adapters/test_saving_adapter_results.py
🚧 Files skipped from review as they are similar to previous changes (1)
- libs/core/kiln_ai/adapters/test_prompt_builders.py
What does this PR do?
When a task run fails, users see a generic failure with no context about what happened before the crash. They would have to dig through the server logs to understand what went wrong, kinda bad UX.
In this PR we introduced a shared ErrorWithTrace type to store additional info.
ErrorWithTraceErrorWithTracecomponent.Checklists