Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 23 additions & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -1235,7 +1235,7 @@ const session = await client.createSession({

### Customize the System Message

Control the AI's behavior and personality:
Control the AI's behavior and personality by appending instructions:

```typescript
const session = await client.createSession({
Expand All @@ -1245,6 +1245,28 @@ const session = await client.createSession({
});
```

For more fine-grained control, use `mode: "customize"` to override individual sections of the system prompt while preserving the rest:

```typescript
const session = await client.createSession({
systemMessage: {
mode: "customize",
sections: {
tone: { action: "replace", content: "Respond in a warm, professional tone. Be thorough in explanations." },
code_change_rules: { action: "remove" },
guidelines: { action: "append", content: "\n* Always cite data sources" },
},
content: "Focus on financial analysis and reporting.",
},
});
```

Available section IDs: `identity`, `tone`, `tool_efficiency`, `environment_context`, `code_change_rules`, `guidelines`, `safety`, `tool_instructions`, `custom_instructions`, `last_instructions`.

Each override supports four actions: `replace`, `remove`, `append`, and `prepend`. Unknown section IDs are handled gracefully — content is appended to additional instructions and a warning is emitted; `remove` on unknown sections is silently ignored.
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

This guide says customize-mode overrides support only four actions, but the SDK also supports per-section transform callbacks (read current section content, return modified content). Please add transform to the documented actions and include a brief example so readers can discover and correctly use it.

Copilot uses AI. Check for mistakes.

See the language-specific SDK READMEs for examples in [TypeScript](../nodejs/README.md), [Python](../python/README.md), [Go](../go/README.md), and [C#](../dotnet/README.md).

---

## Connecting to an External CLI Server
Expand Down
28 changes: 28 additions & 0 deletions dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,34 @@ var session = await client.CreateSessionAsync(new SessionConfig
});
```

#### Customize Mode

Use `Mode = SystemMessageMode.Customize` to selectively override individual sections of the prompt while preserving the rest:

```csharp
var session = await client.CreateSessionAsync(new SessionConfig
{
Model = "gpt-5",
SystemMessage = new SystemMessageConfig
{
Mode = SystemMessageMode.Customize,
Sections = new Dictionary<string, SectionOverride>
{
[SystemPromptSections.Tone] = new() { Action = SectionOverrideAction.Replace, Content = "Respond in a warm, professional tone. Be thorough in explanations." },
[SystemPromptSections.CodeChangeRules] = new() { Action = SectionOverrideAction.Remove },
[SystemPromptSections.Guidelines] = new() { Action = SectionOverrideAction.Append, Content = "\n* Always cite data sources" },
},
Content = "Focus on financial analysis and reporting."
}
});
```

Available section IDs are defined as constants on `SystemPromptSections`: `Identity`, `Tone`, `ToolEfficiency`, `EnvironmentContext`, `CodeChangeRules`, `Guidelines`, `Safety`, `ToolInstructions`, `CustomInstructions`, `LastInstructions`.

Each section override supports four actions: `Replace`, `Remove`, `Append`, and `Prepend`. Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored.
Copy link
Contributor

Choose a reason for hiding this comment

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

The README lists "four actions" but the code supports five: Replace, Remove, Append, Prepend, and transform via the Transform property.

Consider updating to:

Each section override supports five actions: `Replace`, `Remove`, `Append`, `Prepend`, and **transform callbacks via the `Transform` property**. 

The `Transform` property accepts a `Func(string, Task(string))` delegate that receives the current section content and returns the modified content:

\```csharp
SystemMessage = new SystemMessageConfig
{
    Mode = SystemMessageMode.Customize,
    Sections = new Dictionary(string, SectionOverride)
    {
        [SystemPromptSections.Identity] = new SectionOverride
        {
            Transform = async (content) =>
            {
                // Log for observability
                Console.WriteLine($"Identity: {content}");
                // Modify and return
                return content.Replace("GitHub Copilot", "Acme Assistant");
            }
        }
    }
}
\```

Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored.


Comment on lines +536 to +537
Copy link

Copilot AI Mar 20, 2026

Choose a reason for hiding this comment

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

This section documents only Replace/Remove/Append/Prepend, but the C# SDK also supports per-section transforms via SectionOverride.Transform (serialized as action transform). Please document Transform here (and how it interacts with Action) so consumers can use the read/modify/write capability.

Suggested change
Each section override supports four actions: `Replace`, `Remove`, `Append`, and `Prepend`. Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored.
Each section override supports four built-in `Action` values: `Replace`, `Remove`, `Append`, and `Prepend`. Unknown section IDs are handled gracefully: content is appended to additional instructions, and `Remove` overrides are silently ignored.
For advanced scenarios, `SectionOverride` also exposes an optional `Transform` delegate that lets you read and modify the existing section text. When `Transform` is set, it is used instead of the `Action`/`Content` pair and is serialized as an action of type `transform` in the underlying JSON-RPC payloads. For a given section override you should typically set either `Action`/`Content` or `Transform`, but not both.

Copilot uses AI. Check for mistakes.
#### Replace Mode

For full control (removes all guardrails), use `Mode = SystemMessageMode.Replace`:

```csharp
Expand Down
62 changes: 60 additions & 2 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,44 @@ private async Task CleanupConnectionAsync(List<Exception>? errors)
}
}

private static (SystemMessageConfig? wireConfig, Dictionary<string, Func<string, Task<string>>>? callbacks) ExtractTransformCallbacks(SystemMessageConfig? systemMessage)
{
if (systemMessage?.Mode != SystemMessageMode.Customize || systemMessage.Sections == null)
{
return (systemMessage, null);
}

var callbacks = new Dictionary<string, Func<string, Task<string>>>();
var wireSections = new Dictionary<string, SectionOverride>();

foreach (var (sectionId, sectionOverride) in systemMessage.Sections)
{
if (sectionOverride.Transform != null)
{
callbacks[sectionId] = sectionOverride.Transform;
wireSections[sectionId] = new SectionOverride { Action = SectionOverrideAction.Transform };
}
else
{
wireSections[sectionId] = sectionOverride;
}
}

if (callbacks.Count == 0)
{
return (systemMessage, null);
}

var wireConfig = new SystemMessageConfig
{
Mode = systemMessage.Mode,
Content = systemMessage.Content,
Sections = wireSections
};

return (wireConfig, callbacks);
}

/// <summary>
/// Creates a new Copilot session with the specified configuration.
/// </summary>
Expand Down Expand Up @@ -409,6 +447,8 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.Hooks.OnSessionEnd != null ||
config.Hooks.OnErrorOccurred != null);

var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage);

var sessionId = config.SessionId ?? Guid.NewGuid().ToString();

// Create and register the session before issuing the RPC so that
Expand All @@ -424,6 +464,10 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
{
session.RegisterHooks(config.Hooks);
}
if (transformCallbacks != null)
{
session.RegisterTransformCallbacks(transformCallbacks);
}
if (config.OnEvent != null)
{
session.On(config.OnEvent);
Expand All @@ -440,7 +484,7 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig config, Cance
config.ClientName,
config.ReasoningEffort,
config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
config.SystemMessage,
wireSystemMessage,
config.AvailableTools,
config.ExcludedTools,
config.Provider,
Expand Down Expand Up @@ -519,6 +563,8 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.Hooks.OnSessionEnd != null ||
config.Hooks.OnErrorOccurred != null);

var (wireSystemMessage, transformCallbacks) = ExtractTransformCallbacks(config.SystemMessage);

// Create and register the session before issuing the RPC so that
// events emitted by the CLI (e.g. session.start) are not dropped.
var session = new CopilotSession(sessionId, connection.Rpc, _logger);
Expand All @@ -532,6 +578,10 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
{
session.RegisterHooks(config.Hooks);
}
if (transformCallbacks != null)
{
session.RegisterTransformCallbacks(transformCallbacks);
}
if (config.OnEvent != null)
{
session.On(config.OnEvent);
Expand All @@ -548,7 +598,7 @@ public async Task<CopilotSession> ResumeSessionAsync(string sessionId, ResumeSes
config.Model,
config.ReasoningEffort,
config.Tools?.Select(ToolDefinition.FromAIFunction).ToList(),
config.SystemMessage,
wireSystemMessage,
config.AvailableTools,
config.ExcludedTools,
config.Provider,
Expand Down Expand Up @@ -1222,6 +1272,7 @@ private async Task<Connection> ConnectToServerAsync(Process? cliProcess, string?
rpc.AddLocalRpcMethod("permission.request", handler.OnPermissionRequestV2);
rpc.AddLocalRpcMethod("userInput.request", handler.OnUserInputRequest);
rpc.AddLocalRpcMethod("hooks.invoke", handler.OnHooksInvoke);
rpc.AddLocalRpcMethod("systemMessage.transform", handler.OnSystemMessageTransform);
rpc.StartListening();

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

public async Task<SystemMessageTransformRpcResponse> OnSystemMessageTransform(string sessionId, JsonElement sections)
{
var session = client.GetSession(sessionId) ?? throw new ArgumentException($"Unknown session {sessionId}");
return await session.HandleSystemMessageTransformAsync(sections);
}

// Protocol v2 backward-compatibility adapters

public async Task<ToolCallResponseV2> OnToolCallV2(string sessionId,
Expand Down Expand Up @@ -1685,6 +1742,7 @@ private static LogLevel MapLevel(TraceEventType eventType)
[JsonSerializable(typeof(ResumeSessionResponse))]
[JsonSerializable(typeof(SessionMetadata))]
[JsonSerializable(typeof(SystemMessageConfig))]
[JsonSerializable(typeof(SystemMessageTransformRpcResponse))]
[JsonSerializable(typeof(ToolCallResponseV2))]
[JsonSerializable(typeof(ToolDefinition))]
[JsonSerializable(typeof(ToolResultAIContent))]
Expand Down
5 changes: 1 addition & 4 deletions dotnet/src/SdkProtocolVersion.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,5 @@ internal static class SdkProtocolVersion
/// <summary>
/// Gets the SDK protocol version.
/// </summary>
public static int GetVersion()
{
return Version;
}
public static int GetVersion() => Version;
}
71 changes: 71 additions & 0 deletions dotnet/src/Session.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ public sealed partial class CopilotSession : IAsyncDisposable

private SessionHooks? _hooks;
private readonly SemaphoreSlim _hooksLock = new(1, 1);
private Dictionary<string, Func<string, Task<string>>>? _transformCallbacks;
private readonly SemaphoreSlim _transformCallbacksLock = new(1, 1);
private SessionRpc? _sessionRpc;
private int _isDisposed;

Expand Down Expand Up @@ -653,6 +655,72 @@ internal void RegisterHooks(SessionHooks hooks)
};
}

/// <summary>
/// Registers transform callbacks for system message sections.
/// </summary>
/// <param name="callbacks">The transform callbacks keyed by section identifier.</param>
internal void RegisterTransformCallbacks(Dictionary<string, Func<string, Task<string>>>? callbacks)
{
_transformCallbacksLock.Wait();
try
{
_transformCallbacks = callbacks;
}
finally
{
_transformCallbacksLock.Release();
}
}

/// <summary>
/// Handles a systemMessage.transform RPC call from the Copilot CLI.
/// </summary>
/// <param name="sections">The raw JSON element containing sections to transform.</param>
/// <returns>A task that resolves with the transformed sections.</returns>
internal async Task<SystemMessageTransformRpcResponse> HandleSystemMessageTransformAsync(JsonElement sections)
{
Dictionary<string, Func<string, Task<string>>>? callbacks;
await _transformCallbacksLock.WaitAsync();
try
{
callbacks = _transformCallbacks;
}
finally
{
_transformCallbacksLock.Release();
}

var parsed = JsonSerializer.Deserialize(
sections.GetRawText(),
SessionJsonContext.Default.DictionaryStringSystemMessageTransformSection) ?? new();

var result = new Dictionary<string, SystemMessageTransformSection>();
foreach (var (sectionId, data) in parsed)
{
Func<string, Task<string>>? callback = null;
callbacks?.TryGetValue(sectionId, out callback);

if (callback != null)
{
try
{
var transformed = await callback(data.Content ?? "");
result[sectionId] = new SystemMessageTransformSection { Content = transformed };
}
catch
{
result[sectionId] = new SystemMessageTransformSection { Content = data.Content ?? "" };
}
Comment on lines +710 to +713
}
else
{
result[sectionId] = new SystemMessageTransformSection { Content = data.Content ?? "" };
}
}

return new SystemMessageTransformRpcResponse { Sections = result };
}

/// <summary>
/// Gets the complete list of messages and events in the session.
/// </summary>
Expand Down Expand Up @@ -891,5 +959,8 @@ internal record SessionDestroyRequest
[JsonSerializable(typeof(SessionEndHookOutput))]
[JsonSerializable(typeof(ErrorOccurredHookInput))]
[JsonSerializable(typeof(ErrorOccurredHookOutput))]
[JsonSerializable(typeof(SystemMessageTransformSection))]
[JsonSerializable(typeof(SystemMessageTransformRpcResponse))]
[JsonSerializable(typeof(Dictionary<string, SystemMessageTransformSection>))]
internal partial class SessionJsonContext : JsonSerializerContext;
}
Loading
Loading