Skip to content

fix(agent): Claude alias resolution + skill/error tracing + error sanitize (QA F-031/F-029/F-030) - #4855

Merged
mmabrouk merged 1 commit into
big-agentsfrom
fix/agent-sdk-tracing-findings
Jun 25, 2026
Merged

fix(agent): Claude alias resolution + skill/error tracing + error sanitize (QA F-031/F-029/F-030)#4855
mmabrouk merged 1 commit into
big-agentsfrom
fix/agent-sdk-tracing-findings

Conversation

@mmabrouk

Copy link
Copy Markdown
Member

Why

Three QA findings from the final agent-workflows sweep, all in the SDK connection resolver and the runner tracing/error boundary. Disjoint from the HITL (#4854) and MCP (#4853) lanes.

F-031 — the F-017 fix (#4846) broke the documented Claude alias. A claude harness run with the documented model: "haiku" returned 500 model 'haiku' needs a provider prefix (e.g. 'openai/haiku') and never reached the harness. The alias is the documented Claude form, and the hint suggested openai/ on an Anthropic-only harness.

F-029 — skills were invisible in traces. The trace echoed the author skill config but had no "skill loaded" signal and never showed the forced _agenta.* platform skills the harness injects.

F-030 — error runs traced only a count. An errored run carried errors.cumulative=1 and nothing else: no message, no provider, no exception event. The actionable text lived only in the HTTP body.

Error leak (Codex MEDIUM). The runner error string was rethrown verbatim into the user-facing workflow error, so a stack frame / internal path / HTTP-body dump could reach the UI.

What

  • F-031 (platform/connections.py, connections/errors.py): a bare Claude alias (haiku/sonnet/opus + [1m], reused from capabilities.py CLAUDE_MODEL_ALIASES) or a dated claude-* id now resolves to the anthropic provider before the F-017 fail-loud check, so it reaches auth exactly like anthropic/haiku. The missing-provider hint is harness-correct (anthropic/<m> on Claude, openai/<m> elsewhere), derived from the capability table. Inference only fills a missing provider; an explicit one is always honored.
  • F-029 (tracing/otel.ts, extensions/agenta.ts, pi-assets.ts, sandbox_agent.ts): the agent span carries ag.agent.skills.loaded + ag.agent.skills.count with the materialized names (author + forced _agenta.*). Claude/Daytona get it from the runner's tracer; local Pi gets it on Pi's own span via a new AGENTA_SKILLS_LOADED env.
  • F-030 (tracing/otel.ts, sandbox_agent.ts): a new recordError(message, provider) stamps ag.error.message, ag.error.provider, an OTel exception event, and ERROR status on the agent span before flush — on both the catch path and the swallowed-Pi-error path. Local Pi (span-less in the runner) emits a standalone agent_error span so the diagnostic still reaches the /invoke trace.
  • Error sanitize (utils/wire.py, utils/ts_runner.py): result_from_wire and the transport errors route through sanitize_runner_error / _transport_error — one clean line, stack-frame/path leak stripped, capped to 300 chars; the full detail is logged, never shown. The runner's existing conciseError is untouched (a clean message passes through unchanged).

No /run wire fields changed (skills ride an internal env, not the wire); the golden contract is unchanged.

Before / after

Before After
Claude model: "haiku" 500 needs a provider prefix (e.g. 'openai/haiku') resolves to anthropic, reaches auth
Missing-provider hint on Claude openai/<m> anthropic/<m>
Trace, skills author config echo only ag.agent.skills.loaded (incl. forced _agenta.*)
Trace, error run errors.cumulative=1 only ag.error.message + ag.error.provider + exception event
User-facing runner error raw string (could leak a stack/path) clean line; full detail logged

Tests

  • SDK: test_connections_http.py — bare alias resolves to anthropic, dated claude-* resolves, harness-correct hint (Claude vs Pi). test_wire_contract.pysanitize_runner_error (clean passthrough, multiline strip, stack-frame fallback, length cap, none/empty) + a leaky-error round-trip.
  • Runner: new otel-skills-error.test.ts (skills stamped on both tracers, omitted when none, error stamped on the owned span, standalone agent_error span for self-instrumenting Pi, provider fallback). sandbox-agent-pi-assets.test.tsAGENTA_SKILLS_LOADED carried under tracing, omitted otherwise. sandbox-agent-orchestration.test.ts — fake otel gains recordError/output.
  • SDK 357 + integration 4 green; runner 248 vitest + tsc clean; ruff clean.

Decisions logged in qa/final-sweep-decisions.md (D-009..D-012). Live trace re-verify is the orchestrator's.

https://claude.ai/code/session_01GYo3UEfvsZpncagqb28Mbc

…itize (QA F-031/F-029/F-030)

F-031: a bare Claude alias (haiku/sonnet/opus + [1m]) or a dated claude-* id now
resolves to the anthropic provider in the SDK connection resolver instead of
failing F-017's MissingProviderError, and the missing-provider hint names the
harness-reachable provider (anthropic/<m> on Claude, openai/<m> elsewhere). The
alias set is reused from capabilities.py; the harness is threaded from
RuntimeAuthContext.

F-029: the agent span now carries ag.agent.skills.loaded / ag.agent.skills.count
with the materialized skill names (author + forced _agenta.* platform skills) on
every harness — the sandbox-agent otel stamps it for Claude/Daytona, and local Pi
gets it on Pi's own span via the new AGENTA_SKILLS_LOADED env.

F-030: a new recordError(message, provider) on the sandbox-agent otel stamps
ag.error.message, ag.error.provider, an exception event, and ERROR status on the
agent span before flush; local Pi (span-less in the runner) emits a standalone
agent_error span so the diagnostic still reaches the /invoke trace.

Error sanitize: result_from_wire and the ts_runner transport errors route through
sanitize_runner_error / _transport_error — one clean user-facing line, stack/path
leak stripped, length-capped; the full detail is logged, never shown.

Tests: SDK connection + wire-contract unit tests (alias, harness-correct hint,
sanitizer); runner otel + pi-assets + orchestration unit tests.

Claude-Session: https://claude.ai/code/session_01GYo3UEfvsZpncagqb28Mbc
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jun 25, 2026
@vercel

vercel Bot commented Jun 25, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agenta-documentation Ready Ready Preview, Comment Jun 25, 2026 8:02pm

Request Review

@dosubot dosubot Bot added the bug label Jun 25, 2026
@mmabrouk

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Traces can now include which skills were loaded during a run.
    • Error reporting now surfaces cleaner, user-friendly messages while preserving detailed diagnostics internally.
  • Bug Fixes

    • Improved provider detection for bare Claude model names and aliases.
    • Missing-provider errors now give better provider-specific hints.
    • Sandbox agent failures are now recorded more reliably in traces, including provider context when available.

Walkthrough

The PR updates missing-provider handling for Claude models, sanitizes Python transport and wire error messages, and threads skills metadata plus run-error recording through the sandbox agent tracing stack.

Changes

Python connection resolution

Layer / File(s) Summary
Provider hinting and Claude inference
sdks/python/agenta/sdk/agents/connections/errors.py, sdks/python/agenta/sdk/agents/platform/connections.py, sdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.py
MissingProviderError now accepts hint_provider, connection resolution infers anthropic for bare Claude aliases and dated claude-* ids, and tests cover the new hinting and resolution rules.

Python runner error sanitization

Layer / File(s) Summary
Transport error sanitization
sdks/python/agenta/sdk/agents/utils/ts_runner.py, sdks/python/agenta/sdk/agents/utils/wire.py, sdks/python/oss/tests/pytest/unit/agents/test_wire_contract.py
Transport helpers and wire result parsing now log raw failure detail internally, surface cleaned RuntimeError messages, and the tests cover the sanitizer and wire contract.

Agent tracing stack

Layer / File(s) Summary
Skills metadata propagation
services/agent/src/engines/sandbox_agent.ts, services/agent/src/engines/sandbox_agent/pi-assets.ts, services/agent/src/extensions/agenta.ts, services/agent/src/tracing/otel.ts, services/agent/tests/unit/otel-skills-error.test.ts, services/agent/tests/unit/sandbox-agent-pi-assets.test.ts
plan.skillDirs now flows into Pi env, AGENTA_SKILLS_LOADED, OTEL config, and loaded-skill span attributes, and session MCP wiring now includes isDaytona.
Run error recording
services/agent/src/engines/sandbox_agent.ts, services/agent/src/tracing/otel.ts, services/agent/tests/unit/otel-skills-error.test.ts, services/agent/tests/unit/sandbox-agent-orchestration.test.ts
recordError now records run failures on the active agent span or a standalone agent_error span, and sandbox-agent error handling calls it before run finalization.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Agenta-AI/agenta#4814: Shares the same AGENTA_SKILLS_LOADED and OTel skills-stamping path across the sandbox agent, Pi extension, and tracer code.
  • Agenta-AI/agenta#4846: Touches the same Python missing-provider resolution path and MissingProviderError/connection resolver code that this PR extends with harness-aware hints.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 48.94% which is insufficient. The required threshold is 60.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title directly summarizes the main changes: Claude alias resolution, skill/error tracing, and error sanitization.
Description check ✅ Passed The description clearly matches the PR's fixes and tests for alias resolution, skill/error tracing, and error sanitization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/agent-sdk-tracing-findings

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@mmabrouk

Copy link
Copy Markdown
Member Author

Review needed on three specific judgment calls (rest is mechanical):

  1. F-031 alias inference scope. _inferred_claude_provider treats any bare claude-* id (plus the capabilities.py alias set) as anthropic. Is the claude-* prefix the right catch-all for dated ids, or should it be the explicit alias set only? Note: a bare alias now behaves identically to the already-documented anthropic/haiku form — including the pre-existing empty-pool → AmbiguousConnectionError quirk when no anthropic key exists (I deliberately did NOT widen scope to fix that quirk; it affects all providers equally).

  2. F-030 local-Pi standalone agent_error span. The runner can't stamp Pi's own span (different process, already flushed), so for local Pi recordError emits a separate agent_error span under the caller's traceparent. Is an extra span acceptable, or would you prefer the error only land for the harnesses the runner instruments (Claude/Daytona)? See D-010.

  3. Error sanitize boundary. Sanitizing happens once in result_from_wire / the ts_runner transport errors, leaving the runner's conciseError untouched (a clean message passes through unchanged). Confirm this is the right single boundary and that the 300-char cap + stack-frame/path regex are not too aggressive for legitimate messages.

Live trace re-verify (skills + error attributes actually landing on a real run) is the orchestrator's; this PR is implement + unit-test only. Do NOT merge.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Railway Preview Environment

Status Destroyed (PR closed)

Updated at 2026-06-25T20:23:14.960Z

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
sdks/python/agenta/sdk/agents/platform/connections.py (1)

61-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the resolver contract docs for the Claude exception.

ModelRef still documents “There is no model-id->provider table,” and RuntimeAuthContext still says the vault resolver is harness-agnostic. These lines intentionally add a Claude alias/id provider table and pass harness into resolution, so please update those docs to describe this narrow exception.

Also applies to: 403-411

services/agent/src/tracing/otel.ts (1)

1169-1203: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

recordError is only idempotent on the owned-span path.

The doc comment states it is idempotent, but in the span-less branch each call to recordError does tracer.startSpan("agent_error", ...), so a second invocation emits a duplicate agent_error span. This can happen if run.finish()/run.flush() throws after the swallowed-error path already called recordError (the catch in runSandboxAgent then calls it again). Consider a latch (e.g. an errorRecorded flag) so the standalone branch is one-shot.

services/agent/tests/unit/sandbox-agent-orchestration.test.ts (1)

125-130: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Fake output() mirrors finish(), so the swallowed-error guard divergence can't be covered here.

Both output() and finish() return options.output. The real output() (raw accumulated) and finish() (banner-stripped) can diverge — the exact gap noted on sandbox_agent.ts Line 425. Consider letting the fake return distinct values (e.g. options.rawOutput for output()) so a test can assert swallowed-error detection still fires when only a banner streamed.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a39f81bf-08e9-4640-9727-5f9d90f1cb86

📥 Commits

Reviewing files that changed from the base of the PR and between cb9de4c and 3a51244.

📒 Files selected for processing (13)
  • sdks/python/agenta/sdk/agents/connections/errors.py
  • sdks/python/agenta/sdk/agents/platform/connections.py
  • sdks/python/agenta/sdk/agents/utils/ts_runner.py
  • sdks/python/agenta/sdk/agents/utils/wire.py
  • sdks/python/oss/tests/pytest/unit/agents/platform/test_connections_http.py
  • sdks/python/oss/tests/pytest/unit/agents/test_wire_contract.py
  • services/agent/src/engines/sandbox_agent.ts
  • services/agent/src/engines/sandbox_agent/pi-assets.ts
  • services/agent/src/extensions/agenta.ts
  • services/agent/src/tracing/otel.ts
  • services/agent/tests/unit/otel-skills-error.test.ts
  • services/agent/tests/unit/sandbox-agent-orchestration.test.ts
  • services/agent/tests/unit/sandbox-agent-pi-assets.test.ts

Comment on lines 131 to +135
if response.status_code >= 400:
body = await response.aread()
raise RuntimeError(
f"Agent runner HTTP {response.status_code}: {body[:1000]!r}"
raise _transport_error(
f"Agent runner HTTP {response.status_code}",
detail=repr(body[:1000]),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path('sdks/python/agenta/sdk/agents/utils/ts_runner.py')
for start, end in [(1, 240)]:
    print(f'লines {start}-{end}')
    with path.open() as f:
        for i, line in enumerate(f, 1):
            if start <= i <= end:
                print(f"{i:4d}: {line.rstrip()}")
PY

Repository: Agenta-AI/agenta

Length of output: 9205


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
path = Path('sdks/python/agenta/sdk/agents/utils/ts_runner.py')
for start, end in [(1, 240)]:
    print(f"lines {start}-{end}")
    with path.open() as f:
        for i, line in enumerate(f, 1):
            if start <= i <= end:
                print(f"{i:4d}: {line.rstrip()}")
PY

Repository: Agenta-AI/agenta

Length of output: 9205


Avoid buffering the full streamed error body. await response.aread() still loads the whole error payload into memory even though only the first 1000 bytes are logged; read a bounded prefix instead and let the response close.

Comment on lines +59 to +65
message = raw.split("\n", 1)[0].strip()
# If even the first line is a raw stack frame, fall back to a generic line.
if not message or _STACK_FRAME_RE.match(message):
return "agent run failed"
if len(message) > _ERROR_MAX_LEN:
message = message[: _ERROR_MAX_LEN - 1].rstrip() + "…"
return message

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Single-line path leaks still pass through.

This only falls back when the first retained line starts with a stack-frame pattern. Errors like ENOENT: ... '/tmp/session/.claude/settings.json' or Cannot find module /app/... stay intact and will still be surfaced by result_from_wire(), which misses the PR’s “no stack/path leak” contract. Please redact path-like substrings in the kept line, or fall back whenever the retained line still contains one, and add a regression test for that case.

Also applies to: 132-135

Comment on lines +422 to +428
const swallowedPiError =
plan.isPi &&
!plan.isDaytona &&
!output.trim() &&
!run.output().trim() &&
!run.events().some((e) => e.type === "tool_call")
) {
const piError = findSwallowedPiError(plan.sourcePiAgentDir, plan.cwd);
if (piError) {
return {
ok: false,
error: conciseError(
new Error(piError),
plan.harness,
request.provider,
),
};
}
? findSwallowedPiError(plan.sourcePiAgentDir, plan.cwd)
: undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP -C3 'output\s*:\s*\(\)\s*=>\s*accumulated' services/agent/src/tracing/otel.ts
rg -nP -C3 'stripStartupBanner|splitLeadingBanner' services/agent/src/tracing/otel.ts
# Find where the startup banner originates and whether it is appended regardless of sink
rg -nP -C3 'banner' services/agent/src/tracing/otel.ts

Repository: Agenta-AI/agenta

Length of output: 6670


🏁 Script executed:

sed -n '1180,1285p' services/agent/src/tracing/otel.ts
sed -n '410,440p' services/agent/src/engines/sandbox_agent.ts

Repository: Agenta-AI/agenta

Length of output: 4691


🏁 Script executed:

sed -n '1180,1285p' services/agent/src/tracing/otel.ts
sed -n '410,440p' services/agent/src/engines/sandbox_agent.ts

Repository: Agenta-AI/agenta

Length of output: 4691


Compare against banner-stripped output here run.output().trim() still includes pi-acp’s startup banner, while finish() strips that banner before treating the turn as empty. A banner-only failed local-Pi run can skip findSwallowedPiError(...) and still return an empty ok:true turn. Use the same banner-stripped check as finish().

Comment on lines 466 to 476
} catch (err) {
const error = conciseError(err, plan.harness, request.provider);
// Stamp the error message + provider on the agent span before finishing it (F-030), so a
// trace carries the same diagnostic the response does (it previously held only a count).
otel?.recordError(error, request.provider);
otel?.finish();
await otel?.flush().catch(() => {});
return {
ok: false,
error: conciseError(err, plan.harness, request.provider),
error,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

otel?.finish() is unguarded in the catch path.

otel?.flush() swallows rejections via .catch(() => {}), but otel?.finish() can throw and would escape the catch block, replacing the sanitized error with a raw thrown exception (potentially leaking stack/path detail the sanitization step removed). Wrap it defensively.

🛡️ Proposed guard
-    otel?.recordError(error, request.provider);
-    otel?.finish();
-    await otel?.flush().catch(() => {});
+    otel?.recordError(error, request.provider);
+    try {
+      otel?.finish();
+    } catch {
+      // tracing teardown must not mask the sanitized error
+    }
+    await otel?.flush().catch(() => {});
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (err) {
const error = conciseError(err, plan.harness, request.provider);
// Stamp the error message + provider on the agent span before finishing it (F-030), so a
// trace carries the same diagnostic the response does (it previously held only a count).
otel?.recordError(error, request.provider);
otel?.finish();
await otel?.flush().catch(() => {});
return {
ok: false,
error: conciseError(err, plan.harness, request.provider),
error,
};
} catch (err) {
const error = conciseError(err, plan.harness, request.provider);
// Stamp the error message + provider on the agent span before finishing it (F-030), so a
// trace carries the same diagnostic the response does (it previously held only a count).
otel?.recordError(error, request.provider);
try {
otel?.finish();
} catch {
// tracing teardown must not mask the sanitized error
}
await otel?.flush().catch(() => {});
return {
ok: false,
error,
};

@mmabrouk
mmabrouk merged commit ca39ab2 into big-agents Jun 25, 2026
45 of 53 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant