Skip to content

fix(subagents): session-correlated sub-agent log observability#1428

Merged
Aaronontheweb merged 4 commits into
devfrom
feat/subagent-observability
Jun 18, 2026
Merged

fix(subagents): session-correlated sub-agent log observability#1428
Aaronontheweb merged 4 commits into
devfrom
feat/subagent-observability

Conversation

@petabridge-netclaw

@petabridge-netclaw petabridge-netclaw Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Sub-agent observability: session-correlated logs

Makes everything a sub-agent does show up in the logs, correlated back to the parent session and clearly attributable to the sub-agent — without depending on a tracing pipeline the daemon doesn't have.

SubAgentActor

  • Enriches the logger with SessionId (the parent session, for correlation — the same key the session/channel actors already use via WithContext, documented in SessionLoggingScope) and SubSessionId (the full {parent}/subagent/{name}/{runId} scope, so a single sub-agent run is isolable and every line is plainly the sub-agent's).
  • EmitActivity now logs each progress phase, so phases surface in Seq even on the non-streaming spawn_agent path where there is no activity sink (previously a no-op there). The per-delta streaming ping stays Debug-only so we don't emit one Info line per token.
  • Tool-start events and a completion-summary line with cumulative stats.
  • Tool-denial logging promoted Debug → Information.

SubAgentSpawner

  • Non-streaming callers pass a null sink again; progress flows through the sub-agent's own session-correlated logs.

Intentionally NOT done

No OpenTelemetry trace spans. The daemon configures no tracing pipeline — TelemetryRegistrationExtensions wires up logs + metrics only (no WithTracing/AddSource), so raw new Activity() objects export nowhere. The earlier draft's Activity code (in both files) was removed; the unbounded never-read "discard channel" was removed too. Structured logs (SessionId/SubSessionId) are the supported correlation path today.

Tests

SubAgentObservabilityTests covers progress-phase, completion-summary, and tool-start logging on the non-streaming (no-sink) path. All sub-agent tests pass; slopwatch clean; copyright headers verified.

Issues

Closes #1429
Closes #1431

#1430 (OTel spans) was closed separately as not planned — the dead Activity code it referenced is removed here.

@petabridge-netclaw petabridge-netclaw Bot added bug Something isn't working observability labels Jun 17, 2026
Comment thread src/Netclaw.Actors/SubAgents/SubAgentActor.cs Fixed
Comment thread src/Netclaw.Actors/SubAgents/SubAgentSpawner.cs Fixed
@Aaronontheweb

Copy link
Copy Markdown
Collaborator

Revised this PR (commit 303b118) to keep the parts that work and drop the parts that can't, based on how session observability is actually wired in this repo.

Kept (the genuinely useful half):

  • SessionId logger enrichment via WithContext("SessionId", …) — this matches the convention the session/channel actors already use (SlackConversationActor, MattermostSessionBindingActor, SignalRSessionActor) and the key documented in SessionLoggingScope, so sub-agent logs now share one filterable attribute with the parent.
  • Added a second tag, SubSessionId = the full {parent}/subagent/{name}/{runId} scope, so a single sub-agent run is isolable and every line is plainly attributable to the sub-agent.
  • Tool-start events, the completion summary line, and the tool-denial Debug→Information bump.

Dropped (non-functional / counterproductive):

  • Both new Activity("netclaw.subagent.*") blocks. The daemon configures no OpenTelemetry tracing pipelineTelemetryRegistrationExtensions wires up logs (AddOpenTelemetry + IncludeScopes) and metrics (WithMetrics) only, with no WithTracing/AddSource. Raw new Activity() instances aren't tied to a registered ActivitySource, so they export nowhere. (The SubAgentActor one was also never .Start()ed/disposed — dead even on its own terms.) Tracked as Sub-agent internal operations lack OpenTelemetry trace spans #1430.
  • The "discard channel" — it was an unbounded channel nobody reads, so it accumulated ToolActivityUpdates in memory and surfaced nothing in Seq. EmitActivity doesn't log; it only writes to that sink. Replaced with the real fix below.
  • The dangling _diagnosticsScope comment (referenced code that was never written; the LLM call already runs inside SessionDiagnosticsContext.Push).

Real fix for non-streaming progress (#1429): EmitActivity now logs each phase through the enriched logger, so phases reach Seq on the non-streaming spawn_agent path too. The per-delta streaming ping is exempt (it's already logged at Debug) to avoid one Info line per token.

Tests (#1431): SubAgentObservabilityTests covers progress-phase, completion-summary, and tool-start logging on the no-sink path. All 95 sub-agent tests pass; slopwatch clean; headers verified.

@Aaronontheweb Aaronontheweb changed the title fix(providers): improve sub-agent observability and session trace correlation fix(subagents): session-correlated sub-agent log observability Jun 18, 2026
Netclaw Bot and others added 2 commits June 18, 2026 00:05
Add SessionId property enrichment so sub-agent events appear in
session-scoped Seq queries. Previously sub-agents had no SessionId
on their log events — only the ActorPath string contained it.

Changes:

SubAgentActor:
- Enrich logger with SessionId via Context.GetLogger().WithContext()
- Add tool START events (previously only tool results were logged)
- Add summary completion log with cumulative stats (totalToolCalls,
  iterations, duration)
- Track wall-clock duration with Stopwatch for the summary log

SubAgentSpawner:
- Make activitySink mandatory — create discard channel for non-streaming
  paths so all sub-agents emit activity events (previously non-streaming
  paths like spawn_agent had null sink = EmitActivity was a NOOP)
- Add OpenTelemetry trace correlation (child activity linked to parent
  session's current trace) so Seq/Grafana shows sub-agents as spans
  under the parent session
- Promote tool denial logging from LogDebug to LogInformation so tool
  denials are visible at production INFO log level
Refine the sub-agent observability instrumentation so everything the
sub-agent does shows up in the logs, correlated back to the parent
session, without relying on a tracing pipeline that does not exist.

SubAgentActor:
- Enrich the logger with SessionId (parent, for correlation) and
  SubSessionId (the full "{parent}/subagent/{name}/{runId}" scope) so a
  single run is isolable and plainly attributable to the sub-agent.
  SessionId matches the key the session/channel actors already use via
  WithContext, so parent and sub-agent logs share one filterable key.
- EmitActivity now logs each progress phase, so phases surface in Seq
  even on the non-streaming spawn_agent path where there is no activity
  sink (EmitActivity was previously a no-op there). The per-delta
  streaming ping is exempt (already logged at Debug) to avoid flooding.
- Remove the dead "netclaw.subagent.run" Activity: it was never started,
  stopped, or disposed, and the daemon configures no OpenTelemetry
  tracing pipeline (logs + metrics only), so raw Activities export
  nowhere.
- Drop the dangling _diagnosticsScope comment for code never written;
  the LLM call already runs inside SessionDiagnosticsContext.Push.

SubAgentSpawner:
- Remove the "netclaw.subagent.spawn" Activity for the same reason.
- Remove the unbounded discard channel whose reader was never consumed:
  it accumulated activity updates in memory and surfaced nothing in Seq.
  Non-streaming callers pass a null sink again; progress now flows
  through the sub-agent's own session-correlated logs.
- Keep the tool-denial log promotion to Information.

Tests (SubAgentObservabilityTests): progress phase, completion summary,
and tool-start events are logged on the non-streaming path.

Addresses #1429 and #1431. #1430 is deferred: it needs a real tracing
pipeline (WithTracing + a registered ActivitySource), not orphan
Activities; structured logs are the supported correlation path today.
@Aaronontheweb
Aaronontheweb force-pushed the feat/subagent-observability branch from 303b118 to f2d801c Compare June 18, 2026 00:05

@Aaronontheweb Aaronontheweb left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

enrichedLog = enrichedLog.WithContext("SessionId", parentSessionId);
if (!string.IsNullOrWhiteSpace(scopeId))
enrichedLog = enrichedLog.WithContext("SubSessionId", scopeId);
_log = enrichedLog;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM

@Aaronontheweb
Aaronontheweb enabled auto-merge (squash) June 18, 2026 00:15
@Aaronontheweb
Aaronontheweb merged commit 7c3a853 into dev Jun 18, 2026
21 checks passed
@Aaronontheweb
Aaronontheweb deleted the feat/subagent-observability branch June 18, 2026 01:04
Aaronontheweb added a commit that referenced this pull request Jul 8, 2026
… (#1600)

Sub-agent LLM calls never recorded token usage, so every sub-agent's
input/output tokens were invisible to `netclaw stats`. This was a
pre-existing gap, not a regression: SubAgentActor never had an
ISessionMetrics dependency and discarded the ChatResponse.Usage it already
receives from StreamingResponseReader. The recent observability PRs
(#1428, #1468, #1472/#1499) only added logs and pruned dead OTel Activities;
none ever touched token tracking.

Fix records at the source, mirroring the main session:
- Inject ISessionMetrics into SubAgentActor (via SubAgentSpawner/CreateProps).
  DI already registers it as a process-wide singleton, so no Program.cs change.
- Record response.Usage on every LlmResponseReceived (one per LLM turn:
  tool-call turns, retries, the forced-no-tools final turn, repair turns).
- Add cumulative input/output token totals to the completion summary log.

Recorded in the child, not propagated to the parent: ISessionMetrics is the
SAME process-wide singleton both share, so re-recording in the parent would
double-count, and folding sub-agent tokens into the parent's UsageOutput
would corrupt its context-window percentage (the sub-agent has its own
context window).

Regression coverage (all four fail if the recording is removed):
- SubAgentActor bills usage per LLM call and sums across the turn loop.
- Completion summary log carries token totals.
- Full spawner->CreateProps->actor wiring bills tokens to the spawner's metrics.

Adds a UsageOverride hook to the sub-agent test FakeChatClient and extracts a
shared RecordingSessionMetrics test helper.
Aaronontheweb added a commit that referenced this pull request Jul 8, 2026
… (#1600)

Sub-agent LLM calls never recorded token usage, so every sub-agent's
input/output tokens were invisible to `netclaw stats`. This was a
pre-existing gap, not a regression: SubAgentActor never had an
ISessionMetrics dependency and discarded the ChatResponse.Usage it already
receives from StreamingResponseReader. The recent observability PRs
(#1428, #1468, #1472/#1499) only added logs and pruned dead OTel Activities;
none ever touched token tracking.

Fix records at the source, mirroring the main session:
- Inject ISessionMetrics into SubAgentActor (via SubAgentSpawner/CreateProps).
  DI already registers it as a process-wide singleton, so no Program.cs change.
- Record response.Usage on every LlmResponseReceived (one per LLM turn:
  tool-call turns, retries, the forced-no-tools final turn, repair turns).
- Add cumulative input/output token totals to the completion summary log.

Recorded in the child, not propagated to the parent: ISessionMetrics is the
SAME process-wide singleton both share, so re-recording in the parent would
double-count, and folding sub-agent tokens into the parent's UsageOutput
would corrupt its context-window percentage (the sub-agent has its own
context window).

Regression coverage (all four fail if the recording is removed):
- SubAgentActor bills usage per LLM call and sums across the turn loop.
- Completion summary log carries token totals.
- Full spawner->CreateProps->actor wiring bills tokens to the spawner's metrics.

Adds a UsageOverride hook to the sub-agent test FakeChatClient and extracts a
shared RecordingSessionMetrics test helper.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working observability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Missing test coverage for sub-agent observability changes Non-streaming sub-agents lack progress events in Seq

1 participant