Skip to content

Commit 150eb33

Browse files
MackinnonBuckCopilot
authored andcommitted
Add transform operation for system prompt section customization
Adds a new 'transform' action to SectionOverride that enables read-then-write mutation of system prompt sections via callbacks. The SDK intercepts function- valued actions before serialization, stores the callbacks locally, and handles the batched systemMessage.transform JSON-RPC callback from the runtime. Changes across all 4 SDKs (TypeScript, Python, Go, .NET): - Types: SectionTransformFn, SectionOverrideAction (TS/Python), Transform field (Go/.NET), SectionOverrideAction constants (Go) - Client: extractTransformCallbacks helper, transform callback registration, systemMessage.transform RPC handler - Session: transform callback storage and batched dispatch with error handling - E2E tests and shared snapshot YAML files Wire protocol: single batched RPC call with all transform sections, matching the runtime implementation in copilot-agent-runtime PR #5103. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent bd39949 commit 150eb33

21 files changed

+1330
-26
lines changed

dotnet/src/Client.cs

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,44 @@ private async Task CleanupConnectionAsync(List<Exception>? errors)
365365
}
366366
}
367367

368+
private static (SystemMessageConfig? wireConfig, Dictionary<string, Func<string, Task<string>>>? callbacks) ExtractTransformCallbacks(SystemMessageConfig? systemMessage)
369+
{
370+
if (systemMessage?.Mode != SystemMessageMode.Customize || systemMessage.Sections == null)
371+
{
372+
return (systemMessage, null);
373+
}
374+
375+
var callbacks = new Dictionary<string, Func<string, Task<string>>>();
376+
var wireSections = new Dictionary<string, SectionOverride>();
377+
378+
foreach (var (sectionId, sectionOverride) in systemMessage.Sections)
379+
{
380+
if (sectionOverride.Transform != null)
381+
{
382+
callbacks[sectionId] = sectionOverride.Transform;
383+
wireSections[sectionId] = new SectionOverride { Action = SectionOverrideAction.Transform };
384+
}
385+
else
386+
{
387+
wireSections[sectionId] = sectionOverride;
388+
}
389+
}
390+
391+
if (callbacks.Count == 0)
392+
{
393+
return (systemMessage, null);
394+
}
395+
396+
var wireConfig = new SystemMessageConfig
397+
{
398+
Mode = systemMessage.Mode,
399+
Content = systemMessage.Content,
400+
Sections = wireSections
401+
};
402+
403+
return (wireConfig, callbacks);
404+
}
405+
368406
/// <summary>
369407
/// Creates a new Copilot session with the specified configuration.
370408
/// </summary>
@@ -409,6 +447,8 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
409447
config.Hooks.OnSessionEnd != null ||
410448
config.Hooks.OnErrorOccurred != null);
411449

450+
var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage);
451+
412452
var sessionId = config.SessionId ?? Guid.NewGuid().ToString();
413453

414454
// Create and register the session before issuing the RPC so that
@@ -424,6 +464,10 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
424464
{
425465
session.RegisterHooks(config.Hooks);
426466
}
467+
if (transformCallbacks != null)
468+
{
469+
session.RegisterTransformCallbacks(transformCallbacks);
470+
}
427471
if (config.OnEvent != null)
428472
{
429473
session.On(config.OnEvent);
@@ -440,7 +484,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
440484
config.ClientName,
441485
config.ReasoningEffort,
442486
config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
443-
config.SystemMessage,
487+
wireSystemMessage,
444488
config.AvailableTools,
445489
config.ExcludedTools,
446490
config.Provider,
@@ -519,6 +563,8 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
519563
config.Hooks.OnSessionEnd != null ||
520564
config.Hooks.OnErrorOccurred != null);
521565

566+
var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage);
567+
522568
// Create and register the session before issuing the RPC so that
523569
// events emitted by the CLI (e.g. session.start) are not dropped.
524570
var session = new CopilotSession(sessionId, connection.Rpc, _logger);
@@ -532,6 +578,10 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
532578
{
533579
session.RegisterHooks(config.Hooks);
534580
}
581+
if (transformCallbacks != null)
582+
{
583+
session.RegisterTransformCallbacks(transformCallbacks);
584+
}
535585
if (config.OnEvent != null)
536586
{
537587
session.On(config.OnEvent);
@@ -548,7 +598,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
548598
config.Model,
549599
config.ReasoningEffort,
550600
config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
551-
config.SystemMessage,
601+
wireSystemMessage,
552602
config.AvailableTools,
553603
config.ExcludedTools,
554604
config.Provider,
@@ -1222,6 +1272,7 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
12221272
rpc.AddLocalRpcMethod("permission.request", handler.OnPermissionRequestV2);
12231273
rpc.AddLocalRpcMethod("userInput.request", handler.OnUserInputRequest);
12241274
rpc.AddLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke);
1275+
rpc.AddLocalRpcMethod("systemMessage.transform", handler.OnSystemMessageTransform);
12251276
rpc.StartListening();
12261277

12271278
// Transition state to Disconnected if the JSON-RPC connection drops
@@ -1350,6 +1401,12 @@ public async Task<HooksInvokeResponse> OnHooksInvoke(string sessionId, string ho
13501401
return new HooksInvokeResponse(output);
13511402
}
13521403

1404+
public async Task<SystemMessageTransformRpcResponse> OnSystemMessageTransform(string sessionId, JsonElement sections)
1405+
{
1406+
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
1407+
return await session.HandleSystemMessageTransformAsync(sections);
1408+
}
1409+
13531410
// Protocol v2 backward-compatibility adapters
13541411

13551412
public async Task<ToolCallResponseV2> OnToolCallV2(string sessionId,
@@ -1683,9 +1740,9 @@ private static LogLevel MapLevel(TraceEventType eventType)
16831740
[JsonSerializable(typeof(ProviderConfig))]
16841741
[JsonSerializable(typeof(ResumeSessionRequest))]
16851742
[JsonSerializable(typeof(ResumeSessionResponse))]
1686-
[JsonSerializable(typeof(SectionOverride))]
16871743
[JsonSerializable(typeof(SessionMetadata))]
16881744
[JsonSerializable(typeof(SystemMessageConfig))]
1745+
[JsonSerializable(typeof(SystemMessageTransformRpcResponse))]
16891746
[JsonSerializable(typeof(ToolCallResponseV2))]
16901747
[JsonSerializable(typeof(ToolDefinition))]
16911748
[JsonSerializable(typeof(ToolResultAIContent))]

dotnet/src/Session.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ public sealed partial class CopilotSession : IAsyncDisposable
6565

6666
private SessionHooks? _hooks;
6767
private readonly SemaphoreSlim _hooksLock = new(1, 1);
68+
private Dictionary<string, Func<string, Task<string>>>? _transformCallbacks;
69+
private readonly SemaphoreSlim _transformCallbacksLock = new(1, 1);
6870
private SessionRpc? _sessionRpc;
6971
private int _isDisposed;
7072

@@ -653,6 +655,72 @@ internal void RegisterHooks(SessionHooks hooks)
653655
};
654656
}
655657

658+
/// <summary>
659+
/// Registers transform callbacks for system message sections.
660+
/// </summary>
661+
/// <param name="callbacks">The transform callbacks keyed by section identifier.</param>
662+
internal void RegisterTransformCallbacks(Dictionary<string, Func<string, Task<string>>>? callbacks)
663+
{
664+
_transformCallbacksLock.Wait();
665+
try
666+
{
667+
_transformCallbacks = callbacks;
668+
}
669+
finally
670+
{
671+
_transformCallbacksLock.Release();
672+
}
673+
}
674+
675+
/// <summary>
676+
/// Handles a systemMessage.transform RPC call from the Copilot CLI.
677+
/// </summary>
678+
/// <param name="sections">The raw JSON element containing sections to transform.</param>
679+
/// <returns>A task that resolves with the transformed sections.</returns>
680+
internal async Task<SystemMessageTransformRpcResponse> HandleSystemMessageTransformAsync(JsonElement sections)
681+
{
682+
Dictionary<string, Func<string, Task<string>>>? callbacks;
683+
await _transformCallbacksLock.WaitAsync();
684+
try
685+
{
686+
callbacks = _transformCallbacks;
687+
}
688+
finally
689+
{
690+
_transformCallbacksLock.Release();
691+
}
692+
693+
var parsed = JsonSerializer.Deserialize(
694+
sections.GetRawText(),
695+
SessionJsonContext.Default.DictionaryStringSystemMessageTransformSection) ?? new();
696+
697+
var result = new Dictionary<string, SystemMessageTransformSection>();
698+
foreach (var (sectionId, data) in parsed)
699+
{
700+
Func<string, Task<string>>? callback = null;
701+
callbacks?.TryGetValue(sectionId, out callback);
702+
703+
if (callback != null)
704+
{
705+
try
706+
{
707+
var transformed = await callback(data.Content ?? "");
708+
result[sectionId] = new SystemMessageTransformSection { Content = transformed };
709+
}
710+
catch
711+
{
712+
result[sectionId] = new SystemMessageTransformSection { Content = data.Content ?? "" };
713+
}
714+
}
715+
else
716+
{
717+
result[sectionId] = new SystemMessageTransformSection { Content = data.Content ?? "" };
718+
}
719+
}
720+
721+
return new SystemMessageTransformRpcResponse { Sections = result };
722+
}
723+
656724
/// <summary>
657725
/// Gets the complete list of messages and events in the session.
658726
/// </summary>
@@ -890,5 +958,8 @@ internal record SessionDestroyRequest
890958
[JsonSerializable(typeof(SessionEndHookOutput))]
891959
[JsonSerializable(typeof(ErrorOccurredHookInput))]
892960
[JsonSerializable(typeof(ErrorOccurredHookOutput))]
961+
[JsonSerializable(typeof(SystemMessageTransformSection))]
962+
[JsonSerializable(typeof(SystemMessageTransformRpcResponse))]
963+
[JsonSerializable(typeof(Dictionary<string, SystemMessageTransformSection>))]
893964
internal partial class SessionJsonContext : JsonSerializerContext;
894965
}

dotnet/src/Types.cs

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -991,7 +991,10 @@ public enum SectionOverrideAction
991991
Append,
992992
/// <summary>Prepend content before the existing section.</summary>
993993
[JsonStringEnumMemberName("prepend")]
994-
Prepend
994+
Prepend,
995+
/// <summary>Transform the section content via a callback.</summary>
996+
[JsonStringEnumMemberName("transform")]
997+
Transform
995998
}
996999

9971000
/// <summary>
@@ -1000,14 +1003,24 @@ public enum SectionOverrideAction
10001003
public class SectionOverride
10011004
{
10021005
/// <summary>
1003-
/// The operation to perform on this section.
1006+
/// The operation to perform on this section. Ignored when Transform is set.
10041007
/// </summary>
1005-
public SectionOverrideAction Action { get; set; }
1008+
[JsonPropertyName("action")]
1009+
public SectionOverrideAction? Action { get; set; }
10061010

10071011
/// <summary>
10081012
/// Content for the override. Optional for all actions. Ignored for remove.
10091013
/// </summary>
1014+
[JsonPropertyName("content")]
10101015
public string? Content { get; set; }
1016+
1017+
/// <summary>
1018+
/// Transform callback. When set, takes precedence over Action.
1019+
/// Receives current section content, returns transformed content.
1020+
/// Not serialized — the SDK handles this locally.
1021+
/// </summary>
1022+
[JsonIgnore]
1023+
public Func<string, Task<string>>? Transform { get; set; }
10111024
}
10121025

10131026
/// <summary>
@@ -2106,6 +2119,30 @@ public class SetForegroundSessionResponse
21062119
public string? Error { get; set; }
21072120
}
21082121

2122+
/// <summary>
2123+
/// Content data for a single system prompt section in a transform RPC call.
2124+
/// </summary>
2125+
public class SystemMessageTransformSection
2126+
{
2127+
/// <summary>
2128+
/// The content of the section.
2129+
/// </summary>
2130+
[JsonPropertyName("content")]
2131+
public string? Content { get; set; }
2132+
}
2133+
2134+
/// <summary>
2135+
/// Response to a systemMessage.transform RPC call.
2136+
/// </summary>
2137+
public class SystemMessageTransformRpcResponse
2138+
{
2139+
/// <summary>
2140+
/// The transformed sections keyed by section identifier.
2141+
/// </summary>
2142+
[JsonPropertyName("sections")]
2143+
public Dictionary<string, SystemMessageTransformSection>? Sections { get; set; }
2144+
}
2145+
21092146
[JsonSourceGenerationOptions(
21102147
JsonSerializerDefaults.Web,
21112148
AllowOutOfOrderMetadataProperties = true,

0 commit comments

Comments
 (0)