Make tool failures visible telemetry: span outcome attributes, derived error messages, e2e contract - #992
Conversation
e515a9f to
c431459
Compare
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-marketing | e9dda28 | Commit Preview URL Branch Preview URL |
Jun 15 2026, 02:34 AM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | e9dda28 | Jun 15 2026, 02:34 AM |
Cloudflare previewTorn down — the PR is closed. |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
…rror messages, recording-tracer contracts Expected tool failures (ToolResult.fail) ride the Effect success channel, so the tracer recorded healthy spans while users hit upstream error walls — in a week of prod data, zero of 19.5k plugin.openapi.invoke spans carried http.status_code (annotated inside the inner OpenApi.invoke span, not the wrapper queries target) and 562 of 804 executor.tool.execute error spans had an empty status.message (tagged errors with no message field). - Stamp executor.tool.outcome / error_code / error_status on executor.tool.execute and mcp.tool.dispatch whenever a dispatch resolves to a ToolResult, plus tenant/subject attribution so queries no longer need a trace-id join against the outer request span. - Annotate http.status_code on the plugin.openapi.invoke wrapper span. - Derive messages for the message-less SDK tagged errors and stop formatInvocationCauseMessage rendering plain objects as [object Object]. - Add a recording Tracer to @executor-js/sdk/testing and telemetry contract tests that pin the span names, attributes, and error statuses production queries depend on.
Replace the recording-tracer unit tests with one e2e scenario that drives the whole production topology — HTTP API → execution engine → sandbox → OpenAPI invoke → a real upstream returning 502 → OTLP export — and asserts the spans as the suite's motel store actually received them: outcome/error_code/error_status + tenant on executor.tool.execute, http.status_code on plugin.openapi.invoke, and ok/fail distinguishable. The unit tests verified span objects in process; the prod bug class this guards against (attribute stamped on a span the exporter never carries, failure riding the success channel) only shows up at the exported layer. Adds a Telemetry e2e service (motel span search with arrival polling), provided when the suite's motel boots (E2E_MOTEL_URL).
- prod-telemetry repo skill: the executor-cloud Axiom dataset layout (attributes.custom JSON map, span names and their attributes including the new outcome fields), working APL recipes for error digests and org attribution, prod-DB and PostHog access notes, and the deploy-canary procedure. - e2e/AGENTS.md: the Telemetry service — scenarios can assert on the spans the target actually exported, with the arrival-polling and fixture-tagging gotchas.
9c1dc8d to
e9dda28
Compare
Greptile SummaryThis PR makes expected tool failures (
Confidence Score: 4/5Safe to merge; all three production gaps are correctly addressed and the e2e contract pins them at the export layer. The span annotation ordering, Effect tap/withSpan nesting, and skip mechanism are all correct. The one issue is in the new telemetry surface: Effect.promise rather than Effect.tryPromise means a transient network error while polling the motel store becomes an unretryable defect rather than a retried failure — harmless for a local CI motel but worth hardening before the pattern is copied elsewhere. e2e/src/surfaces/telemetry.ts — the Effect.promise calls for fetch and response.json() should be Effect.tryPromise to keep network errors inside the retry loop. Important Files Changed
Sequence DiagramsequenceDiagram
participant C as Caller
participant E as executor.tool.execute span
participant D as mcp.tool.dispatch span
participant O as plugin.openapi.invoke span
participant U as Upstream HTTP
C->>E: execute(address, args)
activate E
Note over E: annotates tenant/subject at span open
E->>D: dispatch to sandbox
activate D
D->>O: OpenAPI invoke
activate O
O->>U: HTTP GET /fail
U-->>O: 502 Bad Gateway
Note over O: annotates http.status_code=502
O-->>D: ToolResult.fail
deactivate O
Note over D: annotateToolResultOutcome()
D-->>E: ToolResult.fail (success channel)
deactivate D
Note over E: Effect.tap(annotateToolResultOutcome)
E-->>C: ToolResult.fail
deactivate E
Note over E,O: All three spans carry failure attributes
|
Why
Expected tool failures (
ToolResult.fail) resolve through the Effect success channel by design, which meant the tracer recorded healthy spans while callers were hitting upstream error walls. Auditing a week of traces showed three concrete gaps:plugin.openapi.invokespans never carriedhttp.status_code— the annotation ran inside the innerOpenApi.invokespan (created byEffect.fn), not the wrapper span queries target. 0 of ~19.5k spans had it.executor.tool.executeerror spans had an emptystatus.message: most SDK tagged errors define nomessagefield, andTaggedErrorClassinstances default to"".[object Object]as their only label (non-Error causes stringified withString(...)).The absence of error data is indistinguishable from health, so none of this was noticed until incidents were debugged by hand.
What
annotateToolResultOutcome(exported from the SDK) stampsexecutor.tool.outcome(ok/fail),executor.tool.error_code, andexecutor.tool.error_statusontoexecutor.tool.executeandmcp.tool.dispatchwhenever a dispatch resolves to aToolResult. Codes and statuses are enumerable identifiers, never user content.executor.tool.execute, so error queries no longer need a trace-id join against the outer request span.http.status_codeon theplugin.openapi.invokewrapper span (in addition to the inner span).messagegetters for the message-less SDK tagged errors (ToolNotFoundError,ToolBlockedError,PluginNotLoadedError,NoHandlerError,ConnectionNotFoundError,ElicitationDeclinedError, integration lifecycle errors), andformatInvocationCauseMessagenow prefers the cause's_tag/ structural stringify over[object Object].e2e/cloud/telemetry-contract.test.ts): drives the whole production topology — HTTP API → execution engine → sandbox → OpenAPI invoke → a real local upstream returning 502 → span batch → OTLP export — and asserts the spans as the suite's trace store actually received them (failure outcome attributes + tenant onexecutor.tool.execute,http.status_codeonplugin.openapi.invoke, success distinguishable from failure). The prod bug class this guards against (an attribute stamped on a span the exporter never carries, a failure riding the success channel) only manifests at the exported layer, so that's where the contract is pinned. Adds aTelemetrye2e service that queries the suite's OTLP store with arrival polling.Verification
packages/core/sdk,packages/core/execution,packages/plugins/openapisuites green;lint,format:check,typecheckclean.Stack