Skip to content

feat: add @eggjs/agent-tracing package for AI agent tracing#412

Open
jerryliang64 wants to merge 3 commits intomasterfrom
worktree-feat-agent-tracing
Open

feat: add @eggjs/agent-tracing package for AI agent tracing#412
jerryliang64 wants to merge 3 commits intomasterfrom
worktree-feat-agent-tracing

Conversation

@jerryliang64
Copy link
Contributor

@jerryliang64 jerryliang64 commented Mar 18, 2026

Summary

  • Add new @eggjs/agent-tracing package for AI agent LLM call tracing
  • Support tracing LangGraph-based and Claude Agent SDK-based AI agent invocations with structured span/trace data
  • LangGraphTracer: extends LangChain BaseTracer, hooks into graph lifecycle events
  • ClaudeAgentTracer: converts Claude SDK messages to LangChain Run format, supports both batch and streaming modes
  • TracingService: shared log formatting, OSS upload, and log service sync
  • AbstractOssClient/AbstractLogServiceClient: IoC injection interfaces for pluggable backends

Adapted from eggjs/egg#5822 for tegg monorepo conventions (CJS module system, mocha tests, lerna versioning).

Test plan

  • All 52 mocha tests passing (npm test --workspace=core/agent-tracing)
  • TypeScript type checking passes (tsc --noEmit)
  • Package structure follows existing core/* conventions

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added comprehensive tracing for Claude agents and LangGraph workflows with configurable agent naming.
    • Optional OSS uploads and log-service syncing for large trace fields and centralized trace management.
    • Published a new package with root and subpath exports for tracer consumers.
  • Tests

    • Added extensive unit and integration tests covering tracer behavior, sessions, env handling, and error cases.
  • Chores

    • Added TypeScript build configs and package manifest for the new tracing package.

Add tracing support for AI agents built with LangGraph and Claude Agent SDK.

- LangGraphTracer: extends LangChain BaseTracer, hooks into graph lifecycle
- ClaudeAgentTracer: converts Claude SDK messages to LangChain Run format
- TraceSession: streaming support for real-time message processing
- TracingService: shared log formatting, OSS upload, and log service sync
- AbstractOssClient/AbstractLogServiceClient: IoC injection interfaces
- Comprehensive mocha tests (52 passing)

Adapted from eggjs/egg#5822 for tegg monorepo conventions (CJS, mocha, lerna).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a dedicated tracing package for AI agents, enhancing observability by capturing detailed execution flows from LangGraph and Claude Agent SDK interactions. It centralizes logging, data storage, and external service integration through a flexible architecture, allowing for custom implementations of OSS and log services. The primary goal is to provide developers with better insights into AI agent behavior and performance.

Highlights

  • New Package Introduction: A new package, @eggjs/agent-tracing, has been added to provide comprehensive tracing capabilities for AI agents within the tegg monorepo.
  • AI Agent Tracing Support: The package supports tracing invocations from both LangGraph-based and Claude Agent SDK-based AI agents, capturing structured span and trace data.
  • LangGraph Integration: A LangGraphTracer is included, extending LangChain's BaseTracer to hook into graph lifecycle events for detailed tracing.
  • Claude Agent SDK Integration: A ClaudeAgentTracer is provided to convert Claude SDK messages into the LangChain Run format, supporting both batch and streaming modes for agent interactions.
  • Centralized Tracing Service: A TracingService handles common tracing operations such as log formatting, uploading large trace data to OSS, and syncing logs to a log service.
  • Pluggable Backends: Abstract interfaces, AbstractOssClient and AbstractLogServiceClient, allow for dependency injection of custom OSS and log service implementations, ensuring flexibility.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a new package @eggjs/agent-tracing for AI agent tracing, which is a significant and well-architected feature. It supports both LangGraph and Claude Agent SDKs, with a clear separation of concerns between tracers and a shared TracingService. The code is well-documented and comes with a comprehensive test suite, which is excellent. My review focuses on improving type safety in a few areas to enhance maintainability.

Comment on lines +370 to +386
const textBlocks = content.filter((c) => c.type === 'text');
const toolBlocks = content.filter((c) => c.type === 'tool_use');

const inputs = {
messages: textBlocks.map((c) => (c as any).text).filter(Boolean),
};

const outputs: any = {};
if (isToolCall) {
outputs.tool_calls = toolBlocks.map((c) => ({
id: (c as any).id,
name: (c as any).name,
input: (c as any).input,
}));
} else {
outputs.content = textBlocks.map((c) => (c as any).text).join('');
}
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

Using a type guard with .filter() can eliminate the need for as any casts, improving type safety and readability. You'll need to add ClaudeTextContent and ClaudeToolUseContent to your imports from ./types for this to work.

Suggested change
const textBlocks = content.filter((c) => c.type === 'text');
const toolBlocks = content.filter((c) => c.type === 'tool_use');
const inputs = {
messages: textBlocks.map((c) => (c as any).text).filter(Boolean),
};
const outputs: any = {};
if (isToolCall) {
outputs.tool_calls = toolBlocks.map((c) => ({
id: (c as any).id,
name: (c as any).name,
input: (c as any).input,
}));
} else {
outputs.content = textBlocks.map((c) => (c as any).text).join('');
}
const textBlocks = content.filter((c): c is ClaudeTextContent => c.type === 'text');
const toolBlocks = content.filter((c): c is ClaudeToolUseContent => c.type === 'tool_use');
const inputs = {
messages: textBlocks.map(c => c.text).filter(Boolean),
};
const outputs: any = {};
if (isToolCall) {
outputs.tool_calls = toolBlocks.map(c => ({
id: c.id,
name: c.name,
input: c.input,
}));
} else {
outputs.content = textBlocks.map(c => c.text).join('');
}

Comment on lines +417 to +424
createToolRunStartInternal(
toolUseBlock: ClaudeContentBlock,
rootRunId: string,
traceId: string,
order: number,
startTime: number,
): Run {
const toolUse = toolUseBlock as any;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The toolUseBlock parameter can be typed as ClaudeToolUseContent instead of ClaudeContentBlock. The call site in handleAssistant already performs a type check (block.type === 'tool_use'), so this change is safe and removes the need for the as any cast, improving type safety. You'll need to import ClaudeToolUseContent from ./types.

Suggested change
createToolRunStartInternal(
toolUseBlock: ClaudeContentBlock,
rootRunId: string,
traceId: string,
order: number,
startTime: number,
): Run {
const toolUse = toolUseBlock as any;
createToolRunStartInternal(
toolUseBlock: ClaudeToolUseContent,
rootRunId: string,
traceId: string,
order: number,
startTime: number,
): Run {
const toolUse = toolUseBlock;

Comment on lines +455 to +456
completeToolRunInternal(toolRun: Run, toolResultBlock: ClaudeContentBlock, startTime: number): void {
const result = toolResultBlock as any;
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

The toolResultBlock parameter can be typed as ClaudeToolResultContent. The call site in handleUser ensures this with block.type === 'tool_result'. This change removes the as any cast and improves type safety. You'll need to import ClaudeToolResultContent from ./types.

Suggested change
completeToolRunInternal(toolRun: Run, toolResultBlock: ClaudeContentBlock, startTime: number): void {
const result = toolResultBlock as any;
completeToolRunInternal(toolRun: Run, toolResultBlock: ClaudeToolResultContent, startTime: number): void {
const result = toolResultBlock;

…spacing)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Mar 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 09ca6f87-a18f-42c0-88f3-87ae4e04b32c

📥 Commits

Reviewing files that changed from the base of the PR and between 8216848 and 3083cbb.

📒 Files selected for processing (1)
  • core/agent-tracing/package.json
✅ Files skipped from review due to trivial changes (1)
  • core/agent-tracing/package.json

📝 Walkthrough

Walkthrough

Adds a new @eggjs/agent-tracing package implementing Claude and LangGraph tracers, a centralized TracingService, abstract OSS/log client interfaces, types, entrypoints (root/claude/langgraph), and comprehensive tests for streaming and batch tracing flows.

Changes

Cohort / File(s) Summary
Package manifest & build
core/agent-tracing/package.json, core/agent-tracing/tsconfig.json, core/agent-tracing/tsconfig.pub.json
New package manifest, exports for root/claude/langgraph, dependencies/peerDependencies, build/test scripts, and TypeScript configs.
Public entrypoints
core/agent-tracing/index.ts, core/agent-tracing/claude.ts, core/agent-tracing/langgraph.ts
Added module barrels re-exporting types and named tracer exports to expose public API surface and subpath imports.
Tracers
core/agent-tracing/src/ClaudeAgentTracer.ts, core/agent-tracing/src/LangGraphTracer.ts
Implemented ClaudeAgentTracer (TraceSession, message conversion, session lifecycle) and LangGraphTracer (LangChain lifecycle hooks → TracingService).
Core service & types
core/agent-tracing/src/TracingService.ts, core/agent-tracing/src/types.ts
Added TracingService for logging, OSS upload, and local sync; defined Claude message shapes, run/cost/resource types, and tracer config helpers.
Abstract integrations
core/agent-tracing/src/AbstractOssClient.ts, core/agent-tracing/src/AbstractLogServiceClient.ts
Added abstract client interfaces for optional OSS uploads and log-service syncing.
Tests & test utils
core/agent-tracing/test/*, core/agent-tracing/test/TestUtils.ts
Comprehensive Jest suites for ClaudeAgentTracer, LangGraphTracer, TracingService, configure behavior, plus test utilities and mocks.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client
    participant Session as TraceSession
    participant Tracer as ClaudeAgentTracer
    participant Service as TracingService
    participant OSS as OSS
    participant LogSvc as LogService

    Client->>Session: processMessage(systemInit)
    Session->>Tracer: convertSDKMessage / start root run
    Tracer->>Service: logTrace(rootRun, START)

    Client->>Session: processMessage(assistant / tool-use)
    Session->>Tracer: convertSDKMessage / create llm/tool runs (START)
    Tracer->>Service: logTrace(childRuns, START)

    Client->>Session: processMessage(user with tool_result)
    Session->>Tracer: complete pending tool runs (END)
    Tracer->>Service: logTrace(toolRuns, END)

    Client->>Session: processMessage(result)
    Session->>Tracer: finalize root run (END/ERROR)
    Tracer->>Service: logTrace(rootRun, END/ERROR)

    Service->>OSS: uploadToOss(key, largeField) (async)
    OSS-->>Service: success/skip
    Service->>LogSvc: syncLocalToLogService(log) [if local env]
    LogSvc-->>Service: success/skip
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

enhancement

Suggested reviewers

  • akitaSummer
  • killagu
  • fengmk2

Poem

🐰 I hop through traces, bright and keen,
Tracking Claude and LangGraph scenes,
Sessions start, tools jump and end,
Logs fly off—OSS and friend,
Cheers! A rabbit's tracing dream.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a new @eggjs/agent-tracing package for AI agent tracing.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-feat-agent-tracing
📝 Coding Plan
  • Generate coding plan for human review comments

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 and usage tips.

…ailable)

@langchain/core requires ReadableStream which is only globally available
in Node.js >= 18. Follow the same pattern as core/vitest.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link

@coderabbitai coderabbitai bot left a comment

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 (8)
core/agent-tracing/tsconfig.json (1)

1-12: Consider consolidating duplicate TypeScript configs.

Both tsconfig.json and tsconfig.pub.json have identical content. If these configs serve distinct purposes (e.g., dev vs. publish), consider documenting the distinction. Otherwise, one could extend the other to reduce duplication.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/agent-tracing/tsconfig.json` around lines 1 - 12, The two identical
TypeScript configs (tsconfig.json and tsconfig.pub.json) should be consolidated:
pick one as the canonical config (e.g., tsconfig.json) and have the other extend
it (using "extends") or delete the duplicate, and add a brief comment or README
entry describing their distinct purpose if they must remain; update the
"compilerOptions" and "exclude" usage accordingly so only the canonical file
contains the full settings and the secondary file merely references it to avoid
duplication.
core/agent-tracing/test/TestUtils.ts (2)

4-4: Import TracingService is only used for type casting.

The import on line 4 is only used in as unknown as TracingService type assertion. Consider using a type-only import to make the intent clearer and potentially improve tree-shaking.

Suggested change
-import { TracingService } from '../src/TracingService';
+import type { TracingService } from '../src/TracingService';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/agent-tracing/test/TestUtils.ts` at line 4, The import of TracingService
in TestUtils.ts is only used for a type assertion (as unknown as
TracingService); change it to a type-only import to clarify intent and aid
bundlers/tree-shaking by replacing the current import with "import type {
TracingService } from '../src/TracingService';" and keep the existing type
assertion usage unchanged (referencing TracingService) so runtime behavior is
unaffected.

35-47: Mock logger may be missing debug method.

The mock logger implements info, warn, and error, but many Logger interfaces also expect a debug method. If tests exercise code paths that call logger.debug(), this could cause runtime errors.

Consider adding debug method
 export function createMockLogger(logs?: string[]): Logger {
   return {
+    debug: (msg: string) => {
+      logs?.push(msg);
+    },
     info: (msg: string) => {
       logs?.push(msg);
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/agent-tracing/test/TestUtils.ts` around lines 35 - 47, The mock logger
returned by createMockLogger lacks a debug method which can cause runtime errors
when code calls logger.debug; add a debug: (msg: string) => { logs?.push(msg); }
implementation to the returned object (alongside info/warn/error) so the mock
fully implements the expected Logger shape, then keep the existing "as unknown
as Logger" cast or update typing if desired.
core/agent-tracing/test/TracingService.test.ts (1)

258-262: Log parsing regex may be fragile to format changes.

The regex ,run=({.*})$ assumes the run JSON is always the last element in the log line. If the log format changes, these tests will silently fail to extract the JSON rather than failing explicitly.

Consider extracting this pattern to a helper function with clearer error handling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/agent-tracing/test/TracingService.test.ts` around lines 258 - 262, The
test's inline regex extraction using /,run=({.*})$/ is fragile; replace it with
a dedicated helper (e.g., extractRunJsonFromLog) that searches for the `,run=`
key more robustly (non-greedy, supports trailing text) and returns parsed JSON
or throws a clear assertion error; update the test to call this helper instead
of using the ad-hoc `runJson` regex so failures explicitly surface (refer to
variables/functions: runJson, parsed, and the JSON parsing/assertion logic in
TracingService.test.ts).
core/agent-tracing/src/types.ts (1)

126-127: FIELDS_TO_OSS definition and export are separated.

The constant is defined on line 126 but exported on line 147. While this works, co-locating the export with the definition would improve readability.

Suggested consolidation
-const FIELDS_TO_OSS = [ 'inputs', 'outputs', 'attachments', 'serialized', 'events' ] as const;
+export const FIELDS_TO_OSS = [ 'inputs', 'outputs', 'attachments', 'serialized', 'events' ] as const;
 ...
-export { FIELDS_TO_OSS };

Also applies to: 147-147

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/agent-tracing/src/types.ts` around lines 126 - 127, The FIELDS_TO_OSS
constant is defined separately from its export; inline the export by changing
the standalone definition to an exported declaration (e.g., export const
FIELDS_TO_OSS = ...) and remove the later separate export statement to co-locate
definition and export for readability; ensure no other code relies on the
original separate export name and update imports/uses if necessary (symbol:
FIELDS_TO_OSS).
core/agent-tracing/src/TracingService.ts (1)

56-56: Complex ternary could be clearer.

The expression process.env.FAAS_ENV || env === 'local' ? '' : \env=${env},`relies on operator precedence that may confuse readers. The||binds tighter than?:, so this evaluates as (process.env.FAAS_ENV || env === 'local') ? '' : ...`.

Suggested clarification
-    const envSegment = process.env.FAAS_ENV || env === 'local' ? '' : `env=${env},`;
+    const shouldOmitEnv = process.env.FAAS_ENV || env === 'local';
+    const envSegment = shouldOmitEnv ? '' : `env=${env},`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/agent-tracing/src/TracingService.ts` at line 56, The ternary computing
envSegment in TracingService is hard to read due to mixed || and ?: precedence;
make the intent explicit by rewriting the condition (e.g., wrap the boolean
check in parentheses or use an if/else) so it's clear you're checking whether
FAAS_ENV is set or env === 'local' before choosing '' or `env=${env},`; update
the expression that assigns envSegment to use (process.env.FAAS_ENV || env ===
'local') ? '' : `env=${env},` or an equivalent if/else to improve readability.
core/agent-tracing/test/LangGraphTracer.test.ts (1)

55-56: Hardcoded sleep(500) introduces potential test flakiness.

Multiple tests use await sleep(500) to wait for async tracing operations. This can lead to flaky tests on slow CI runners or unnecessary delays on fast machines. Consider using a polling/retry approach or exposing a flush mechanism on the tracer.

Example polling approach
async function waitForCapturedRuns(
  capturedRuns: CapturedEntry[],
  minCount: number,
  timeoutMs = 2000
): Promise<void> {
  const start = Date.now();
  while (capturedRuns.length < minCount && Date.now() - start < timeoutMs) {
    await sleep(50);
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/agent-tracing/test/LangGraphTracer.test.ts` around lines 55 - 56, The
test currently uses a hardcoded await sleep(500) in LangGraphTracer.test.ts
which causes flakiness; replace this with a deterministic wait by polling the
tracer output or using a flush API: implement and call a helper like
waitForCapturedRuns(capturedRuns, expectedCount, timeoutMs) that loops with
short sleeps until capturedRuns.length >= expectedCount (or call
tracer.flush()/await tracer.waitForFlush() if available), and remove the fixed
sleep(500) so the test waits only until the tracer work is actually done.
core/agent-tracing/src/LangGraphTracer.ts (1)

77-82: Missing onAgentError hook for symmetry with other hook groups.

All other hook groups have START/END/ERROR variants (onChainError, onToolError, onLLMError, onRetrieverError), but the agent hooks only have onAgentAction (START) and onAgentEnd (END). Consider adding onAgentError if the base tracer supports it, to ensure error states during agent execution are also captured.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/agent-tracing/src/LangGraphTracer.ts` around lines 77 - 82, Add a
symmetric error hook for agents by implementing an onAgentError(run: Run): void
| Promise<void> method that mirrors onAgentAction and onAgentEnd; call
this.logTrace(run, RunStatus.ERROR) inside it (use the same Run type and
RunStatus enum as in onAgentAction/onAgentEnd). Ensure the base tracer or
interface supports onAgentError before adding the method so it correctly
overrides the hook.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@core/agent-tracing/src/ClaudeAgentTracer.ts`:
- Around line 377-386: The current branch that sets outputs when isToolCall only
writes outputs.tool_calls and drops any companion assistant text parsed into
textBlocks; update the branch in ClaudeAgentTracer.ts (the block that builds
outputs, using variables isToolCall, toolBlocks, and textBlocks) so that when
isToolCall is true you still populate outputs.content with textBlocks.map(c =>
c.text).join('') if textBlocks is non-empty (i.e., include both
outputs.tool_calls and outputs.content), ensuring the span preserves assistant
text alongside tool call entries.
- Around line 154-159: The root result span is dropping structured_output from
SDKResultMessage; update the assignments that set outputs so they preserve
structured_output when present — specifically extend this.rootRun.outputs (in
ClaudeAgentTracer where startTime/end_time are set) to include structured_output
when message.structured_output exists, and likewise update the
normalizeSdkMessage function to copy structured_output into the normalized
output object for success variants; reference the this.rootRun.outputs
assignment and normalizeSdkMessage so both places include structured_output
alongside result, is_error, and num_turns.
- Around line 75-120: convertSDKMessage currently drops
SDKAssistantMessage.error and handleAssistant only creates runs for 'tool_use'
or 'text', so assistant messages that carry only an error are lost; update
convertSDKMessage to extract and retain the optional error property from
SDKAssistantMessage into the converted message, and update handleAssistant to
detect when a message has no tool_use/text but does have an error and emit an
error span/run (use the tracer APIs analogous to
createLLMRunInternal/createToolRunStartInternal—e.g., create an LLM error run or
a dedicated error run via your tracer, attach the error details to the run, push
it into this.rootRun.child_runs, and call this.tracer.logTrace with an error
status like RunStatus.ERROR) so assistant-side SDK errors surface in traces.
- Around line 123-136: handleUser currently only iterates
message.message.content for tool_result blocks and ignores a top-level
SDKUserMessage.tool_use_result; update either the SDKUserMessage->ClaudeMessage
conversion to include the top-level tool_use_result into ClaudeMessage (so it
appears in message.message.content) or extend handleUser to explicitly check
message.tool_use_result and treat it the same as a tool_result block: look up
the tool run via pendingToolUses.get(tool_use_id), call
tracer.completeToolRunInternal(toolRun, toolUseResult, Date.now()), call
tracer.logTrace(toolRun, appropriate RunStatus), and delete the pendingToolUses
entry; reference functions/types: handleUser, ClaudeMessage, SDKUserMessage,
tool_use_result, pendingToolUses, tracer.completeToolRunInternal,
tracer.logTrace.

---

Nitpick comments:
In `@core/agent-tracing/src/LangGraphTracer.ts`:
- Around line 77-82: Add a symmetric error hook for agents by implementing an
onAgentError(run: Run): void | Promise<void> method that mirrors onAgentAction
and onAgentEnd; call this.logTrace(run, RunStatus.ERROR) inside it (use the same
Run type and RunStatus enum as in onAgentAction/onAgentEnd). Ensure the base
tracer or interface supports onAgentError before adding the method so it
correctly overrides the hook.

In `@core/agent-tracing/src/TracingService.ts`:
- Line 56: The ternary computing envSegment in TracingService is hard to read
due to mixed || and ?: precedence; make the intent explicit by rewriting the
condition (e.g., wrap the boolean check in parentheses or use an if/else) so
it's clear you're checking whether FAAS_ENV is set or env === 'local' before
choosing '' or `env=${env},`; update the expression that assigns envSegment to
use (process.env.FAAS_ENV || env === 'local') ? '' : `env=${env},` or an
equivalent if/else to improve readability.

In `@core/agent-tracing/src/types.ts`:
- Around line 126-127: The FIELDS_TO_OSS constant is defined separately from its
export; inline the export by changing the standalone definition to an exported
declaration (e.g., export const FIELDS_TO_OSS = ...) and remove the later
separate export statement to co-locate definition and export for readability;
ensure no other code relies on the original separate export name and update
imports/uses if necessary (symbol: FIELDS_TO_OSS).

In `@core/agent-tracing/test/LangGraphTracer.test.ts`:
- Around line 55-56: The test currently uses a hardcoded await sleep(500) in
LangGraphTracer.test.ts which causes flakiness; replace this with a
deterministic wait by polling the tracer output or using a flush API: implement
and call a helper like waitForCapturedRuns(capturedRuns, expectedCount,
timeoutMs) that loops with short sleeps until capturedRuns.length >=
expectedCount (or call tracer.flush()/await tracer.waitForFlush() if available),
and remove the fixed sleep(500) so the test waits only until the tracer work is
actually done.

In `@core/agent-tracing/test/TestUtils.ts`:
- Line 4: The import of TracingService in TestUtils.ts is only used for a type
assertion (as unknown as TracingService); change it to a type-only import to
clarify intent and aid bundlers/tree-shaking by replacing the current import
with "import type { TracingService } from '../src/TracingService';" and keep the
existing type assertion usage unchanged (referencing TracingService) so runtime
behavior is unaffected.
- Around line 35-47: The mock logger returned by createMockLogger lacks a debug
method which can cause runtime errors when code calls logger.debug; add a debug:
(msg: string) => { logs?.push(msg); } implementation to the returned object
(alongside info/warn/error) so the mock fully implements the expected Logger
shape, then keep the existing "as unknown as Logger" cast or update typing if
desired.

In `@core/agent-tracing/test/TracingService.test.ts`:
- Around line 258-262: The test's inline regex extraction using /,run=({.*})$/
is fragile; replace it with a dedicated helper (e.g., extractRunJsonFromLog)
that searches for the `,run=` key more robustly (non-greedy, supports trailing
text) and returns parsed JSON or throws a clear assertion error; update the test
to call this helper instead of using the ad-hoc `runJson` regex so failures
explicitly surface (refer to variables/functions: runJson, parsed, and the JSON
parsing/assertion logic in TracingService.test.ts).

In `@core/agent-tracing/tsconfig.json`:
- Around line 1-12: The two identical TypeScript configs (tsconfig.json and
tsconfig.pub.json) should be consolidated: pick one as the canonical config
(e.g., tsconfig.json) and have the other extend it (using "extends") or delete
the duplicate, and add a brief comment or README entry describing their distinct
purpose if they must remain; update the "compilerOptions" and "exclude" usage
accordingly so only the canonical file contains the full settings and the
secondary file merely references it to avoid duplication.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f911f5c1-585b-44fa-a929-d50c908fdd34

📥 Commits

Reviewing files that changed from the base of the PR and between d4d0006 and 8216848.

📒 Files selected for processing (17)
  • core/agent-tracing/claude.ts
  • core/agent-tracing/index.ts
  • core/agent-tracing/langgraph.ts
  • core/agent-tracing/package.json
  • core/agent-tracing/src/AbstractLogServiceClient.ts
  • core/agent-tracing/src/AbstractOssClient.ts
  • core/agent-tracing/src/ClaudeAgentTracer.ts
  • core/agent-tracing/src/LangGraphTracer.ts
  • core/agent-tracing/src/TracingService.ts
  • core/agent-tracing/src/types.ts
  • core/agent-tracing/test/ClaudeAgentTracer.test.ts
  • core/agent-tracing/test/Configure.test.ts
  • core/agent-tracing/test/LangGraphTracer.test.ts
  • core/agent-tracing/test/TestUtils.ts
  • core/agent-tracing/test/TracingService.test.ts
  • core/agent-tracing/tsconfig.json
  • core/agent-tracing/tsconfig.pub.json

Comment on lines +75 to +120
const content = message.message?.content || [];
const hasToolUse = content.some(c => c.type === 'tool_use');
const hasText = content.some(c => c.type === 'text');

if (hasToolUse) {
const eventTime = Date.now();
// Create LLM run that initiated tool calls
const llmRun = this.tracer.createLLMRunInternal(
message,
this.rootRunId,
this.traceId,
this.executionOrder++,
eventTime,
true,
);
this.rootRun.child_runs.push(llmRun);
this.tracer.logTrace(llmRun, RunStatus.END);

// Create tool runs (will be completed when tool_result arrives)
for (const block of content) {
if (block.type === 'tool_use') {
const toolRun = this.tracer.createToolRunStartInternal(
block,
this.rootRunId,
this.traceId,
this.executionOrder++,
eventTime,
);
this.rootRun.child_runs.push(toolRun);
this.pendingToolUses.set(block.id, toolRun);
this.tracer.logTrace(toolRun, RunStatus.START);
}
}
} else if (hasText) {
// Text-only response
const llmRun = this.tracer.createLLMRunInternal(
message,
this.rootRunId,
this.traceId,
this.executionOrder++,
Date.now(),
false,
);
this.rootRun.child_runs.push(llmRun);
this.tracer.logTrace(llmRun, RunStatus.END);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What fields are defined on SDKAssistantMessagein the Anthropic Agent SDK TypeScript API, and does it include an optionalerror property?

💡 Result:

The SDKAssistantMessage interface in the Anthropic Agent SDK TypeScript API (@anthropic-ai/claude-agent-sdk) defines the following fields: - type: "assistant" - uuid: UUID - session_id: string - message: BetaMessage (from Anthropic SDK, includes id, content, model, stop_reason, usage) - parent_tool_use_id: string | null - error?: SDKAssistantMessageError (optional) Yes, it includes an optional error property of type SDKAssistantMessageError, which can be one of: 'authentication_failed', 'billing_error', 'rate_limit', 'invalid_request', 'server_error', 'max_output_tokens', or 'unknown'. This is from the official Anthropic documentation, with the latest SDK release v0.2.79 on 2026-03-18.

Citations:


🏁 Script executed:

# Locate the ClaudeAgentTracer.ts file
fd -t f "ClaudeAgentTracer.ts"

Repository: eggjs/tegg

Length of output: 97


🏁 Script executed:

# Search for convertSDKMessage method
rg -n "convertSDKMessage" --type ts

Repository: eggjs/tegg

Length of output: 273


🏁 Script executed:

# Get the full ClaudeAgentTracer.ts file with line numbers
fd -t f "ClaudeAgentTracer.ts" -x cat -n {}

Repository: eggjs/tegg

Length of output: 19124


Handle assistant message errors in the trace.

The convertSDKMessage() method drops the optional error property from SDKAssistantMessage (lines 269-276), and handleAssistant() only emits runs when there is tool_use or text content. If an assistant turn fails with an error but produces neither content type, the message vanishes from the trace entirely. Add error handling to surface assistant-side SDK errors (authentication failures, billing errors, rate limits, etc.) as error spans.

Affected code locations

Lines 269-276 (convertSDKMessage): Error property not extracted
Lines 75-120 (handleAssistant): No error case handling

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/agent-tracing/src/ClaudeAgentTracer.ts` around lines 75 - 120,
convertSDKMessage currently drops SDKAssistantMessage.error and handleAssistant
only creates runs for 'tool_use' or 'text', so assistant messages that carry
only an error are lost; update convertSDKMessage to extract and retain the
optional error property from SDKAssistantMessage into the converted message, and
update handleAssistant to detect when a message has no tool_use/text but does
have an error and emit an error span/run (use the tracer APIs analogous to
createLLMRunInternal/createToolRunStartInternal—e.g., create an LLM error run or
a dedicated error run via your tracer, attach the error details to the run, push
it into this.rootRun.child_runs, and call this.tracer.logTrace with an error
status like RunStatus.ERROR) so assistant-side SDK errors surface in traces.

Comment on lines +123 to +136
private handleUser(message: ClaudeMessage): void {
if (!message.message?.content) return;

for (const block of message.message.content) {
if (block.type === 'tool_result') {
const toolRun = this.pendingToolUses.get(block.tool_use_id);
if (toolRun) {
this.tracer.completeToolRunInternal(toolRun, block, Date.now());
const status = block.is_error ? RunStatus.ERROR : RunStatus.END;
this.tracer.logTrace(toolRun, status);
this.pendingToolUses.delete(block.tool_use_id);
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Does SDKUserMessagein the Anthropic Agent SDK TypeScript API include a top-leveltool_use_result, separate from message.content?

💡 Result:

Yes, SDKUserMessage in the Anthropic Claude Agent SDK TypeScript API includes a top-level tool_use_result?: unknown field, separate from message.content. The full type definition from official documentation is: typescript type SDKUserMessage = { type: "user"; uuid?: UUID; session_id: string; message: MessageParam; // From Anthropic SDK parent_tool_use_id: string | null; isSynthetic?: boolean; tool_use_result?: unknown; }; This tool_use_result field allows providing tool execution results back to the agent as a user message, distinct from the content within message (which is of type MessageParam from the core Anthropic SDK). It supports the agent loop where users supply tool results after Claude requests tool use. The field is also present in related types like SDKUserMessageReplay. Sources confirm this structure in the Agent SDK reference docs and code snippets from Anthropic's documentation (updated as of 2026).

Citations:


🏁 Script executed:

find . -type f -name "ClaudeAgentTracer.ts" | head -5

Repository: eggjs/tegg

Length of output: 99


🏁 Script executed:

wc -l ./core/agent-tracing/src/ClaudeAgentTracer.ts

Repository: eggjs/tegg

Length of output: 103


🏁 Script executed:

cat -n ./core/agent-tracing/src/ClaudeAgentTracer.ts

Repository: eggjs/tegg

Length of output: 19124


🏁 Script executed:

head -20 ./core/agent-tracing/src/types.ts

Repository: eggjs/tegg

Length of output: 438


🏁 Script executed:

cat -n ./core/agent-tracing/src/types.ts

Repository: eggjs/tegg

Length of output: 4511


Extract and process top-level tool_use_result from SDKUserMessage in tool span completion.

The Anthropic Agent SDK's SDKUserMessage includes a top-level tool_use_result field separate from message.content. Currently, lines 280-287 convert SDKUserMessage to ClaudeMessage but do not extract the tool_use_result field (though the type definition supports it at line 102 of types.ts). This causes any data passed via tool_use_result to be silently discarded rather than included in the tool span output.

The handleUser() method (lines 123-136) only processes tool_result blocks from message.message.content, never checking for a top-level tool_use_result value.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/agent-tracing/src/ClaudeAgentTracer.ts` around lines 123 - 136,
handleUser currently only iterates message.message.content for tool_result
blocks and ignores a top-level SDKUserMessage.tool_use_result; update either the
SDKUserMessage->ClaudeMessage conversion to include the top-level
tool_use_result into ClaudeMessage (so it appears in message.message.content) or
extend handleUser to explicitly check message.tool_use_result and treat it the
same as a tool_result block: look up the tool run via
pendingToolUses.get(tool_use_id), call tracer.completeToolRunInternal(toolRun,
toolUseResult, Date.now()), call tracer.logTrace(toolRun, appropriate
RunStatus), and delete the pendingToolUses entry; reference functions/types:
handleUser, ClaudeMessage, SDKUserMessage, tool_use_result, pendingToolUses,
tracer.completeToolRunInternal, tracer.logTrace.

Comment on lines +154 to +159
this.rootRun.end_time = this.startTime + (message.duration_ms || 0);
this.rootRun.outputs = {
result: message.result,
is_error: message.is_error,
num_turns: message.num_turns,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Can SDKResultMessagein the Anthropic Agent SDK TypeScript API includestructured_output, and on which result variant(s)?

💡 Result:

Yes, SDKResultMessage in the Anthropic Agent SDK TypeScript API can include a structured_output field of type unknown? (optional). It appears only in the "success" variant (subtype: "success"). Error variants (subtypes: "error_max_turns", "error_during_execution", "error_max_budget_usd", "error_max_structured_output_retries") do not include this field, based on the official type definition.

Citations:


🏁 Script executed:

cd core/agent-tracing && find . -name "ClaudeAgentTracer.ts" -type f

Repository: eggjs/tegg

Length of output: 80


🏁 Script executed:

cat -n core/agent-tracing/src/ClaudeAgentTracer.ts | sed -n '150,165p'

Repository: eggjs/tegg

Length of output: 659


🏁 Script executed:

cat -n core/agent-tracing/src/ClaudeAgentTracer.ts | sed -n '285,315p'

Repository: eggjs/tegg

Length of output: 1304


Preserve structured_output on the root result span.

The SDKResultMessage can include structured_output in the success variant, but both locations drop it:

  • Lines 154–159: rootRun.outputs extracts only result, is_error, num_turns
  • Lines 291–307: normalizeSdkMessage normalizes the message without including structured_output

Add structured_output to the output objects when present in success variants to avoid losing machine-readable payloads.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/agent-tracing/src/ClaudeAgentTracer.ts` around lines 154 - 159, The root
result span is dropping structured_output from SDKResultMessage; update the
assignments that set outputs so they preserve structured_output when present —
specifically extend this.rootRun.outputs (in ClaudeAgentTracer where
startTime/end_time are set) to include structured_output when
message.structured_output exists, and likewise update the normalizeSdkMessage
function to copy structured_output into the normalized output object for success
variants; reference the this.rootRun.outputs assignment and normalizeSdkMessage
so both places include structured_output alongside result, is_error, and
num_turns.

Comment on lines +377 to +386
const outputs: any = {};
if (isToolCall) {
outputs.tool_calls = toolBlocks.map(c => ({
id: (c as any).id,
name: (c as any).name,
input: (c as any).input,
}));
} else {
outputs.content = textBlocks.map(c => (c as any).text).join('');
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Keep assistant text alongside tool calls.

If the same assistant message has both text and tool_use blocks, this branch records only outputs.tool_calls. The companion text is parsed into textBlocks, but never makes it into the span output.

💡 Proposed fix
     const outputs: any = {};
     if (isToolCall) {
+      const contentText = textBlocks.map(c => (c as any).text).join('');
       outputs.tool_calls = toolBlocks.map(c => ({
         id: (c as any).id,
         name: (c as any).name,
         input: (c as any).input,
       }));
+      if (contentText) {
+        outputs.content = contentText;
+      }
     } else {
       outputs.content = textBlocks.map(c => (c as any).text).join('');
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@core/agent-tracing/src/ClaudeAgentTracer.ts` around lines 377 - 386, The
current branch that sets outputs when isToolCall only writes outputs.tool_calls
and drops any companion assistant text parsed into textBlocks; update the branch
in ClaudeAgentTracer.ts (the block that builds outputs, using variables
isToolCall, toolBlocks, and textBlocks) so that when isToolCall is true you
still populate outputs.content with textBlocks.map(c => c.text).join('') if
textBlocks is non-empty (i.e., include both outputs.tool_calls and
outputs.content), ensuring the span preserves assistant text alongside tool call
entries.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant