fix(subagents): session-correlated sub-agent log observability#1428
Merged
Conversation
This was referenced Jun 17, 2026
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):
Dropped (non-functional / counterproductive):
Real fix for non-streaming progress (#1429): Tests (#1431): |
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
force-pushed
the
feat/subagent-observability
branch
from
June 18, 2026 00:05
303b118 to
f2d801c
Compare
Aaronontheweb
approved these changes
Jun 18, 2026
| enrichedLog = enrichedLog.WithContext("SessionId", parentSessionId); | ||
| if (!string.IsNullOrWhiteSpace(scopeId)) | ||
| enrichedLog = enrichedLog.WithContext("SubSessionId", scopeId); | ||
| _log = enrichedLog; |
Aaronontheweb
enabled auto-merge (squash)
June 18, 2026 00:15
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
SessionId(the parent session, for correlation — the same key the session/channel actors already use viaWithContext, documented inSessionLoggingScope) andSubSessionId(the full{parent}/subagent/{name}/{runId}scope, so a single sub-agent run is isolable and every line is plainly the sub-agent's).EmitActivitynow logs each progress phase, so phases surface in Seq even on the non-streamingspawn_agentpath where there is no activity sink (previously a no-op there). The per-delta streaming ping staysDebug-only so we don't emit one Info line per token.Debug → Information.SubAgentSpawner
nullsink 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 —
TelemetryRegistrationExtensionswires up logs + metrics only (noWithTracing/AddSource), so rawnew Activity()objects export nowhere. The earlier draft'sActivitycode (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
SubAgentObservabilityTestscovers 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 deadActivitycode it referenced is removed here.