-
Notifications
You must be signed in to change notification settings - Fork 28
fix(subagents): record spawn lifecycle in the session transcript #1468
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
118 changes: 118 additions & 0 deletions
118
src/Netclaw.Actors.Tests/SubAgents/SubAgentSpawnObservabilityTests.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,118 @@ | ||
| // ----------------------------------------------------------------------- | ||
| // <copyright file="SubAgentSpawnObservabilityTests.cs" company="Petabridge, LLC"> | ||
| // Copyright (C) 2026 - 2026 Petabridge, LLC <https://petabridge.com> | ||
| // </copyright> | ||
| // ----------------------------------------------------------------------- | ||
| using Microsoft.Extensions.Logging; | ||
| using Netclaw.Actors.SubAgents; | ||
| using Netclaw.Actors.Tools; | ||
| using Netclaw.Configuration; | ||
| using Netclaw.Tests.Utilities; | ||
| using Netclaw.Tools; | ||
| using Xunit; | ||
|
|
||
| namespace Netclaw.Actors.Tests.SubAgents; | ||
|
|
||
| /// <summary> | ||
| /// Regression coverage for sub-agent spawn observability. A sub-agent's own actor | ||
| /// logs go through Akka's async logger bridge, where the diagnostics AsyncLocal is | ||
| /// gone, so they never reach the per-session <c>session.log</c>. The spawn lifecycle | ||
| /// is instead recorded by parent-side breadcrumbs that must run while the parent | ||
| /// session scope is active — otherwise a refused or failed spawn is invisible in the | ||
| /// session transcript. These tests assert the scope is active at log time, which is | ||
| /// exactly the condition <c>RollingFileLoggerProvider</c> uses to route a line to | ||
| /// <c>session.log</c>. | ||
| /// </summary> | ||
| public sealed class SubAgentSpawnObservabilityTests : IDisposable | ||
| { | ||
| private const string SessionId = "C123/167.42"; | ||
|
|
||
| private readonly DisposableTempDir _dir = new(); | ||
| private readonly NetclawPaths _paths; | ||
|
|
||
| public SubAgentSpawnObservabilityTests() | ||
| { | ||
| _paths = new NetclawPaths(_dir.Path); | ||
| _paths.EnsureDirectoriesExist(); | ||
| } | ||
|
|
||
| public void Dispose() => _dir.Dispose(); | ||
|
|
||
| [Fact] | ||
| public async Task Spawner_missing_session_context_records_breadcrumbs_under_session_scope() | ||
| { | ||
| var logger = new CapturingLogger<SubAgentSpawner>(); | ||
| // Only the parent-side breadcrumb path runs before the early return, so the | ||
| // unused collaborators are never dereferenced. | ||
| var spawner = new SubAgentSpawner( | ||
| chatClientProvider: null!, | ||
| new ToolRegistry(), | ||
| toolAccessPolicy: null!, | ||
| approvalService: null, | ||
| promptProvider: null!, | ||
| logger); | ||
|
|
||
| // A context with a session id but no SpawnChildActor factory — the | ||
| // "subagent tried to spawn but never launched" failure shape. | ||
| var context = new ToolExecutionContext(SessionId, null) { Audience = TrustAudience.Personal }; | ||
|
|
||
| var result = await spawner.SpawnAsync( | ||
| Profile("summarizer"), "do the work", null, context, TestContext.Current.CancellationToken); | ||
|
|
||
| Assert.False(result.Success); | ||
| // The spawn attempt and its failure are both visible in the session transcript. | ||
| Assert.Contains(logger.Entries, e => e.SessionScope == SessionId && e.Message.Contains("spawn requested")); | ||
| Assert.Contains(logger.Entries, e => e.SessionScope == SessionId && e.Message.Contains("no session context available")); | ||
| } | ||
|
|
||
| [Fact] | ||
| public async Task Tool_refusal_records_real_reason_under_session_scope() | ||
| { | ||
| var logger = new CapturingLogger<SpawnAgentTool>(); | ||
| var registry = new SubAgentDefinitionRegistry(); | ||
| var tool = new SpawnAgentTool(registry, spawner: null!, _paths, logger: logger); | ||
|
|
||
| // Public audience is refused with a deliberately opaque model-facing string; | ||
| // the operator-facing breadcrumb must still record the real reason. | ||
| var context = new ToolExecutionContext(SessionId, null) { Audience = TrustAudience.Public }; | ||
|
|
||
| var result = await tool.ExecuteAsync( | ||
| new Dictionary<string, object?> { ["agent"] = "summarizer", ["task"] = "do the work" }, | ||
| context, | ||
| TestContext.Current.CancellationToken); | ||
|
|
||
| Assert.Equal("Error: This tool is not available.", result); | ||
| Assert.Contains( | ||
| logger.Entries, | ||
| e => e.Level == LogLevel.Warning | ||
| && e.SessionScope == SessionId | ||
| && e.Message.Contains("refused") | ||
| && e.Message.Contains("Public")); | ||
| } | ||
|
|
||
| private static SubAgentProfile Profile(string name) => new() | ||
| { | ||
| Name = name, | ||
| Description = "test agent", | ||
| SystemPrompt = "You are a test agent.", | ||
| ToolNames = ["file_read"], | ||
| Visibility = SubAgentVisibility.UserFacing | ||
| }; | ||
|
|
||
| private sealed class CapturingLogger<T> : ILogger<T> | ||
| { | ||
| public readonly List<(LogLevel Level, string Message, string? SessionScope)> Entries = new(); | ||
|
|
||
| public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null; | ||
|
|
||
| public bool IsEnabled(LogLevel logLevel) => true; | ||
|
|
||
| public void Log<TState>( | ||
| LogLevel logLevel, | ||
| EventId eventId, | ||
| TState state, | ||
| Exception? exception, | ||
| Func<TState, Exception?, string> formatter) | ||
| => Entries.Add((logLevel, formatter(state, exception), SessionDiagnosticsContext.SessionId)); | ||
| } | ||
| } |
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
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
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
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Need to load the logger early so the sub-agent logs get created right away during startup, otherwise we miss the trace for issues like #1467