# Instrument Agent

> Add Progress Observability instrumentation to an existing AI agent or LLM app — Python, TypeScript/JavaScript, or .NET, including LangChain, LangGraph, LlamaIndex, CrewAI, OpenAI Agents, Haystack, MCP servers and Microsoft.Extensions.AI — with the smallest possible diff, then hand off with where to confirm the traces. Use when the user asks to "instrument my agent", "add observability", "add tracing/telemetry to this repo", "connect this to Progress Observability", or has an existing uninstrumented project they want on the platform. Not for creating a project from scratch — this only edits code that already exists.

- Skill: `telerik/instrument-agent` (Agent Skill, multi-file: 5 files)
- Install (CLI): `npx skillmds@latest add telerik/instrument-agent`
- Raw SKILL.md: https://api.skillmd.com/api/skills/telerik/instrument-agent/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- License: MIT
- Author: telerik (https://skillmd.com/u/telerik)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/telerik/instrument-agent

---


# Instrument an existing agent

Retrofit Progress Observability onto an existing codebase with the smallest
possible diff, then run it once so spans start flowing. For a project that
does not exist yet, this is the wrong skill — it only edits what is already
there.

**This skill writes code** — the instrumentation edits and nothing else. It never
restructures the app, and it never reads the platform back: confirming the traces
arrived is left to `/health-check`.

<!-- copilot:start -->
## 1 · Detect — before touching anything

Scan the repo and report what you found, then the diff you intend to make,
*before* editing:

- **Language & entry point** — `pyproject.toml`/`requirements.txt`,
  `package.json` (check `"type": "module"` — it decides the wiring path),
  `*.csproj`.
- **LLM SDKs and frameworks in use** — imports of `openai`, `anthropic`,
  `langchain`, `llama_index`, `@langchain/*`, `Microsoft.Extensions.AI`, etc.
  Check each against the supported-instruments list in the language reference.
  A client pointed at an **OpenAI-compatible endpoint** (OpenRouter, LiteLLM,
  vLLM, Together, most gateways) counts as OpenAI — see the reference.
  A .NET agent built from `AIProjectClient` (Azure AI Foundry) has no
  wrappable chat client — different wiring; see the Foundry section in
  `references/dotnet.md`.
- **Existing telemetry** — OpenTelemetry setup, Traceloop, or a previous
  Progress Observability init. Look for it before writing anything, and check
  the reference for ordering rather than assuming init belongs first.

  **Python (measured).** The SDK attaches to a provider the app already
  installed, so Progress ends up alongside the app's exporter rather than
  replacing it — **but only if init runs after the app's own setup**. Init
  first and the app's `set_tracer_provider()` becomes a no-op with one warning
  line, killing its existing telemetry while Progress spans keep arriving.
  Grep for `set_tracer_provider` / `TracerProvider(`. Details in
  `references/python.md`.

  **.NET (measured).** No ordering constraint: providers coexist and the
  app's own tracing is unaffected. The hazard is different — a chat client
  that already has `.UseOpenTelemetry()` records every call twice once
  `.AddObservability()` is added, doubling token and cost figures. Grep for
  `UseOpenTelemetry` before wiring and report the overlap rather than
  removing either layer yourself. One hit is not overlap:
  `UseOpenTelemetry(sourceName: ObservabilityTracer.SourceName)` on an
  *agent* builder is Progress wiring itself — leave it. Progress spans land
  in their own trace by design; see `references/dotnet.md`.

  **TypeScript (measured).** The app's `provider.register()` first, then
  `instrument()`: on OpenTelemetry 2.x both the app's exporter and Progress
  receive every span. Init first and the telemetry splits — global-tracer
  spans reach Progress only, `provider.getTracer()` spans reach the app only.
  **On OpenTelemetry 1.x the recommended order attaches and then silently
  exports nothing to Progress** (a serializer mismatch, visible only with
  `debug: true`), so check the app's `@opentelemetry/*` major before
  declaring success. Grep for `NodeTracerProvider` / `NodeSDK` /
  `.register(`. Details and the Genkit case in `references/typescript.md`.
- **Scope** — a monorepo, several services, or more than one agent needs a
  "which one?" question before you touch anything. Don't pick for the user.
- **Config style** — dotenv, user secrets, plain env — instrumentation config
  should arrive the same way the app's other secrets do.
- **Package manager** — lockfiles decide the install command: `uv.lock` /
  `poetry.lock` / `Pipfile` for Python, `yarn.lock` / `pnpm-lock.yaml` for
  Node (defaults: `pip` / `npm`); `.csproj` always means `dotnet add package`.
- **Meta package present? (Python)** — if the dependency list has
  `langchain-core` or `llama-index-core` but not the `langchain` /
  `llama-index` meta package, note it: adding it is a required Wire edit
  below. Check the dependency file, not the imports.

If a framework in use is *not* in the supported list — DSPy, AutoGen,
Pydantic AI, Semantic Kernel and Google ADK are the common ones — say so
plainly and offer the decorator / manual-span fallback from the reference.
Never imply auto-instrumentation covers a framework it doesn't. **LangGraph is
supported** in Python (it rides the LangChain instrumentor and produces full
graph topology); it is *not* supported in the JS SDK. **Genkit is supported
in the JS SDK from 3.1.0**, with an ordering rule (init before the first flow)
and a double-count to block — see the reference. Check the language reference
rather than assuming either way.

## 2 · Wire — follow the language reference exactly

Open the matching reference and use its snippets verbatim — they are verified
against the published packages, not reconstructed:

- `references/python.md` — `progress-observability` (PyPI)
- `references/typescript.md` — `@progress/observability` (npm)
- `references/dotnet.md` — `Progress.Observability.Instrumentation` (NuGet)

Rules that hold across all three:

- **Init at process start, before any LLM client exists.** In ESM Node that
  means the hooks import and `Observability.instrument()` run before the app is
  even imported; in Python, before the LLM and framework imports — not merely
  before clients are constructed. An import can bind references that later
  patching never reaches, and which imports do that is not classifiable from
  the outside, so the default removes the judgment call; `# noqa: E402` on
  the displaced imports is the accepted cost. In .NET, before the agent is
  built. **Two exceptions,
  both in the language references:** an app that installs its own
  OpenTelemetry provider (init goes after that), and Haystack (init goes
  after `import haystack.tracing`). Check the reference before assuming
  earlier is safer — in both cases it isn't.
- **Minimal diff.** Typically: one dependency, one import, one init call, env
  var wiring, and a flush-on-exit. If you find yourself moving app code around,
  stop and reconsider — including "just" exporting a module-scope script so you
  have something to wrap. That is restructuring, and it is never the answer.
- **App with no LLM calls: instrument every step, not the entry point alone.**
  Decorators (Python/.NET) and `wrapFunctionWithSpan` (TS) are the *only* span
  source in that app, so wrap the entry point **and** each internal step **and**
  every tool-like callable, on the functions the app already has. One span
  around the top is the failure mode to avoid: it produces a clean, plausible
  trace that misrepresents a multi-step pipeline as a single unit, and nothing
  in the output reveals it. Each reference has the kind-by-kind table. When
  auto-instrumentation is doing the work instead, add none of them.
- **Python + LangChain or LlamaIndex: the meta package is a REQUIRED edit.**
  `langchain-core` / `llama-index-core` alone leave the instrumentor off: the
  app runs, LLM spans arrive, and no structure is ever emitted — silently.
  (Measured: 2 spans → 8 for LangChain, 1 → 17 for LlamaIndex, from that one
  line.) Add `langchain` / `llama-index` to the dependency file **alongside the
  `-core` package, never in place of it** — the app imports `langchain_core` /
  `llama_index.core` by name, so those stay declared. Say in your report that
  you added it and why; gate table in `references/python.md`.
- **TypeScript + LangChain: `@langchain/core` declared is a REQUIRED edit.**
  The hierarchy patch keys off `@langchain/core` appearing in the app's own
  `package.json`; an app that declares only `@langchain/openai` gets a lone
  `chat` span per call and no chain structure, silently (measured). Add it
  alongside, never in place of, what the app already declares.
- **Never ask the user to paste a key into the chat.** Reference the env var
  or config entry by name and let them set it themselves, in their own shell,
  `.env`, or secret store. Read config to detect what exists; never echo a
  secret's value back.
- **A missing key must be loud.** In Python and TS, read it so an unset
  variable raises (`os.environ["OBSERVABILITY_API_KEY"]`, not `.get(...)`).
  In .NET, follow the reference's optional-tracing pattern instead: skip init
  with a **prominent warning** and keep the app running. Either way, the one
  forbidden outcome is silence — an app that runs, exports nothing, and gives
  no clue why.
- **Keys via config, never hardcoded.** The key here is the **Integration** key
  (`ac_p_…`) from Progress Observability → API Keys. It is not the MCP key
  (`acm_…`) — that one belongs to the *coding agent's* environment for the
  verify step, not to the app.
- **Declaring the dependency is a file edit, and it is never optional.** Add the
  SDK to `package.json` / `requirements.txt` / `pyproject.toml` / `.csproj`
  yourself, creating the `dependencies` section if the manifest has none. Do
  this even when you have been told not to run installs, and even when you
  cannot run them — an import of a package the manifest doesn't declare is
  `ERR_MODULE_NOT_FOUND` the moment anyone runs the app. Telling the user to
  run `npm install …` afterwards is **not** a substitute for the edit; running
  the installer is the separate, optional half.
- **Check before adding.** If the SDK is already in the manifest, don't re-add
  it — note its version and move on (suggest an update only if it's older than
  the reference's verified version). Otherwise add the latest, through the
  project's *own* package manager where you can run it (each reference lists
  the commands), after a quick registry check of the current version — if the
  registry is ahead of the reference's "verified against" version, say so in
  your report rather than assuming the reference still holds.
- **`app_name` is the platform identity** — a short stable slug. Everything in
  the platform (and every other skill in this plugin) filters by it, so confirm
  it with the user. **Read it from the environment with the agreed name as the
  fallback**, rather than hardcoding it outright:

  ```python
  app_name=os.environ.get("OBSERVABILITY_APP_NAME", "my-app")
  ```

  Same shape in TS (`process.env.OBSERVABILITY_APP_NAME ?? 'my-app'`) and .NET.
  The literal still documents the intent, but the same build can then report as
  a different service per environment — dev, staging, prod, CI — with no code
  change. Hardcoding it means every environment lands in one bucket.

  **Add a variable; don't repurpose an existing one.** Where the app already
  names itself for its own reasons — an agent's `name:`, a service
  registration, a CLI banner — leave that alone and introduce a separate
  `app_name`. Pointing an existing field at your new env var makes the app's
  behavior change with a telemetry setting, which is app logic edited for an
  instrumentation task.
- **Content capture default is ON.** Prompts/completions are sent unless
  content tracing is disabled. For apps handling sensitive data, offer the
  content off-switch (each reference shows it) — metadata keeps flowing.

## 3 · Run once

Have the user run the app so it emits at least one trace (run it yourself if it
is runnable here). If LLM credentials aren't available, the decorator/manual
span path in each reference produces real spans with **no** LLM call — wire one
`workflow`-decorated function and run that, so the pipeline can be proven
end-to-end before the model keys exist.

## 4 · Confirm & hand off — no platform read

Instrumentation is finished once the edits are in and the app has emitted at
least one trace. **This skill does not read back over MCP.** Confirming that the
spans actually landed is a separate, read-only step the user runs when they want
it — never something this skill does automatically. Do **not** call
`list_observations`, `get_observation_details`, or any other platform tool here.

Report what you already know from the edits themselves — the language, the
framework, and whether auto-instrumentation or manual spans are carrying the
trace — then tell the user plainly that wiring is done, and hand off with where
to confirm the traces:

> Wiring is complete. Run your agent so it produces some traffic, then open
> **observability.progress.com** and confirm the traces are flowing in for
> service `<app_name>` — they should appear within a minute of the run.

An unverified wiring is a normal, healthy outcome — especially for a new user on
the free tier, who has no MCP key to read traces with. Treating it as a failure
is a bad first experience for exactly the people most likely to be trying the
product for the first time.

Never touch the platform from this skill — no reads, no writes. The only changes
it makes are the local instrumentation edits.
<!-- copilot:end -->

