Skip to content

docs: Node.js inbound trace-context example calls session.registerTool(), which is not a public API #2222

Description

@examon

Summary

The Node.js example in the CLI -> SDK (inbound) part of
docs/observability/opentelemetry.md
shows how a tool handler can restore the CLI's W3C trace context. It registers the tool like this:

session.registerTool(myTool, async (args, invocation) => { ... });

registerTool is not a member of the public CopilotSession type. Copying the example into a
TypeScript project fails to compile, and if the types are bypassed it throws at runtime.

The plural registerTools() does exist on the class, but it is marked @internal and the package is
built with stripInternal: true, so it is not in the shipped declarations either. There is no public
session method for registering a tool after the session exists, so a reader cannot repair the example
by switching to the plural form - the whole registration step has to be restructured.

The trace-restoration logic in the example is fine. ToolInvocation.traceparent and
ToolInvocation.tracestate are real, public, and correctly delivered to handlers. Only the
registration mechanism is wrong.

Reproduction

Against the published package, no CLI, auth, model call or network needed beyond the install:

mkdir repro && cd repro
printf '{"name":"repro","version":"0.0.0","private":true,"type":"module"}\n' > package.json
npm install @github/copilot-sdk@1.0.8 typescript@5.9.3 @opentelemetry/api@1.9.1

repro.ts - the documented snippet, with only the scaffolding the surrounding docs supply
out-of-band (session, myTool and doWork) added so it is a self-contained compilation unit.
The SDK-facing lines are the documentation's, unchanged:

import type { CopilotSession } from "@github/copilot-sdk";
declare const session: CopilotSession;
declare const myTool: unknown;
declare const doWork: (args: unknown) => Promise<unknown>;
import { propagation, context, trace } from "@opentelemetry/api";

session.registerTool(myTool, async (args, invocation) => {
  // Restore the CLI's trace context as the active context
  const carrier = {
    traceparent: invocation.traceparent,
    tracestate: invocation.tracestate,
  };
  const parentCtx = propagation.extract(context.active(), carrier);

  // Create a child span under the CLI's span
  const tracer = trace.getTracer("my-app");
  return context.with(parentCtx, () =>
    tracer.startActiveSpan("my-tool", async (span) => {
      try {
        const result = await doWork(args);
        return result;
      } finally {
        span.end();
      }
    })
  );
});
$ npx tsc --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext --strict --skipLibCheck repro.ts
repro.ts(7,9): error TS2339: Property 'registerTool' does not exist on type 'CopilotSession'.
repro.ts(7,37): error TS7006: Parameter 'args' implicitly has an 'any' type.
repro.ts(7,43): error TS7006: Parameter 'invocation' implicitly has an 'any' type.
$ echo $?
2

The two TS7006 errors are a consequence of the same defect: because the method does not resolve,
the compiler has no contextual type for the callback parameters, so the example's own
invocation.traceparent access is not type-checked at all.

Runtime confirmation, no session required:

$ node --input-type=module -e 'import { CopilotSession } from "@github/copilot-sdk";
  const p = CopilotSession.prototype;
  console.log("registerTool: ", typeof p.registerTool);
  console.log("registerTools:", typeof p.registerTools);'
registerTool:  undefined
registerTools: function

So a user who bypasses the types gets TypeError: session.registerTool is not a function.

Expected vs actual

  • Expected: the documented Node.js example for restoring inbound CLI trace context in a tool
    handler compiles and runs.
  • Actual: it does not compile (TS2339), and would throw at runtime. There is no public
    session.register*Tool* method to correct it with, so the example has to be restructured to
    register the tool through createSession({ tools: [...] }).

Why it was not caught

The block carries a <!-- docs-validate: skip --> marker, so scripts/docs-validation never
compiles it. That marker is reasonable here - the validation setup has no @opentelemetry/api
dependency, so an unskipped block would fail on the missing third-party import rather than on SDK
drift - but it does mean this kind of drift is invisible to CI.

Suggested fix

Attach the handler with defineTool() and register the tool at session creation, which is the
public path and the one used elsewhere in the documentation
(see docs/getting-started.md).
The trace-context logic itself does not need to change. That form compiles clean against the same
published package, and it has the added benefit that args and invocation become contextually
typed, so the trace-context access is actually checked:

$ npx tsc --noEmit --target ES2022 --module NodeNext --moduleResolution NodeNext --strict --skipLibCheck fixed.ts
$ echo $?
0

Environment

  • @github/copilot-sdk 1.0.8 (npm latest); also reproduces against a build of current main
  • TypeScript 5.9.3, @opentelemetry/api 1.9.1
  • Node.js 22, Linux x64

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions