Instrument A365 Observability
Trigger phrases — any of these will activate this skill automatically:
- "instrument observability for this agent"
- "add a365 observability to this agent"
- "add observability to this agent"
- "set up tracing for this agent"
- "make this agent visible in microsoft defender"
- "enable agent 365 telemetry"
- "wire up opentelemetry for this agent"
- "add observability to this .net agent"
- "add observability to this node.js agent"
- "add a365 observability to this python agent"
Overview
This skill instruments Microsoft Agent 365 observability into an existing agent codebase
without disrupting the agent's core logic. It:
- Detects the agent type (.NET AgentFramework, Node.js, or Python)
- Installs the correct A365 observability packages (core + hosting + optional extensions)
- Wires observability in the entry point
- Adds BaggageBuilder context or BaggageMiddleware to message handlers
- Implements the agentic token resolver with caching
- Adds manual instrumentation scopes (InvokeAgentScope, InferenceScope, ExecuteToolScope — required for store publishing)
- Updates configuration files with observability settings
- Validates the build passes
Store publishing requirement: The Agent 365 store validation requires InvokeAgentScope,
InferenceScope, and ExecuteToolScope to be implemented. This skill wires them.
All changes are additive and idempotent — rerunning the skill is safe.
Phase 0: Load Detection Cache and Validate
Task-list display (applies throughout this skill). This skill creates tasks inline via **TaskCreate** — "..." markers at the start of each phase, and marks them complete at phase end. The user must see this progress visibly. Each TaskCreate line corresponds to one checklist item; exactly one item in_progress at a time.
- Claude Code:
TaskCreate is in allowed-tools — calling it renders a native checklist UI; subsequent TaskUpdate calls flip statuses.
- VS Code Copilot Chat / GitHub Copilot CLI:
allowed-tools is ignored — before Phase 0.1, scan this SKILL.md for all **TaskCreate** — "..." lines and emit a markdown checklist in chat upfront (- [ ] Load detection cache…, - [ ] Determine agent kind…, etc.); flip items to - [x] as each phase completes.
TaskCreate — "Load detection cache and validate with user"
Step 0.1 — Triage the workspace
Run in parallel:
- Glob
**/*.csproj, package.json, requirements.txt, pyproject.toml, src/**/*.ts, **/*.cs, **/*.py → hasProjectFiles.
- Read
.a365-workspace-detection.local.json → cacheState (fresh if detectedAt < 60 min, stale if older, missing if absent).
Decide:
cacheState |
hasProjectFiles |
Action |
fresh |
— |
Continue to Step 0.2 below. |
missing / stale |
false |
Hard stop with a useful message: "This skill instruments an existing agent for observability — there's no agent code in this workspace yet. Run /agent365:make-ai-teammate (if this will be a Teams/Copilot agent) or /agent365:make-a365-agent first to scaffold and register the agent, then come back here." Do not proceed. |
missing / stale |
true |
Tell the user: "Found existing agent code but no fresh Agent 365 registration. I'll run a365-setup now to register it and write the detection cache, then continue here automatically." Read ${CLAUDE_PLUGIN_ROOT}/skills/a365-setup/SKILL.md and follow it through completion, then continue to Step 0.2. |
Step 0.2 — Load from cache
🛑 STOP — .a365-workspace-detection.local.json MUST exist before this step. Read the file path .a365-workspace-detection.local.json in the working directory. If it does not exist, you arrived at Step 0.2 by skipping Step 0.1's triage routing. Do NOT proceed. Do NOT invent default cache values. Do NOT run any further phase (no npm install, no dotnet add package, no pip install, no file edits). Instead:
- Tell the user verbatim: "I skipped the Step 0.1 triage and the detection cache wasn't written. Running
a365-setup now to fix that, then I'll return here."
- Read
${CLAUDE_PLUGIN_ROOT}/skills/a365-setup/SKILL.md and follow it to completion.
- Re-verify the file now exists, then continue below.
The stop hook (validate-instrument-observability.js) will fail the session at end if the cache file is missing — this guard exists so the model halts immediately rather than instrumenting against unknown authMode.
Load from cache: agentStack, programmingLanguage, usesTeamsOrCopilot, agentType, authMode (if previously stored).
Present the loaded values in one message and wait for confirmation:
Here's what we detected about your agent:
• Stack: {agentStack}
• Language: {programmingLanguage}
Reply **yes** to confirm, or describe any corrections.
TaskUpdate — Mark complete: "Load detection cache and validate with user"
Phase 0.5: Agent Kind and Authentication Mode
TaskCreate — "Determine agent kind and authentication mode"
Read ${CLAUDE_PLUGIN_ROOT}/shared/agent-detection.md — section "Agent Type and Auth Mode Detection" — and follow it exactly.
If agentType and authMode are already present in the detection cache (from a prior skill run in this session OR pre-populated by a parent skill like make-ai-teammate), the confirmation behavior depends on agentType:
agentType = "ai-teammate" — skip the confirmation prompt entirely. The AI Teammate identity model is unambiguous (authMode = agentic-user, no obo/s2s decision exists), so a confirm prompt adds friction without catching drift. Proceed silently.
agentType = "system-agent" — confirm the cached values with the user before proceeding, since the obo/s2s choice is meaningful and a stale value would silently route to the wrong token path.
Read authMode case-insensitively (S2S = s2s, OBO = obo); always write back the canonical lowercase value.
Store agentType (ai-teammate = AI Teammate, or system-agent = Agent (Non AI Teammate)) and authMode:
- AI Teammate:
agentic-user (agent's own M365 identity — not the caller's token; auto-set, no question needed)
- Agent (Non AI Teammate):
obo (On-Behalf-Of — signed-in user token) or s2s (Service Principal, no user token)
Update .a365-workspace-detection.local.json — merge agentType and authMode into the existing cache file, preserving all other fields (agentStack, programmingLanguage, usesTeamsOrCopilot, detectedAt). Use the Write tool to write the merged object back.
The authMode value drives Phases 3–5: OBO and S2S paths differ in entry point wiring (Phase 3), message handler pattern (Phase 4), and token resolver (Phase 5). Phases 2, 6, 7, and 8 are identical regardless of authMode.
TaskUpdate — Mark complete: "Determine agent type and authentication mode"
Phase 1: Detect Agent Type
TaskCreate — "Detect agent type and load reference patterns"
Read ${CLAUDE_PLUGIN_ROOT}/shared/agent-detection.md for detection heuristics.
Run detection following the rules in agent-detection.md:
- Check for
.NET AgentFramework indicators (Microsoft.Agent.*, AgentFramework) → .csproj
- Check for
Node.js indicators (package.json, @langchain, openai, @microsoft/agents-*)
- Check for
Python indicators (requirements.txt, pyproject.toml, .py files, microsoft-agents)
- Determine package file (*.csproj, package.json, pyproject.toml/requirements.txt)
- Determine entry point (Program.cs, index.ts/js, app.py / host_agent_server.py)
- Determine message handler location
Load reference patterns:
- If .NET: Read
${CLAUDE_PLUGIN_ROOT}/skills/instrument-observability/references/dotnet-observability.md
- If Node.js: Read
${CLAUDE_PLUGIN_ROOT}/skills/instrument-observability/references/nodejs-observability.md
- If Python: Read
${CLAUDE_PLUGIN_ROOT}/skills/instrument-observability/references/python-observability.md
If agent type cannot be determined, write marker .a365setup-unknown-agent and exit early with clear error message.
Framework-support soft-warn matrix. Check (programmingLanguage, agentStack) from the detection cache and surface a warning when the stack lacks first-class auto-instrumentation in @microsoft/opentelemetry or microsoft-opentelemetry. The skill still proceeds — observability is the OTel SDK underneath, which works for any HTTP-based LLM — but the user should know they'll need to add manual InferenceScope.start wrappers around each LLM call.
| Lang |
Stack |
Action |
| Node.js |
LangChain, OpenAI Agents SDK, Claude SDK |
✅ Auto-instrumented (Claude with custom shape — see Phase 5.5) |
| Node.js |
Semantic Kernel, Google ADK |
⚠ Soft-warn — auto-instrumentation may not patch the LLM library; add manual InferenceScope.start around each LLM call |
| Python |
Agent Framework, OpenAI, Google ADK |
✅ Auto-instrumented |
| Python |
LangChain, Claude SDK, CrewAI |
⚠ Soft-warn — same as Node.js SK/ADK |
| .NET |
Agent Framework, Semantic Kernel |
✅ Auto-instrumented via .UseOpenTelemetry() on IChatClient |
| .NET |
Azure AI Foundry |
⚠ Soft-warn — best-effort wiring |
For soft-warn rows, surface verbatim: "Auto-instrumentation in <unified-distro-package> doesn't patch your LLM library directly. The skill will still wire useMicrosoftOpenTelemetry/UseMicrosoftOpenTelemetry (OTel SDK + A365 exporter), but you'll need to manually wrap each LLM call with InferenceScope.start(...) to capture gen_ai.* spans. See <language>-observability.md § 'InferenceScope — Manual Wrapping' for the pattern." Continue to Phase 2.
TaskUpdate — Mark complete and report detected agent type (+ any soft-warn) to user.
Phase 2: Install A365 Observability Packages
TaskCreate — "Install A365 observability packages"
All languages converge on a single unified distro that re-exports the legacy
A365 observability + hosting types and auto-instruments common LLM SDKs:
| Language |
Install command |
S2S extra (FMI token chain) |
| .NET |
dotnet add package Microsoft.OpenTelemetry |
dotnet add package Azure.Identity Microsoft.Identity.Client |
| Node.js |
npm install @microsoft/opentelemetry |
npm install @azure/msal-node @azure/identity |
| Python |
pip install microsoft-opentelemetry |
pip install msal azure-identity httpx |
Do not install legacy *.Observability.Runtime / -hosting / -extensions-*
packages alongside the unified distro — the distro re-exports their types and
mixing the two produces CS0433 duplicate-type errors (.NET) or duplicate spans
(Node.js / Python). After install, verify the package appears in the manifest
(*.csproj / package.json / requirements.txt or pyproject.toml).
pip install does not update the dependency manifest — prefer
uv add microsoft-opentelemetry (or poetry add ...) for Python.
Python — Google ADK gotcha: if pyproject.toml lists google-adk,
uv sync will backtrack for minutes resolving the OTel graph. Pin OTel via
[tool.uv] override-dependencies — see python-observability.md → "Google ADK
projects — pin the OTel stack" for the exact block. Other Python stacks
(AgentFramework, LangChain, OpenAI, Claude, Semantic Kernel) don't need this.
Full per-language package tables, version constraints, and the LangChain extras
flag live in the references — see the "Required packages" section of:
${CLAUDE_PLUGIN_ROOT}/skills/instrument-observability/references/dotnet-observability.md
${CLAUDE_PLUGIN_ROOT}/skills/instrument-observability/references/nodejs-observability.md
${CLAUDE_PLUGIN_ROOT}/skills/instrument-observability/references/python-observability.md
TaskUpdate — Mark complete.
Phase 3: Wire Observability in Entry Point
TaskCreate — "Wire observability in entry point"
Pre-existing placeholders: As of CLI 1.1, a365 setup all auto-writes Agent365Observability placeholder sections to appsettings.json (.NET) or .env (Node.js/Python). Before creating config from scratch, check if placeholders already exist and fill in values rather than duplicating the section.
For .NET AgentFramework
Read the current entry point (Program.cs or detected file).
Edit — Add observability wiring following the reference pattern in dotnet-observability.md:
- Add
using Microsoft.OpenTelemetry; to Program.cs.
- OBO / agentic-user path (AI Teammate AND Standard .NET agents): Call
builder.UseMicrosoftOpenTelemetry(o => { ... }) with o.Exporters = ExportTarget.Agent365 | ExportTarget.Console (Dev) or ExportTarget.Agent365 (Production). The distro auto-registers IExporterTokenCache<AgenticTokenStruct> in DI — no AddAgenticTracingExporter() call needed. Leave o.Agent365.Exporter.UseS2SEndpoint at its default (false) — the exporter POSTs to /observability/ which the OBO token cache authenticates. Also set "EnableAgent365Exporter": true in appsettings.json to activate the backend exporter — the SDK defaults this to false when absent, so without it the exporter is wired but inert.
- Required:
IChatClient.UseOpenTelemetry() — when registering the IChatClient (e.g. Azure OpenAI), chain .AsBuilder().UseFunctionInvocation().UseOpenTelemetry(sourceName: null, cfg => cfg.EnableSensitiveData = true).Build(). This is what makes the AI SDK emit the gen_ai.inference and gen_ai.tool spans that InvokeAgentScope (Phase 5.5) anchors as children. Skipping this means no LLM spans appear in MAC, even with everything else wired — the InvokeAgent parent becomes a hollow span. EnableSensitiveData = true includes prompts/completions in span attributes (PII consideration — set to false for regulated data).
- S2S path: First Write the two scaffold files from the reference doc —
Observability/ObservabilityServiceExtensions.cs (DI extension with AddAgent365Observability() using ServiceTokenCache and conditional ObservabilityTokenService) and Observability/ObservabilityTokenService.cs (background service that acquires the Observability API token via the MSAL FMI 3-hop chain with .WithFmiPath() targeting scope api://9b975845-388f-4429-889e-eab1ef63949c/.default, supports MSI with client-secret fallback). Then call builder.Services.AddAgent365Observability(); and builder.UseMicrosoftOpenTelemetry(...) with token resolver reading from the ServiceTokenCache. Critical: Set o.Agent365.Exporter.UseS2SEndpoint = true in the options callback — without this, the exporter posts to the wrong path (/observability/ instead of /observabilityService/) and gets HTTP 401. See "Known Issues" section.
- Optionally register
adapter.Use(new BaggageTurnMiddleware()) (OBO path only) to auto-populate baggage on every request
- Mark all new lines with:
// A365 Observability — best-effort instrumentation (verify against official sample)
Preserve all existing code — only add new lines, never remove.
For Node.js
Read the current entry point (index.ts, app.ts, or detected file).
Edit — Add observability initialization following the reference pattern in nodejs-observability.md:
- Import
useMicrosoftOpenTelemetry, shutdownMicrosoftOpenTelemetry, configureA365Hosting, and AgenticTokenCacheInstance — all from @microsoft/opentelemetry (single package as of GA 1.0; do NOT import from the legacy -observability, -hosting, or -runtime packages).
- OBO / agentic-user path: Call
useMicrosoftOpenTelemetry({ a365: { enabled: true, enableObservabilityExporter: true, tokenResolver } }) before any LLM/framework imports. Both enabled: true AND enableObservabilityExporter: true are required in 1.0+ to actually export spans. Wire tokenResolver to AgenticTokenCacheInstance.getObservabilityToken(agentId, tenantId) ?? ''.
- S2S path: First Write
observability/token-cache.ts (in-memory token cache with cacheToken/getCachedToken/tokenResolver) and observability/observability-token-service.ts using the scaffold pattern from nodejs-observability.md (S2S section). This module acquires the Observability API token via MSAL FMI 3-hop chain (@azure/msal-node with fmiPath parameter, targeting scope api://9b975845-388f-4429-889e-eab1ef63949c/.default, supports MSI with client-secret fallback) and refreshes it every 50 min. Then call useMicrosoftOpenTelemetry({ a365: { enabled: true, enableObservabilityExporter: true, useS2SEndpoint: true, tokenResolver: a365TokenResolver } }). useS2SEndpoint: true is now a first-class option (1.0+); the old workaround with custom Agent365Exporter via spanProcessors and ENABLE_A365_OBSERVABILITY_EXPORTER=false is no longer needed for new instrumentation. If the old workaround is already present in an existing agent, leave it in place — do not delete code as part of this additive skill; flag it in the final summary as a candidate for cleanup if the user explicitly asks to migrate.
- Both paths: Call
configureA365Hosting(adapter, { enableBaggage: true }) once at startup to register BaggageMiddleware. This replaces manual adapter.use(new BaggageMiddleware()) and removes the need for BaggageBuilderUtils.fromTurnContext in handlers.
- Both paths: Register
SIGTERM/SIGINT handlers calling await shutdownMicrosoftOpenTelemetry() to flush pending spans on shutdown.
- Auto-instrumentation note: Do NOT call
OpenAIAgentsTraceInstrumentor.enable() or LangChainTraceInstrumentor.instrument() — these are auto-enabled in 1.0+ and manual calls cause duplicate spans. To opt out, set instrumentationOptions: { openaiAgents: { enabled: false } }.
- Mark all new lines with:
// A365 Observability — best-effort instrumentation (verify against official sample)
Preserve all existing code — only add new lines, never remove.
For Python
Read the current entry point (app.py, host_agent_server.py, or detected file).
Edit — Add observability configuration following the reference pattern in python-observability.md:
- Import
use_microsoft_opentelemetry from microsoft.opentelemetry (single unified package; do NOT import from the legacy microsoft_agents_a365.* namespace).
- OBO / agentic-user path: Call
use_microsoft_opentelemetry(enable_a365=True, a365_enable_observability_exporter=True, a365_token_resolver=...). Both enable_a365=True AND a365_enable_observability_exporter=True are required in 1.0+ to actually export spans. Wire a365_token_resolver to AgenticTokenCache().get_observability_token from microsoft.opentelemetry.a365.hosting.token_cache_helpers (or a custom resolver reading from token_cache.py).
- S2S path: First Write
observability/token_cache.py (in-memory token cache with cache_token/get_cached_token) and observability/observability_token_service.py using the scaffold pattern from python-observability.md (S2S section). This module acquires the Observability API token via a 3-hop FMI chain: direct HTTP POST with fmi_path for Hops 1+2 (MSAL Python does not properly serialize fmi_path — known limitation), then msal.ConfidentialClientApplication for Hop 3, targeting scope api://9b975845-388f-4429-889e-eab1ef63949c/.default, supports MSI with client-secret fallback, refreshes every 50 min via an asyncio background task. Then call use_microsoft_opentelemetry(enable_a365=True, a365_enable_observability_exporter=True, a365_use_s2s_endpoint=True, a365_token_resolver=...). a365_use_s2s_endpoint=True is now a first-class kwarg — no workaround needed. Schedule run_token_service() as an asyncio task and call acquire_initial_token() in your aiohttp lifespan startup. Also install msal, azure-identity, and httpx.
- Both paths: Call
ObservabilityHostingManager.configure(adapter.middleware_set, ObservabilityHostingOptions(enable_baggage=True)) once at startup to auto-populate baggage from TurnContext. Note: enable_baggage defaults to False — must be explicitly set to True.
- Auto-instrumentation note: Do NOT call legacy
*Instrumentor().instrument() methods for LangChain/OpenAI/SK/AgentFramework — these are auto-enabled in 1.0+ and manual calls cause duplicate spans.
- Mark all new lines with:
# A365 Observability — best-effort instrumentation (verify against official sample)
Preserve all existing code — only add new lines, never remove.
TaskUpdate — Mark complete.
Phase 4: Add BaggageBuilder Context to Message Handler
TaskCreate — "Add BaggageBuilder context to message handler"
Skip this phase if BaggageMiddleware was registered in Phase 3 — the middleware handles
baggage propagation automatically for every request.
Auth mode note: All three authMode values use authHandlerName: "AGENTIC" in the
code — the token exchange call is identical. The identity in traces is determined by Azure AD
provisioning and the incoming token. Add an inline comment indicating which mode was chosen.
For .NET AgentFramework
Read the detected message handler file.
Edit — Follow the reference pattern in dotnet-observability.md (full code sample under "Agent Class — Message Handler (OBO Path)"):
OBO path (obo / agentic-user) — applies to both AI Teammate agents and Standard .NET agents:
- Inject
IExporterTokenCache<AgenticTokenStruct> in the constructor (auto-registered by the distro — no AddAgenticTracingExporter() call needed).
- Inject
IConfiguration (for blueprint/observability config) and ILogger<MyAgent>.
- Resolve agent ID for BOTH auth paths — agentic instance ID from the Activity for agentic turns, decoded from the auth token via
Utility.ResolveAgentIdentity(context, authToken) for non-agentic turns (the SDK names the second parameter generically authToken — it accepts both OBO tokens and agentic-path tokens returned by UserAuthorization.GetTurnTokenAsync). Do NOT fall back to Guid.Empty.ToString() — that creates a synthetic identity the exporter cannot authenticate, polluting traces with "No token obtained. Skipping export for this identity." warnings.string? resolvedAgentId = null;
if (turnContext.Activity.IsAgenticRequest())
{
resolvedAgentId = turnContext.Activity.GetAgenticInstanceId();
}
else if (!string.IsNullOrEmpty(authHandlerName))
{
try
{
var authToken = await UserAuthorization
.GetTurnTokenAsync(turnContext, authHandlerName, cancellationToken: cancellationToken)
.ConfigureAwait(false);
if (!string.IsNullOrEmpty(authToken))
{
resolvedAgentId = Utility.ResolveAgentIdentity(turnContext, authToken);
}
}
catch (Exception ex)
{
_logger.LogDebug(ex, "Could not resolve agent id from auth token; A365 observability skipped for this turn.");
}
}
var resolvedTenantId = turnContext.Activity.Conversation?.TenantId
?? turnContext.Activity.Recipient?.TenantId;
var hasObservabilityIdentity = !string.IsNullOrEmpty(resolvedAgentId)
&& !string.IsNullOrEmpty(resolvedTenantId);
GetAgenticInstanceId() returns the agent's service principal object ID (the instance ID assigned by A365 for the Teams agentic identity). Utility.ResolveAgentIdentity(context, authToken) decodes the agent identity from a JWT — works for both OBO tokens and agentic-path tokens (SDK signature names the param generically authToken). Both paths produce the same kind of ID — what shows up in MAC Advanced Hunting.
- Conditional baggage + token registration — only when
hasObservabilityIdentity == true. Skip both calls cleanly when the identity can't be resolved:using IDisposable? baggageScope = hasObservabilityIdentity
? new BaggageBuilder()
.TenantId(resolvedTenantId!)
.AgentId(resolvedAgentId!)
.Build()
: null;
if (hasObservabilityIdentity)
{
try
{
_agentTokenCache.RegisterObservability(
resolvedAgentId!,
resolvedTenantId!,
new AgenticTokenStruct(
userAuthorization: UserAuthorization,
turnContext: turnContext,
authHandlerName: authHandlerName ?? string.Empty),
EnvironmentUtils.GetObservabilityAuthenticationScope());
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to register observability token.");
}
}
Note: Some SDK versions support object-initializer syntax instead. If the constructor form fails to compile, try property-initializer: new AgenticTokenStruct { UserAuthorization = ..., TurnContext = ..., AuthHandlerName = ... }.
- The
authHandlerName should resolve to the agentic auth handler name (from config AgentApplication:AgenticAuthHandlerName) when IsAgenticRequest() is true, OBO handler name (from AgentApplication:OboAuthHandlerName) otherwise.
- Keep the
Agent365Observability section in appsettings.json (EnableAgent365Exporter and base exporter settings are still required — Phase 6 handles these). For OBO, you do not need to hardcode per-agent IDs, tenant IDs, or S2S credentials in that section — the agent ID and tenant ID are resolved from the request at runtime on each turn.
- The inline pattern shown above is preferred for new code (mirrors PR #308 in
microsoft/Agent365-Samples). The older A365OtelWrapper.InvokeObservedAgentOperation(...) static-wrapper pattern at Agent365-samples/dotnet/agent-framework/sample-agent/telemetry/A365OtelWrapper.cs is functionally equivalent but uses a separate helper class.
S2S path:
- Inject
Agent365ObservabilityContext (singleton registered by AddAgent365Observability()) in the constructor — not IExporterTokenCache<AgenticTokenStruct>
- Baggage: Use
new BaggageBuilder().FromTurnContext(turnContext).Build() as a separate using var baggageScope — FromTurnContext() is an extension on BaggageBuilder only; it does not exist on InvokeAgentScope or any scope type
- Scope: Use
InvokeAgentScope.Start(new Request(...), new InvokeAgentScopeDetails(endpoint: new Uri("...")), _obs.AgentDetails, callerDetails) as a separate using var scope — InvokeAgentScopeDetails has no parameterless constructor; always pass at least endpoint. CallerDetails with the blueprint sponsor's identity is required for S2S traces to appear in the portal
- No per-turn
RegisterObservability() call; no .FromTurnContext() chaining on the scope
- Add inline comment:
// A365 auth mode: S2S — FMI 3-hop chain via ObservabilityTokenService (scope: api://9b975845-388f-4429-889e-eab1ef63949c/.default)
Mark all new lines with: // A365 Observability — best-effort instrumentation (verify against official sample)
Preserve all existing handler logic.
For Node.js
Read the detected message handler file.
Edit — Refresh the per-turn exporter token following the reference pattern in nodejs-observability.md:
- Import
AgenticTokenCacheInstance from @microsoft/opentelemetry (single unified package).
- OBO paths only (
obo / agentic-user): Resolve agentId and tenantId dynamically from TurnContext each turn (never from config), then refresh the exporter token (non-fatal, wrap in try/catch):const agentId = turnContext.activity?.recipient?.agenticAppId ?? '';
const tenantId = turnContext.activity?.recipient?.tenantId ?? '';
await AgenticTokenCacheInstance.RefreshObservabilityToken(
agentId, tenantId, turnContext,
agentApplication.authorization, // ← the AgentApplication auth object, NOT an auth-handler name string
);
obo (signed-in user): agentApplication.authorization exchanges the token as the signed-in user → traces attributed to the user
obo (agentic identity): agentApplication.authorization exchanges the token as the agentic user provisioned in Azure AD → traces attributed to the agent
- Default observability scope is auto-applied (
api://9b975845-388f-4429-889e-eab1ef63949c/.default) — no need to import getObservabilityAuthenticationScope (removed in 1.0).
- Recommended pattern: Extract the agentId/tenantId resolution and token refresh into a
preloadObservabilityToken(turnContext) helper function to keep the handler clean. See nodejs-observability.md for the full helper implementation.
- S2S path: Do NOT call
AgenticTokenCacheInstance.RefreshObservabilityToken — there is no user authorization token. The tokenResolver passed to useMicrosoftOpenTelemetry() (set up in Phase 3) handles authentication via the FMI 3-hop chain token service.
- Baggage construction is done in Phase 5.5 (canonical pattern), NOT here. In Phase 5.5 the message handler builds
BaggageBuilderUtils.fromTurnContext(new BaggageBuilder(), turnContext as any).build() and runs InvokeAgentScope.start(...) inside baggageScope.run(...). The configureA365Hosting(adapter, { enableBaggage: true }) middleware registered in Phase 3 is a fallback that auto-populates baggage outside the handler, but it does NOT cover the scopes you'll add in Phase 5.5 — those need the manual outer wrapping or they get filtered as Partitioned into 0 identity groups. In Phase 4 itself, just refresh the token; do NOT call InvokeAgentScope.start here.
- Add inline comment:
// A365 auth mode: {authMode} — see: https://learn.microsoft.com/en-us/entra/agent-id/agent-on-behalf-of-oauth-flow
- Mark all new lines with:
// A365 Observability — best-effort instrumentation (verify against official sample)
Preserve all existing handler logic.
For Python
Read the detected message handler file AND host_agent_server.py — the helper lives in the HOST file, not the agent class. The verified AF sample places _setup_observability_token in host_agent_server.py:130-156 so it has access to the AgentApplication instance and can be called by activity middleware. Per-turn baggage construction also lives in the handler/middleware layer in host_agent_server.py, NOT in agent.py.
Edit host_agent_server.py (the host file) — Refresh the per-turn exporter token following the reference pattern in python-observability.md:
- Default observability scope is auto-applied by
microsoft-opentelemetry 1.1+ — do not import get_observability_authentication_scope unless you need to override the default. If overriding, pass via a365_observability_scope_override to use_microsoft_opentelemetry. The exchange_token() call below omits scopes= and lets the auth handler resolve the default.
- Import
cache_agentic_token from token_cache (the custom module created in Phase 5) — or use AgenticTokenCache from the hosting helpers.
- OBO paths only (
obo / agentic-user): Resolve agent_id and tenant_id dynamically from context each turn (never from config), then exchange the OBO token (non-fatal, wrap in try/except):agent_id = context.activity.recipient.agentic_app_id
tenant_id = context.activity.recipient.tenant_id
await self._setup_observability_token(context, tenant_id, agent_id)
The _setup_observability_token helper exchanges and caches the token:async def _setup_observability_token(self, context, tenant_id, agent_id):
exaau_token = await self.agent_app.auth.exchange_token(
context,
scopes=get_observability_authentication_scope(),
auth_handler_id=self.auth_handler_name # from config — NOT hardcoded "AGENTIC"
)
cache_agentic_token(tenant_id, agent_id, exaau_token.token)
auth_handler_name must come from config (e.g., AgentApplication:AgenticAuthHandlerName) — never hardcode "AGENTIC"; it is the registered auth handler name in your agent setup.
agentic-user (AI Teammate): the exchange returns a token for the agent's own Agentic User identity → traces attribute to the agent
obo (non-AI Teammate): the exchange returns whatever the configured auth handler resolves — typically the signed-in user, but it can also be the agent's own identity if the handler is configured that way
- S2S path: Do NOT call
_setup_observability_token — token comes from the background token service wired in Phase 3. The handler should NOT touch tokens.
- Baggage: No manual baggage construction in the handler. Phase 3 registered
ObservabilityHostingManager.configure(adapter.middleware_set, ObservabilityHostingOptions(enable_baggage=True)) which auto-populates baggage from TurnContext for every request. (Optional fallback if you skipped that: build manually with populate(builder, context) then with builder.build():.)
- Add inline comment:
# A365 auth mode: {authMode} — see: https://learn.microsoft.com/en-us/entra/agent-id/agent-on-behalf-of-oauth-flow
- Mark all new lines with:
# A365 Observability — best-effort instrumentation (verify against official sample)
Preserve all existing handler logic.
TaskUpdate — Mark complete.
Phase 5: Implement Agentic Token Resolver
TaskCreate — "Implement agentic token resolver with caching"
For AI Teammate agents and Standard agents on the OBO/agentic-user path, the built-in token cache handles caching automatically — no custom resolver needed. With the Microsoft.OpenTelemetry distro the cache is auto-registered by UseMicrosoftOpenTelemetry(...) (Phase 3). With the legacy individual packages it's registered explicitly via AddAgenticTracingExporter (.NET), AgenticTokenCacheInstance (Node.js), or AgenticTokenCache (Python). Skip to step 3 for these agents.
For .NET AgentFramework (hosting path)
The distro's builder.UseMicrosoftOpenTelemetry(...) call (Phase 3) auto-registers IExporterTokenCache<AgenticTokenStruct> in DI — no separate AddAgenticTracingExporter() call is needed. If you're on the legacy two-package wiring, AddAgenticTracingExporter() provides the same DI instance.
In the agent class, inject IExporterTokenCache<AgenticTokenStruct> in the constructor and call RegisterObservability(...) per turn (already done in Phase 4).
For .NET AgentFramework (S2S path)
The ObservabilityTokenService background service (created in Phase 3 via the scaffold) acquires and refreshes the Observability API token automatically via the FMI 3-hop chain (Blueprint → Agent Identity → Power Platform PFAT token) — no manual TokenResolver delegate needed.
Check if Observability/ObservabilityServiceExtensions.cs and Observability/ObservabilityTokenService.cs exist. If yes, skip — they were already created in Phase 3.
If absent (Phase 3 was skipped or re-running the skill on a partial state), create them now following the S2S scaffold patterns in dotnet-observability.md. These files provide AddAgent365Observability() (DI extension registering AddServiceTracingExporter, ObservabilityTokenService, and Agent365ObservabilityContext) and ObservabilityTokenService (background service that acquires the Observability API token via the FMI 3-hop chain and refreshes it every 50 minutes).
For Node.js (OBO path)
AgenticTokenCacheInstance from @microsoft/agents-a365-observability-hosting handles caching automatically. The useMicrosoftOpenTelemetry() call in Phase 3 wires it as the tokenResolver. No additional token resolver module is needed unless Use_Custom_Resolver=true is required (see reference doc for custom resolver pattern).
For Node.js (S2S path)
Check if observability/observability-token-service.ts exists. If yes, skip — it was created in Phase 3.
If absent (Phase 3 was skipped or re-running), create observability/token-cache.ts and observability/observability-token-service.ts now using the scaffold from nodejs-observability.md (S2S section). The token service uses MSAL (@azure/msal-node) with fmiPath to acquire tokens via the FMI 3-hop chain targeting scope api://9b975845-388f-4429-889e-eab1ef63949c/.default. Call startTokenService(config) at app startup and pass tokenResolver from the cache module to useMicrosoftOpenTelemetry().
For Python (OBO path)
The token_cache.py custom module (located at project root or observability/token_cache.py) provides cache_agentic_token and get_cached_agentic_token. The a365_token_resolver in use_microsoft_opentelemetry() (Phase 3) is wired to get_cached_agentic_token. The per-turn _setup_observability_token helper (Phase 4) calls cache_agentic_token after each OBO exchange. If token_cache.py is absent (e.g., this phase is reached before Phase 4 ran), create it now following the OBO token cache pattern in python-observability.md.
For Python (S2S path)
Check if observability/observability_token_service.py exists. If yes, skip — it was created in Phase 3.
If absent, create observability/token_cache.py and observability/observability_token_service.py now using the scaffold from python-observability.md (S2S section). The token service uses MSAL (msal.ConfidentialClientApplication) with fmi_path to acquire tokens via the FMI 3-hop chain targeting scope api://9b975845-388f-4429-889e-eab1ef63949c/.default. Call acquire_initial_token() for pre-warm, schedule run_token_service() as asyncio.create_task(), and pass token_cache.get_cached_token as the a365_token_resolver in use_microsoft_opentelemetry().
TaskUpdate — Mark complete.
Phase 5.5: Wire Manual Instrumentation Scopes
TaskCreate — "Wire InvokeAgentScope, InferenceScope, ExecuteToolScope"
Auto-instrumentation vs manual: Whether manual scopes are needed depends on authMode and
whether auto-instrumentation framework extensions were installed in Phase 2.
| Situation |
InvokeAgentScope |
InferenceScope |
ExecuteToolScope |
authMode = "s2s" |
Required — add always |
Required — add always |
Required — add always |
| OBO + framework extension installed (Phase 2) |
Required — add always |
Skip — auto-instrumentation generates these |
Only for local/custom tools not covered by the extension |
| OBO + no framework extension |
Required — add always |
Required — add always |
Required — add always |
"Autonomous" agents can run on either OBO or S2S — auth mode is the actual differentiator here, not whether the agent is autonomous.
Rule: Never skip InvokeAgentScope — it wraps the turn and is always required for traces to
appear in the MAC portal. Auto-instrumentation extensions cover LLM calls (InferenceScope) and
framework-managed tool calls (ExecuteToolScope), but they do not wrap the agent turn itself.
Determine which scopes to add:
- If
authMode = "s2s": proceed directly — add all three scopes without prompting (required for S2S agents).
- If OBO and a framework extension was installed in Phase 2:
- Add
InvokeAgentScope always.
- **Skip `In
…(truncated)
1---2name: instrument-observability3description: Instruments Microsoft Agent 365 observability into existing .NET AgentFramework, Node.js, or Python agents. Adds OTel-based tracing, context propagation, A365 exporter, manual instrumentation scopes (InvokeAgentScope, InferenceScope, ExecuteToolScope — required for store publishing), and updates configuration files. Asks a two-stage question — agent kind (AI Teammate or Agent (Non AI Teammate)) and auth mode — to determine the correct token path: OBO (`obo` / `agentic-user`) or Service Principal (`s2s`) (FMI 3-hop token chain with Power Platform scope supported for .NET, Node.js, and Python — each language gets a scaffold token-service file that acquires and refreshes the Observability API token via the FMI chain). Non-destructive and idempotent.4---56# Instrument A365 Observability78> **Trigger phrases** — any of these will activate this skill automatically:9> - "instrument observability for this agent"10> - "add a365 observability to this agent"11> - "add observability to this agent"12> - "set up tracing for this agent"13> - "make this agent visible in microsoft defender"14> - "enable agent 365 telemetry"15> - "wire up opentelemetry for this agent"16> - "add observability to this .net agent"17> - "add observability to this node.js agent"18> - "add a365 observability to this python agent"1920---2122## Overview2324This skill instruments Microsoft Agent 365 observability into an existing agent codebase25without disrupting the agent's core logic. It:26271. **Detects** the agent type (.NET AgentFramework, Node.js, or Python)282. **Installs** the correct A365 observability packages (core + hosting + optional extensions)293. **Wires** observability in the entry point304. **Adds** BaggageBuilder context or BaggageMiddleware to message handlers315. **Implements** the agentic token resolver with caching326. **Adds** manual instrumentation scopes (InvokeAgentScope, InferenceScope, ExecuteToolScope — **required for store publishing**)337. **Updates** configuration files with observability settings348. **Validates** the build passes3536> **Store publishing requirement:** The Agent 365 store validation requires `InvokeAgentScope`,37> `InferenceScope`, and `ExecuteToolScope` to be implemented. This skill wires them.3839All changes are **additive** and **idempotent** — rerunning the skill is safe.4041---4243## Phase 0: Load Detection Cache and Validate4445> **Task-list display (applies throughout this skill).** This skill creates tasks **inline** via `**TaskCreate** — "..."` markers at the start of each phase, and marks them complete at phase end. The user must see this progress visibly. Each `TaskCreate` line corresponds to one checklist item; exactly one item in_progress at a time.46> - **Claude Code:** `TaskCreate` is in `allowed-tools` — calling it renders a native checklist UI; subsequent `TaskUpdate` calls flip statuses.47> - **VS Code Copilot Chat / GitHub Copilot CLI:** `allowed-tools` is ignored — before Phase 0.1, scan this SKILL.md for all `**TaskCreate** — "..."` lines and emit a markdown checklist in chat upfront (`- [ ] Load detection cache…`, `- [ ] Determine agent kind…`, etc.); flip items to `- [x]` as each phase completes.4849**TaskCreate** — "Load detection cache and validate with user"5051### Step 0.1 — Triage the workspace5253Run in parallel:5455- **Glob** `**/*.csproj`, `package.json`, `requirements.txt`, `pyproject.toml`, `src/**/*.ts`, `**/*.cs`, `**/*.py` → `hasProjectFiles`.56- **Read** `.a365-workspace-detection.local.json` → `cacheState` (`fresh` if `detectedAt` < 60 min, `stale` if older, `missing` if absent).5758Decide:5960| `cacheState` | `hasProjectFiles` | Action |61|--------------|-------------------|--------|62| `fresh` | — | Continue to Step 0.2 below. |63| `missing` / `stale` | false | **Hard stop with a useful message:** *"This skill instruments an existing agent for observability — there's no agent code in this workspace yet. Run `/agent365:make-ai-teammate` (if this will be a Teams/Copilot agent) or `/agent365:make-a365-agent` first to scaffold and register the agent, then come back here."* Do not proceed. |64| `missing` / `stale` | true | Tell the user: *"Found existing agent code but no fresh Agent 365 registration. I'll run `a365-setup` now to register it and write the detection cache, then continue here automatically."* **Read** `${CLAUDE_PLUGIN_ROOT}/skills/a365-setup/SKILL.md` and follow it through completion, then continue to Step 0.2. |6566### Step 0.2 — Load from cache6768**🛑 STOP — `.a365-workspace-detection.local.json` MUST exist before this step.** Read the file path `.a365-workspace-detection.local.json` in the working directory. If it does not exist, you arrived at Step 0.2 by skipping Step 0.1's triage routing. Do NOT proceed. Do NOT invent default cache values. Do NOT run any further phase (no `npm install`, no `dotnet add package`, no `pip install`, no file edits). Instead:69701. Tell the user verbatim: *"I skipped the Step 0.1 triage and the detection cache wasn't written. Running `a365-setup` now to fix that, then I'll return here."*712. **Read** `${CLAUDE_PLUGIN_ROOT}/skills/a365-setup/SKILL.md` and follow it to completion.723. Re-verify the file now exists, then continue below.7374The stop hook (`validate-instrument-observability.js`) will fail the session at end if the cache file is missing — this guard exists so the model halts immediately rather than instrumenting against unknown `authMode`.7576Load from cache: `agentStack`, `programmingLanguage`, `usesTeamsOrCopilot`, `agentType`, `authMode` (if previously stored).7778Present the loaded values in one message and wait for confirmation:7980```81Here's what we detected about your agent:82 • Stack: {agentStack}83 • Language: {programmingLanguage}8485Reply **yes** to confirm, or describe any corrections.86```8788**TaskUpdate** — Mark complete: "Load detection cache and validate with user"8990---9192## Phase 0.5: Agent Kind and Authentication Mode9394**TaskCreate** — "Determine agent kind and authentication mode"9596**Read** `${CLAUDE_PLUGIN_ROOT}/shared/agent-detection.md` — section **"Agent Type and Auth Mode Detection"** — and follow it exactly.9798If `agentType` and `authMode` are already present in the detection cache (from a prior skill run in this session OR pre-populated by a parent skill like make-ai-teammate), the confirmation behavior depends on `agentType`:99- **`agentType = "ai-teammate"`** — skip the confirmation prompt entirely. The AI Teammate identity model is unambiguous (`authMode = agentic-user`, no obo/s2s decision exists), so a confirm prompt adds friction without catching drift. Proceed silently.100- **`agentType = "system-agent"`** — confirm the cached values with the user before proceeding, since the obo/s2s choice is meaningful and a stale value would silently route to the wrong token path.101102Read `authMode` case-insensitively (`S2S` = `s2s`, `OBO` = `obo`); always write back the canonical lowercase value.103104Store `agentType` (`ai-teammate` = AI Teammate, or `system-agent` = Agent (Non AI Teammate)) and `authMode`:105- **AI Teammate:** `agentic-user` (agent's own M365 identity — not the caller's token; auto-set, no question needed)106- **Agent (Non AI Teammate):** `obo` (On-Behalf-Of — signed-in user token) or `s2s` (Service Principal, no user token)107108**Update `.a365-workspace-detection.local.json`** — merge `agentType` and `authMode` into the existing cache file, preserving all other fields (`agentStack`, `programmingLanguage`, `usesTeamsOrCopilot`, `detectedAt`). Use the **Write** tool to write the merged object back.109110The `authMode` value drives Phases 3–5: OBO and S2S paths differ in entry point wiring (Phase 3), message handler pattern (Phase 4), and token resolver (Phase 5). **Phases 2, 6, 7, and 8 are identical regardless of `authMode`.**111112**TaskUpdate** — Mark complete: "Determine agent type and authentication mode"113114---115116## Phase 1: Detect Agent Type117118**TaskCreate** — "Detect agent type and load reference patterns"1191201. **Read** `${CLAUDE_PLUGIN_ROOT}/shared/agent-detection.md` for detection heuristics.1211222. **Run detection** following the rules in `agent-detection.md`:123 - Check for `.NET AgentFramework` indicators (Microsoft.Agent.*, AgentFramework) → `.csproj`124 - Check for `Node.js` indicators (package.json, @langchain, openai, @microsoft/agents-*)125 - Check for `Python` indicators (requirements.txt, pyproject.toml, `.py` files, `microsoft-agents`)126 - Determine package file (*.csproj, package.json, pyproject.toml/requirements.txt)127 - Determine entry point (Program.cs, index.ts/js, app.py / host_agent_server.py)128 - Determine message handler location1291303. **Load reference patterns:**131 - If .NET: **Read** `${CLAUDE_PLUGIN_ROOT}/skills/instrument-observability/references/dotnet-observability.md`132 - If Node.js: **Read** `${CLAUDE_PLUGIN_ROOT}/skills/instrument-observability/references/nodejs-observability.md`133 - If Python: **Read** `${CLAUDE_PLUGIN_ROOT}/skills/instrument-observability/references/python-observability.md`1341354. **If agent type cannot be determined**, write marker `.a365setup-unknown-agent` and **exit early** with clear error message.1361375. **Framework-support soft-warn matrix.** Check `(programmingLanguage, agentStack)` from the detection cache and surface a warning when the stack lacks first-class auto-instrumentation in `@microsoft/opentelemetry` or `microsoft-opentelemetry`. The skill still proceeds — observability is the OTel SDK underneath, which works for any HTTP-based LLM — but the user should know they'll need to add manual `InferenceScope.start` wrappers around each LLM call.138139 | Lang | Stack | Action |140 |---|---|---|141 | Node.js | LangChain, OpenAI Agents SDK, Claude SDK | ✅ Auto-instrumented (Claude with custom shape — see Phase 5.5) |142 | Node.js | Semantic Kernel, Google ADK | ⚠ Soft-warn — auto-instrumentation may not patch the LLM library; add manual `InferenceScope.start` around each LLM call |143 | Python | Agent Framework, OpenAI, Google ADK | ✅ Auto-instrumented |144 | Python | LangChain, Claude SDK, CrewAI | ⚠ Soft-warn — same as Node.js SK/ADK |145 | .NET | Agent Framework, Semantic Kernel | ✅ Auto-instrumented via `.UseOpenTelemetry()` on `IChatClient` |146 | .NET | Azure AI Foundry | ⚠ Soft-warn — best-effort wiring |147148 For soft-warn rows, surface verbatim: *"Auto-instrumentation in `<unified-distro-package>` doesn't patch your LLM library directly. The skill will still wire `useMicrosoftOpenTelemetry`/`UseMicrosoftOpenTelemetry` (OTel SDK + A365 exporter), but you'll need to manually wrap each LLM call with `InferenceScope.start(...)` to capture `gen_ai.*` spans. See `<language>-observability.md` § 'InferenceScope — Manual Wrapping' for the pattern."* Continue to Phase 2.1491506. **TaskUpdate** — Mark complete and report detected agent type (+ any soft-warn) to user.151152---153154## Phase 2: Install A365 Observability Packages155156**TaskCreate** — "Install A365 observability packages"157158All languages converge on a single unified distro that re-exports the legacy159A365 observability + hosting types and auto-instruments common LLM SDKs:160161| Language | Install command | S2S extra (FMI token chain) |162|----------|-----------------|------------------------------|163| .NET | `dotnet add package Microsoft.OpenTelemetry` | `dotnet add package Azure.Identity Microsoft.Identity.Client` |164| Node.js | `npm install @microsoft/opentelemetry` | `npm install @azure/msal-node @azure/identity` |165| Python | `pip install microsoft-opentelemetry` | `pip install msal azure-identity httpx` |166167**Do not** install legacy `*.Observability.Runtime` / `-hosting` / `-extensions-*`168packages alongside the unified distro — the distro re-exports their types and169mixing the two produces CS0433 duplicate-type errors (.NET) or duplicate spans170(Node.js / Python). After install, verify the package appears in the manifest171(`*.csproj` / `package.json` / `requirements.txt` or `pyproject.toml`).172`pip install` does not update the dependency manifest — prefer173`uv add microsoft-opentelemetry` (or `poetry add ...`) for Python.174175**Python — Google ADK gotcha:** if `pyproject.toml` lists `google-adk`,176`uv sync` will backtrack for minutes resolving the OTel graph. Pin OTel via177`[tool.uv] override-dependencies` — see python-observability.md → "Google ADK178projects — pin the OTel stack" for the exact block. Other Python stacks179(AgentFramework, LangChain, OpenAI, Claude, Semantic Kernel) don't need this.180181Full per-language package tables, version constraints, and the LangChain extras182flag live in the references — see the "Required packages" section of:183184- `${CLAUDE_PLUGIN_ROOT}/skills/instrument-observability/references/dotnet-observability.md`185- `${CLAUDE_PLUGIN_ROOT}/skills/instrument-observability/references/nodejs-observability.md`186- `${CLAUDE_PLUGIN_ROOT}/skills/instrument-observability/references/python-observability.md`187188**TaskUpdate** — Mark complete.189190---191192## Phase 3: Wire Observability in Entry Point193194**TaskCreate** — "Wire observability in entry point"195196> **Pre-existing placeholders:** As of CLI 1.1, `a365 setup all` auto-writes `Agent365Observability` placeholder sections to `appsettings.json` (.NET) or `.env` (Node.js/Python). Before creating config from scratch, **check if placeholders already exist** and fill in values rather than duplicating the section.197198### For .NET AgentFramework1992001. **Read** the current entry point (`Program.cs` or detected file).2012022. **Edit** — Add observability wiring following the reference pattern in `dotnet-observability.md`:203 - Add `using Microsoft.OpenTelemetry;` to `Program.cs`.204 - **OBO / agentic-user path** (AI Teammate AND Standard .NET agents): Call `builder.UseMicrosoftOpenTelemetry(o => { ... })` with `o.Exporters = ExportTarget.Agent365 | ExportTarget.Console` (Dev) or `ExportTarget.Agent365` (Production). The distro **auto-registers `IExporterTokenCache<AgenticTokenStruct>`** in DI — no `AddAgenticTracingExporter()` call needed. Leave `o.Agent365.Exporter.UseS2SEndpoint` at its default (`false`) — the exporter POSTs to `/observability/` which the OBO token cache authenticates. Also set `"EnableAgent365Exporter": true` in `appsettings.json` to activate the backend exporter — the SDK defaults this to `false` when absent, so without it the exporter is wired but inert.205 - **Required: `IChatClient.UseOpenTelemetry()`** — when registering the `IChatClient` (e.g. Azure OpenAI), chain `.AsBuilder().UseFunctionInvocation().UseOpenTelemetry(sourceName: null, cfg => cfg.EnableSensitiveData = true).Build()`. This is what makes the AI SDK emit the `gen_ai.inference` and `gen_ai.tool` spans that `InvokeAgentScope` (Phase 5.5) anchors as children. **Skipping this means no LLM spans appear in MAC, even with everything else wired** — the `InvokeAgent` parent becomes a hollow span. `EnableSensitiveData = true` includes prompts/completions in span attributes (PII consideration — set to `false` for regulated data).206 - **S2S path**: First **Write** the two scaffold files from the reference doc — `Observability/ObservabilityServiceExtensions.cs` (DI extension with `AddAgent365Observability()` using `ServiceTokenCache` and conditional `ObservabilityTokenService`) and `Observability/ObservabilityTokenService.cs` (background service that acquires the Observability API token via the MSAL FMI 3-hop chain with `.WithFmiPath()` targeting scope `api://9b975845-388f-4429-889e-eab1ef63949c/.default`, supports MSI with client-secret fallback). Then call `builder.Services.AddAgent365Observability();` and `builder.UseMicrosoftOpenTelemetry(...)` with token resolver reading from the `ServiceTokenCache`. **Critical:** Set `o.Agent365.Exporter.UseS2SEndpoint = true` in the options callback — without this, the exporter posts to the wrong path (`/observability/` instead of `/observabilityService/`) and gets HTTP 401. See "Known Issues" section.207 - Optionally register `adapter.Use(new BaggageTurnMiddleware())` (OBO path only) to auto-populate baggage on every request208 - Mark all new lines with: `// A365 Observability — best-effort instrumentation (verify against official sample)`2092103. **Preserve** all existing code — only add new lines, never remove.211212### For Node.js2132141. **Read** the current entry point (`index.ts`, `app.ts`, or detected file).2152162. **Edit** — Add observability initialization following the reference pattern in `nodejs-observability.md`:217 - Import `useMicrosoftOpenTelemetry`, `shutdownMicrosoftOpenTelemetry`, `configureA365Hosting`, and `AgenticTokenCacheInstance` — **all from `@microsoft/opentelemetry`** (single package as of GA 1.0; do NOT import from the legacy `-observability`, `-hosting`, or `-runtime` packages).218 - **OBO / agentic-user path**: Call `useMicrosoftOpenTelemetry({ a365: { enabled: true, enableObservabilityExporter: true, tokenResolver } })` **before** any LLM/framework imports. Both `enabled: true` AND `enableObservabilityExporter: true` are required in 1.0+ to actually export spans. Wire `tokenResolver` to `AgenticTokenCacheInstance.getObservabilityToken(agentId, tenantId) ?? ''`.219 - **S2S path**: First **Write** `observability/token-cache.ts` (in-memory token cache with `cacheToken`/`getCachedToken`/`tokenResolver`) and `observability/observability-token-service.ts` using the scaffold pattern from `nodejs-observability.md` (S2S section). This module acquires the Observability API token via MSAL FMI 3-hop chain (`@azure/msal-node` with `fmiPath` parameter, targeting scope `api://9b975845-388f-4429-889e-eab1ef63949c/.default`, supports MSI with client-secret fallback) and refreshes it every 50 min. Then call `useMicrosoftOpenTelemetry({ a365: { enabled: true, enableObservabilityExporter: true, useS2SEndpoint: true, tokenResolver: a365TokenResolver } })`. `useS2SEndpoint: true` is now a first-class option (1.0+); the old workaround with custom `Agent365Exporter` via `spanProcessors` and `ENABLE_A365_OBSERVABILITY_EXPORTER=false` is no longer needed for new instrumentation. If the old workaround is already present in an existing agent, leave it in place — do not delete code as part of this additive skill; flag it in the final summary as a candidate for cleanup if the user explicitly asks to migrate.220 - **Both paths**: Call `configureA365Hosting(adapter, { enableBaggage: true })` once at startup to register `BaggageMiddleware`. This replaces manual `adapter.use(new BaggageMiddleware())` and removes the need for `BaggageBuilderUtils.fromTurnContext` in handlers.221 - **Both paths**: Register `SIGTERM`/`SIGINT` handlers calling `await shutdownMicrosoftOpenTelemetry()` to flush pending spans on shutdown.222 - **Auto-instrumentation note**: Do NOT call `OpenAIAgentsTraceInstrumentor.enable()` or `LangChainTraceInstrumentor.instrument()` — these are auto-enabled in 1.0+ and manual calls cause duplicate spans. To opt out, set `instrumentationOptions: { openaiAgents: { enabled: false } }`.223 - Mark all new lines with: `// A365 Observability — best-effort instrumentation (verify against official sample)`2242253. **Preserve** all existing code — only add new lines, never remove.226227### For Python2282291. **Read** the current entry point (`app.py`, `host_agent_server.py`, or detected file).2302312. **Edit** — Add observability configuration following the reference pattern in `python-observability.md`:232 - Import `use_microsoft_opentelemetry` from `microsoft.opentelemetry` (single unified package; do NOT import from the legacy `microsoft_agents_a365.*` namespace).233 - **OBO / agentic-user path**: Call `use_microsoft_opentelemetry(enable_a365=True, a365_enable_observability_exporter=True, a365_token_resolver=...)`. Both `enable_a365=True` AND `a365_enable_observability_exporter=True` are required in 1.0+ to actually export spans. Wire `a365_token_resolver` to `AgenticTokenCache().get_observability_token` from `microsoft.opentelemetry.a365.hosting.token_cache_helpers` (or a custom resolver reading from `token_cache.py`).234 - **S2S path**: First **Write** `observability/token_cache.py` (in-memory token cache with `cache_token`/`get_cached_token`) and `observability/observability_token_service.py` using the scaffold pattern from `python-observability.md` (S2S section). This module acquires the Observability API token via a 3-hop FMI chain: direct HTTP POST with `fmi_path` for Hops 1+2 (MSAL Python does not properly serialize `fmi_path` — known limitation), then `msal.ConfidentialClientApplication` for Hop 3, targeting scope `api://9b975845-388f-4429-889e-eab1ef63949c/.default`, supports MSI with client-secret fallback, refreshes every 50 min via an `asyncio` background task. Then call `use_microsoft_opentelemetry(enable_a365=True, a365_enable_observability_exporter=True, a365_use_s2s_endpoint=True, a365_token_resolver=...)`. `a365_use_s2s_endpoint=True` is now a first-class kwarg — no workaround needed. Schedule `run_token_service()` as an asyncio task and call `acquire_initial_token()` in your aiohttp lifespan startup. Also install `msal`, `azure-identity`, and `httpx`.235 - **Both paths**: Call `ObservabilityHostingManager.configure(adapter.middleware_set, ObservabilityHostingOptions(enable_baggage=True))` once at startup to auto-populate baggage from `TurnContext`. **Note:** `enable_baggage` defaults to `False` — must be explicitly set to `True`.236 - **Auto-instrumentation note**: Do NOT call legacy `*Instrumentor().instrument()` methods for LangChain/OpenAI/SK/AgentFramework — these are auto-enabled in 1.0+ and manual calls cause duplicate spans.237 - Mark all new lines with: `# A365 Observability — best-effort instrumentation (verify against official sample)`2382393. **Preserve** all existing code — only add new lines, never remove.2402414. **TaskUpdate** — Mark complete.242243---244245## Phase 4: Add BaggageBuilder Context to Message Handler246247**TaskCreate** — "Add BaggageBuilder context to message handler"248249> **Skip this phase** if BaggageMiddleware was registered in Phase 3 — the middleware handles250> baggage propagation automatically for every request.251252> **Auth mode note:** All three `authMode` values use `authHandlerName: "AGENTIC"` in the253> code — the token exchange call is identical. The identity in traces is determined by Azure AD254> provisioning and the incoming token. Add an inline comment indicating which mode was chosen.255256### For .NET AgentFramework2572581. **Read** the detected message handler file.2592602. **Edit** — Follow the reference pattern in `dotnet-observability.md` (full code sample under "Agent Class — Message Handler (OBO Path)"):261262 **OBO path** (`obo` / `agentic-user`) — applies to both **AI Teammate** agents and **Standard .NET agents**:263 - Inject `IExporterTokenCache<AgenticTokenStruct>` in the constructor (auto-registered by the distro — no `AddAgenticTracingExporter()` call needed).264 - Inject `IConfiguration` (for blueprint/observability config) and `ILogger<MyAgent>`.265 - **Resolve agent ID for BOTH auth paths** — agentic instance ID from the Activity for agentic turns, **decoded from the auth token** via `Utility.ResolveAgentIdentity(context, authToken)` for non-agentic turns (the SDK names the second parameter generically `authToken` — it accepts both OBO tokens and agentic-path tokens returned by `UserAuthorization.GetTurnTokenAsync`). Do NOT fall back to `Guid.Empty.ToString()` — that creates a synthetic identity the exporter cannot authenticate, polluting traces with `"No token obtained. Skipping export for this identity."` warnings.266 ```csharp267 string? resolvedAgentId = null;268 if (turnContext.Activity.IsAgenticRequest())269 {270 resolvedAgentId = turnContext.Activity.GetAgenticInstanceId();271 }272 else if (!string.IsNullOrEmpty(authHandlerName))273 {274 try275 {276 var authToken = await UserAuthorization277 .GetTurnTokenAsync(turnContext, authHandlerName, cancellationToken: cancellationToken)278 .ConfigureAwait(false);279 if (!string.IsNullOrEmpty(authToken))280 {281 resolvedAgentId = Utility.ResolveAgentIdentity(turnContext, authToken);282 }283 }284 catch (Exception ex)285 {286 _logger.LogDebug(ex, "Could not resolve agent id from auth token; A365 observability skipped for this turn.");287 }288 }289290 var resolvedTenantId = turnContext.Activity.Conversation?.TenantId291 ?? turnContext.Activity.Recipient?.TenantId;292293 var hasObservabilityIdentity = !string.IsNullOrEmpty(resolvedAgentId)294 && !string.IsNullOrEmpty(resolvedTenantId);295 ```296 `GetAgenticInstanceId()` returns the agent's **service principal object ID** (the instance ID assigned by A365 for the Teams agentic identity). `Utility.ResolveAgentIdentity(context, authToken)` decodes the agent identity from a JWT — works for both OBO tokens and agentic-path tokens (SDK signature names the param generically `authToken`). Both paths produce the same kind of ID — what shows up in MAC Advanced Hunting.297 - **Conditional baggage + token registration** — only when `hasObservabilityIdentity == true`. Skip both calls cleanly when the identity can't be resolved:298 ```csharp299 using IDisposable? baggageScope = hasObservabilityIdentity300 ? new BaggageBuilder()301 .TenantId(resolvedTenantId!)302 .AgentId(resolvedAgentId!)303 .Build()304 : null;305306 if (hasObservabilityIdentity)307 {308 try309 {310 _agentTokenCache.RegisterObservability(311 resolvedAgentId!,312 resolvedTenantId!,313 new AgenticTokenStruct(314 userAuthorization: UserAuthorization,315 turnContext: turnContext,316 authHandlerName: authHandlerName ?? string.Empty),317 EnvironmentUtils.GetObservabilityAuthenticationScope());318 }319 catch (Exception ex)320 {321 _logger.LogWarning(ex, "Failed to register observability token.");322 }323 }324 ```325 Note: Some SDK versions support object-initializer syntax instead. If the constructor form fails to compile, try property-initializer: `new AgenticTokenStruct { UserAuthorization = ..., TurnContext = ..., AuthHandlerName = ... }`.326 - The `authHandlerName` should resolve to the agentic auth handler name (from config `AgentApplication:AgenticAuthHandlerName`) when `IsAgenticRequest()` is true, OBO handler name (from `AgentApplication:OboAuthHandlerName`) otherwise.327 - **Keep the `Agent365Observability` section in `appsettings.json`** (`EnableAgent365Exporter` and base exporter settings are still required — Phase 6 handles these). For **OBO**, you do **not** need to hardcode per-agent IDs, tenant IDs, or S2S credentials in that section — the agent ID and tenant ID are resolved from the request at runtime on each turn.328 - **The inline pattern shown above is preferred** for new code (mirrors PR #308 in `microsoft/Agent365-Samples`). The older `A365OtelWrapper.InvokeObservedAgentOperation(...)` static-wrapper pattern at `Agent365-samples/dotnet/agent-framework/sample-agent/telemetry/A365OtelWrapper.cs` is functionally equivalent but uses a separate helper class.329330 **S2S path**:331 - Inject `Agent365ObservabilityContext` (singleton registered by `AddAgent365Observability()`) in the constructor — **not** `IExporterTokenCache<AgenticTokenStruct>`332 - **Baggage:** Use `new BaggageBuilder().FromTurnContext(turnContext).Build()` as a separate `using var baggageScope` — `FromTurnContext()` is an extension on `BaggageBuilder` **only**; it does not exist on `InvokeAgentScope` or any scope type333 - **Scope:** Use `InvokeAgentScope.Start(new Request(...), new InvokeAgentScopeDetails(endpoint: new Uri("...")), _obs.AgentDetails, callerDetails)` as a separate `using var scope` — `InvokeAgentScopeDetails` has **no parameterless constructor**; always pass at least `endpoint`. `CallerDetails` with the blueprint sponsor's identity is **required** for S2S traces to appear in the portal334 - **No** per-turn `RegisterObservability()` call; **no** `.FromTurnContext()` chaining on the scope335 - Add inline comment: `// A365 auth mode: S2S — FMI 3-hop chain via ObservabilityTokenService (scope: api://9b975845-388f-4429-889e-eab1ef63949c/.default)`336337 Mark all new lines with: `// A365 Observability — best-effort instrumentation (verify against official sample)`3383393. **Preserve** all existing handler logic.340341### For Node.js3423431. **Read** the detected message handler file.3443452. **Edit** — Refresh the per-turn exporter token following the reference pattern in `nodejs-observability.md`:346 - Import `AgenticTokenCacheInstance` from `@microsoft/opentelemetry` (single unified package).347 - **OBO paths only** (`obo` / `agentic-user`): Resolve `agentId` and `tenantId` dynamically from TurnContext each turn (never from config), then refresh the exporter token (non-fatal, wrap in try/catch):348 ```349 const agentId = turnContext.activity?.recipient?.agenticAppId ?? '';350 const tenantId = turnContext.activity?.recipient?.tenantId ?? '';351 await AgenticTokenCacheInstance.RefreshObservabilityToken(352 agentId, tenantId, turnContext,353 agentApplication.authorization, // ← the AgentApplication auth object, NOT an auth-handler name string354 );355 ```356 - `obo` (signed-in user): `agentApplication.authorization` exchanges the token as the **signed-in user** → traces attributed to the user357 - `obo` (agentic identity): `agentApplication.authorization` exchanges the token as the **agentic user** provisioned in Azure AD → traces attributed to the agent358 - Default observability scope is auto-applied (`api://9b975845-388f-4429-889e-eab1ef63949c/.default`) — no need to import `getObservabilityAuthenticationScope` (removed in 1.0).359 - **Recommended pattern:** Extract the agentId/tenantId resolution and token refresh into a `preloadObservabilityToken(turnContext)` helper function to keep the handler clean. See `nodejs-observability.md` for the full helper implementation.360 - **S2S path**: Do **NOT** call `AgenticTokenCacheInstance.RefreshObservabilityToken` — there is no user authorization token. The `tokenResolver` passed to `useMicrosoftOpenTelemetry()` (set up in Phase 3) handles authentication via the FMI 3-hop chain token service.361 - **Baggage construction is done in Phase 5.5 (canonical pattern), NOT here.** In Phase 5.5 the message handler builds `BaggageBuilderUtils.fromTurnContext(new BaggageBuilder(), turnContext as any).build()` and runs `InvokeAgentScope.start(...)` inside `baggageScope.run(...)`. The `configureA365Hosting(adapter, { enableBaggage: true })` middleware registered in Phase 3 is a fallback that auto-populates baggage outside the handler, but it does NOT cover the scopes you'll add in Phase 5.5 — those need the manual outer wrapping or they get filtered as `Partitioned into 0 identity groups`. In Phase 4 itself, just refresh the token; do NOT call `InvokeAgentScope.start` here.362 - Add inline comment: `// A365 auth mode: {authMode} — see: https://learn.microsoft.com/en-us/entra/agent-id/agent-on-behalf-of-oauth-flow`363 - Mark all new lines with: `// A365 Observability — best-effort instrumentation (verify against official sample)`3643653. **Preserve** all existing handler logic.366367### For Python3683691. **Read** the detected message handler file AND `host_agent_server.py` — the helper lives in the HOST file, not the agent class. The verified AF sample places `_setup_observability_token` in `host_agent_server.py:130-156` so it has access to the `AgentApplication` instance and can be called by activity middleware. Per-turn baggage construction also lives in the handler/middleware layer in `host_agent_server.py`, NOT in `agent.py`.3703712. **Edit `host_agent_server.py`** (the host file) — Refresh the per-turn exporter token following the reference pattern in `python-observability.md`:372 - Default observability scope is auto-applied by `microsoft-opentelemetry` 1.1+ — do **not** import `get_observability_authentication_scope` unless you need to override the default. If overriding, pass via `a365_observability_scope_override` to `use_microsoft_opentelemetry`. The `exchange_token()` call below omits `scopes=` and lets the auth handler resolve the default.373 - Import `cache_agentic_token` from `token_cache` (the custom module created in Phase 5) — or use `AgenticTokenCache` from the hosting helpers.374 - **OBO paths only** (`obo` / `agentic-user`): Resolve `agent_id` and `tenant_id` dynamically from context each turn (never from config), then exchange the OBO token (non-fatal, wrap in try/except):375 ```python376 agent_id = context.activity.recipient.agentic_app_id377 tenant_id = context.activity.recipient.tenant_id378 await self._setup_observability_token(context, tenant_id, agent_id)379 ```380 The `_setup_observability_token` helper exchanges and caches the token:381 ```python382 async def _setup_observability_token(self, context, tenant_id, agent_id):383 exaau_token = await self.agent_app.auth.exchange_token(384 context,385 scopes=get_observability_authentication_scope(),386 auth_handler_id=self.auth_handler_name # from config — NOT hardcoded "AGENTIC"387 )388 cache_agentic_token(tenant_id, agent_id, exaau_token.token)389 ```390 - `auth_handler_name` must come from config (e.g., `AgentApplication:AgenticAuthHandlerName`) — **never hardcode `"AGENTIC"`**; it is the registered auth handler name in your agent setup.391 - `agentic-user` (AI Teammate): the exchange returns a token for the **agent's own Agentic User** identity → traces attribute to the agent392 - `obo` (non-AI Teammate): the exchange returns whatever the configured auth handler resolves — typically the **signed-in user**, but it can also be the agent's own identity if the handler is configured that way393 - **S2S path**: Do **NOT** call `_setup_observability_token` — token comes from the background token service wired in Phase 3. The handler should NOT touch tokens.394 - **Baggage:** No manual baggage construction in the handler. Phase 3 registered `ObservabilityHostingManager.configure(adapter.middleware_set, ObservabilityHostingOptions(enable_baggage=True))` which auto-populates baggage from `TurnContext` for every request. (Optional fallback if you skipped that: build manually with `populate(builder, context)` then `with builder.build():`.)395 - Add inline comment: `# A365 auth mode: {authMode} — see: https://learn.microsoft.com/en-us/entra/agent-id/agent-on-behalf-of-oauth-flow`396 - Mark all new lines with: `# A365 Observability — best-effort instrumentation (verify against official sample)`3973983. **Preserve** all existing handler logic.3994004. **TaskUpdate** — Mark complete.401402---403404## Phase 5: Implement Agentic Token Resolver405406**TaskCreate** — "Implement agentic token resolver with caching"407408For AI Teammate agents and Standard agents on the OBO/agentic-user path, the built-in token cache handles caching automatically — no custom resolver needed. With the **`Microsoft.OpenTelemetry` distro** the cache is auto-registered by `UseMicrosoftOpenTelemetry(...)` (Phase 3). With the **legacy individual packages** it's registered explicitly via `AddAgenticTracingExporter` (.NET), `AgenticTokenCacheInstance` (Node.js), or `AgenticTokenCache` (Python). Skip to step 3 for these agents.409410### For .NET AgentFramework (hosting path)4114121. The distro's `builder.UseMicrosoftOpenTelemetry(...)` call (Phase 3) **auto-registers `IExporterTokenCache<AgenticTokenStruct>`** in DI — no separate `AddAgenticTracingExporter()` call is needed. If you're on the legacy two-package wiring, `AddAgenticTracingExporter()` provides the same DI instance.4134142. In the agent class, inject `IExporterTokenCache<AgenticTokenStruct>` in the constructor and call `RegisterObservability(...)` per turn (already done in Phase 4).415416### For .NET AgentFramework (S2S path)417418The `ObservabilityTokenService` background service (created in Phase 3 via the scaffold) acquires and refreshes the Observability API token automatically via the FMI 3-hop chain (Blueprint → Agent Identity → Power Platform PFAT token) — no manual `TokenResolver` delegate needed.4194201. **Check** if `Observability/ObservabilityServiceExtensions.cs` and `Observability/ObservabilityTokenService.cs` exist. If yes, **skip** — they were already created in Phase 3.4214222. **If absent** (Phase 3 was skipped or re-running the skill on a partial state), create them now following the S2S scaffold patterns in `dotnet-observability.md`. These files provide `AddAgent365Observability()` (DI extension registering `AddServiceTracingExporter`, `ObservabilityTokenService`, and `Agent365ObservabilityContext`) and `ObservabilityTokenService` (background service that acquires the Observability API token via the FMI 3-hop chain and refreshes it every 50 minutes).423424### For Node.js (OBO path)425426`AgenticTokenCacheInstance` from `@microsoft/agents-a365-observability-hosting` handles caching automatically. The `useMicrosoftOpenTelemetry()` call in Phase 3 wires it as the `tokenResolver`. No additional token resolver module is needed unless `Use_Custom_Resolver=true` is required (see reference doc for custom resolver pattern).427428### For Node.js (S2S path)429430**Check** if `observability/observability-token-service.ts` exists. If yes, **skip** — it was created in Phase 3.431432**If absent** (Phase 3 was skipped or re-running), create `observability/token-cache.ts` and `observability/observability-token-service.ts` now using the scaffold from `nodejs-observability.md` (S2S section). The token service uses MSAL (`@azure/msal-node`) with `fmiPath` to acquire tokens via the FMI 3-hop chain targeting scope `api://9b975845-388f-4429-889e-eab1ef63949c/.default`. Call `startTokenService(config)` at app startup and pass `tokenResolver` from the cache module to `useMicrosoftOpenTelemetry()`.433434### For Python (OBO path)435436The `token_cache.py` custom module (located at project root or `observability/token_cache.py`) provides `cache_agentic_token` and `get_cached_agentic_token`. The `a365_token_resolver` in `use_microsoft_opentelemetry()` (Phase 3) is wired to `get_cached_agentic_token`. The per-turn `_setup_observability_token` helper (Phase 4) calls `cache_agentic_token` after each OBO exchange. If `token_cache.py` is absent (e.g., this phase is reached before Phase 4 ran), create it now following the OBO token cache pattern in `python-observability.md`.437438### For Python (S2S path)439440**Check** if `observability/observability_token_service.py` exists. If yes, **skip** — it was created in Phase 3.441442**If absent**, create `observability/token_cache.py` and `observability/observability_token_service.py` now using the scaffold from `python-observability.md` (S2S section). The token service uses MSAL (`msal.ConfidentialClientApplication`) with `fmi_path` to acquire tokens via the FMI 3-hop chain targeting scope `api://9b975845-388f-4429-889e-eab1ef63949c/.default`. Call `acquire_initial_token()` for pre-warm, schedule `run_token_service()` as `asyncio.create_task()`, and pass `token_cache.get_cached_token` as the `a365_token_resolver` in `use_microsoft_opentelemetry()`.443444**TaskUpdate** — Mark complete.445446---447448## Phase 5.5: Wire Manual Instrumentation Scopes449450**TaskCreate** — "Wire InvokeAgentScope, InferenceScope, ExecuteToolScope"451452> **Auto-instrumentation vs manual:** Whether manual scopes are needed depends on `authMode` and453> whether auto-instrumentation framework extensions were installed in Phase 2.454>455> | Situation | InvokeAgentScope | InferenceScope | ExecuteToolScope |456> |---|---|---|---|457> | `authMode = "s2s"` | Required — add always | Required — add always | Required — add always |458> | OBO + framework extension installed (Phase 2) | Required — add always | **Skip** — auto-instrumentation generates these | Only for local/custom tools not covered by the extension |459> | OBO + no framework extension | Required — add always | Required — add always | Required — add always |460>461> "Autonomous" agents can run on either OBO or S2S — auth mode is the actual differentiator here, not whether the agent is autonomous.462>463> **Rule:** Never skip `InvokeAgentScope` — it wraps the turn and is always required for traces to464> appear in the MAC portal. Auto-instrumentation extensions cover LLM calls (`InferenceScope`) and465> framework-managed tool calls (`ExecuteToolScope`), but they do not wrap the agent turn itself.466467**Determine which scopes to add:**468469- If `authMode = "s2s"`: proceed directly — add all three scopes without prompting (required for S2S agents).470- If OBO and a framework extension **was** installed in Phase 2:471 - Add `InvokeAgentScope` always.472 - **Skip `In473474…(truncated)