Skip to content

Commit fa4e463

Browse files
Add MessageOptions.agentMode and fix per-message mode misuse
Adds a public agentMode field on MessageOptions across .NET, Node, Python, and Go SDKs so callers can set the per-message UI mode (interactive/plan/autopilot/shell) without abusing the queue-delivery mode field. Fixes the misleading Mode XML doc in .NET, and updates the four E2E/unit tests that were setting mode=`plan` (via casts, type:ignore, or silently in .NET) to use the new agentMode field and assert the runtime echoes it back on user.message.
1 parent 36d1906 commit fa4e463

22 files changed

Lines changed: 243 additions & 40 deletions

dotnet/src/Session.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ public async Task<string> SendAsync(MessageOptions options, CancellationToken ca
276276
Prompt = options.Prompt,
277277
Attachments = options.Attachments,
278278
Mode = options.Mode,
279+
AgentMode = options.AgentMode,
279280
Traceparent = traceparent,
280281
Tracestate = tracestate,
281282
RequestHeaders = options.RequestHeaders,
@@ -1665,6 +1666,8 @@ internal record SendMessageRequest
16651666
public string Prompt { get; init; } = string.Empty;
16661667
public IList<UserMessageAttachment>? Attachments { get; init; }
16671668
public string? Mode { get; init; }
1669+
[JsonPropertyName("agentMode")]
1670+
public AgentMode? AgentMode { get; init; }
16681671
public string? Traceparent { get; init; }
16691672
public string? Tracestate { get; init; }
16701673
public IDictionary<string, string>? RequestHeaders { get; init; }

dotnet/src/Types.cs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1711,6 +1711,29 @@ public enum SystemMessageMode
17111711
Customize
17121712
}
17131713

1714+
/// <summary>
1715+
/// The UI mode the agent is in for a given turn.
1716+
/// </summary>
1717+
/// <remarks>
1718+
/// Set on <see cref="MessageOptions.AgentMode"/> to send a message in a specific mode; defaults to the session's current mode.
1719+
/// </remarks>
1720+
[JsonConverter(typeof(JsonStringEnumConverter<AgentMode>))]
1721+
public enum AgentMode
1722+
{
1723+
/// <summary>The agent is responding interactively to the user.</summary>
1724+
[JsonStringEnumMemberName("interactive")]
1725+
Interactive,
1726+
/// <summary>The agent is preparing a plan before making changes.</summary>
1727+
[JsonStringEnumMemberName("plan")]
1728+
Plan,
1729+
/// <summary>The agent is working autonomously toward task completion.</summary>
1730+
[JsonStringEnumMemberName("autopilot")]
1731+
Autopilot,
1732+
/// <summary>The agent is in shell-focused UI mode.</summary>
1733+
[JsonStringEnumMemberName("shell")]
1734+
Shell
1735+
}
1736+
17141737
/// <summary>
17151738
/// Specifies the operation to perform on a system message section.
17161739
/// </summary>
@@ -2645,6 +2668,7 @@ private MessageOptions(MessageOptions? other)
26452668

26462669
Attachments = other.Attachments is not null ? [.. other.Attachments] : null;
26472670
Mode = other.Mode;
2671+
AgentMode = other.AgentMode;
26482672
Prompt = other.Prompt;
26492673
RequestHeaders = other.RequestHeaders is not null
26502674
? new Dictionary<string, string>(other.RequestHeaders)
@@ -2660,10 +2684,16 @@ private MessageOptions(MessageOptions? other)
26602684
/// </summary>
26612685
public IList<UserMessageAttachment>? Attachments { get; set; }
26622686
/// <summary>
2663-
/// Interaction mode for the message (e.g., "plan", "edit").
2687+
/// How to deliver the message. <c>"enqueue"</c> (default) appends to the message queue;
2688+
/// <c>"immediate"</c> interjects during an in-progress turn.
26642689
/// </summary>
26652690
public string? Mode { get; set; }
26662691
/// <summary>
2692+
/// The UI mode the agent was in when this message was sent (for example "plan", "autopilot").
2693+
/// Defaults to the session's current mode when unset.
2694+
/// </summary>
2695+
public AgentMode? AgentMode { get; set; }
2696+
/// <summary>
26672697
/// Custom per-turn HTTP headers for outbound model requests.
26682698
/// </summary>
26692699
public IDictionary<string, string>? RequestHeaders { get; set; }

dotnet/test/E2E/ModeHandlersE2ETests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public async Task Should_Invoke_Exit_Plan_Mode_Handler_When_Model_Uses_Tool()
5353

5454
var response = await session.SendAndWaitAsync(new MessageOptions
5555
{
56-
Mode = "plan",
56+
AgentMode = AgentMode.Plan,
5757
Prompt = "Create a brief implementation plan for adding a greeting.txt file, then request approval with exit_plan_mode.",
5858
}, timeout: TimeSpan.FromSeconds(120));
5959

dotnet/test/E2E/SessionE2ETests.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -851,20 +851,19 @@ await session.SendAndWaitAsync(new MessageOptions
851851
}
852852

853853
[Fact]
854-
public async Task Should_Send_With_Mode_Property()
854+
public async Task Should_Send_With_AgentMode_Property()
855855
{
856856
var session = await CreateSessionAsync();
857857

858858
await session.SendAndWaitAsync(new MessageOptions
859859
{
860860
Prompt = "Say mode ok.",
861-
Mode = "plan",
861+
AgentMode = AgentMode.Plan,
862862
});
863863

864864
var userMessage = (await session.GetEventsAsync()).OfType<UserMessageEvent>().Last();
865865
Assert.Equal("Say mode ok.", userMessage.Data.Content);
866-
// The current runtime accepts the per-message mode option but does not echo it on user.message.
867-
Assert.Null(userMessage.Data.AgentMode);
866+
Assert.Equal(UserMessageAgentMode.Plan, userMessage.Data.AgentMode);
868867
}
869868

870869
[Fact]

dotnet/test/Unit/SerializationTests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,21 +57,21 @@ public void MessageOptions_CanSerializeRequestHeaders_WithSdkOptions()
5757
var original = new MessageOptions
5858
{
5959
Prompt = "real prompt",
60-
Mode = "plan",
60+
Mode = "enqueue",
6161
RequestHeaders = new Dictionary<string, string> { ["X-Trace"] = "trace-value" }
6262
};
6363

6464
var json = JsonSerializer.Serialize(original, options);
6565
using var document = JsonDocument.Parse(json);
6666
var root = document.RootElement;
6767
Assert.Equal("real prompt", root.GetProperty("prompt").GetString());
68-
Assert.Equal("plan", root.GetProperty("mode").GetString());
68+
Assert.Equal("enqueue", root.GetProperty("mode").GetString());
6969
Assert.Equal("trace-value", root.GetProperty("requestHeaders").GetProperty("X-Trace").GetString());
7070

7171
var deserialized = JsonSerializer.Deserialize<MessageOptions>(json, options);
7272
Assert.NotNull(deserialized);
7373
Assert.Equal("real prompt", deserialized.Prompt);
74-
Assert.Equal("plan", deserialized.Mode);
74+
Assert.Equal("enqueue", deserialized.Mode);
7575
Assert.Equal("trace-value", deserialized.RequestHeaders!["X-Trace"]);
7676
}
7777

@@ -84,15 +84,15 @@ public void SendMessageRequest_CanSerializeRequestHeaders_WithSdkOptions()
8484
requestType,
8585
("SessionId", "session-id"),
8686
("Prompt", "real prompt"),
87-
("Mode", "plan"),
87+
("Mode", "enqueue"),
8888
("RequestHeaders", new Dictionary<string, string> { ["X-Trace"] = "trace-value" }));
8989

9090
var json = JsonSerializer.Serialize(request, requestType, options);
9191
using var document = JsonDocument.Parse(json);
9292
var root = document.RootElement;
9393
Assert.Equal("session-id", root.GetProperty("sessionId").GetString());
9494
Assert.Equal("real prompt", root.GetProperty("prompt").GetString());
95-
Assert.Equal("plan", root.GetProperty("mode").GetString());
95+
Assert.Equal("enqueue", root.GetProperty("mode").GetString());
9696
Assert.Equal("trace-value", root.GetProperty("requestHeaders").GetProperty("X-Trace").GetString());
9797
}
9898

go/internal/e2e/mode_handlers_e2e_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ func TestModeHandlersE2E(t *testing.T) {
8484
)
8585

8686
response, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
87-
Prompt: planPrompt,
88-
Mode: "plan",
87+
Prompt: planPrompt,
88+
AgentMode: copilot.AgentModePlan,
8989
})
9090
if err != nil {
9191
t.Fatalf("Failed to send message: %v", err)

go/internal/e2e/session_e2e_test.go

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,7 +1499,7 @@ func TestSessionMessageOptionsE2E(t *testing.T) {
14991499
t.Fatalf("Failed to start client: %v", err)
15001500
}
15011501

1502-
t.Run("should send with mode property", func(t *testing.T) {
1502+
t.Run("should send with agentMode property", func(t *testing.T) {
15031503
ctx.ConfigureForTest(t)
15041504

15051505
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
@@ -1510,8 +1510,8 @@ func TestSessionMessageOptionsE2E(t *testing.T) {
15101510
}
15111511

15121512
_, err = session.SendAndWait(t.Context(), copilot.MessageOptions{
1513-
Prompt: "Say mode ok.",
1514-
Mode: "plan",
1513+
Prompt: "Say mode ok.",
1514+
AgentMode: copilot.AgentModePlan,
15151515
})
15161516
if err != nil {
15171517
t.Fatalf("SendAndWait failed: %v", err)
@@ -1535,10 +1535,8 @@ func TestSessionMessageOptionsE2E(t *testing.T) {
15351535
if userMsg.Content != "Say mode ok." {
15361536
t.Errorf("Expected Content 'Say mode ok.', got %q", userMsg.Content)
15371537
}
1538-
// The current runtime accepts the per-message mode option but does not
1539-
// echo it back on the user.message event.
1540-
if userMsg.AgentMode != nil {
1541-
t.Errorf("Expected AgentMode=nil, got %v", *userMsg.AgentMode)
1538+
if userMsg.AgentMode == nil || *userMsg.AgentMode != copilot.UserMessageAgentModePlan {
1539+
t.Errorf("Expected AgentMode=plan, got %v", userMsg.AgentMode)
15421540
}
15431541
})
15441542

go/session.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ func (s *Session) Send(ctx context.Context, options MessageOptions) (string, err
289289
Prompt: options.Prompt,
290290
Attachments: options.Attachments,
291291
Mode: options.Mode,
292+
AgentMode: options.AgentMode,
292293
Traceparent: traceparent,
293294
Tracestate: tracestate,
294295
RequestHeaders: options.RequestHeaders,

go/types.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1316,10 +1316,26 @@ type MessageOptions struct {
13161316
Attachments []Attachment
13171317
// Mode is the message delivery mode (default: "enqueue")
13181318
Mode string
1319+
// AgentMode is the UI mode the agent was in when this message was sent
1320+
// (for example "plan" or "autopilot"). Defaults to the session's current
1321+
// mode when empty.
1322+
AgentMode AgentMode
13191323
// RequestHeaders are custom per-turn HTTP headers for outbound model requests.
13201324
RequestHeaders map[string]string
13211325
}
13221326

1327+
// AgentMode is the UI mode the agent is in for a given turn. See
1328+
// [MessageOptions.AgentMode].
1329+
type AgentMode = rpc.SendAgentMode
1330+
1331+
// AgentMode values supported by the runtime.
1332+
const (
1333+
AgentModeInteractive = rpc.SendAgentModeInteractive
1334+
AgentModePlan = rpc.SendAgentModePlan
1335+
AgentModeAutopilot = rpc.SendAgentModeAutopilot
1336+
AgentModeShell = rpc.SendAgentModeShell
1337+
)
1338+
13231339
// SessionEventHandler is a callback for session events
13241340
type SessionEventHandler func(event SessionEvent)
13251341

@@ -1685,6 +1701,7 @@ type sessionSendRequest struct {
16851701
Prompt string `json:"prompt"`
16861702
Attachments []Attachment `json:"attachments,omitempty"`
16871703
Mode string `json:"mode,omitempty"`
1704+
AgentMode AgentMode `json:"agentMode,omitempty"`
16881705
Traceparent string `json:"traceparent,omitempty"`
16891706
Tracestate string `json:"tracestate,omitempty"`
16901707
RequestHeaders map[string]string `json:"requestHeaders,omitempty"`

java/src/main/java/com/github/copilot/sdk/CopilotSession.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -475,6 +475,7 @@ public CompletableFuture<String> send(MessageOptions options) {
475475
request.setPrompt(options.getPrompt());
476476
request.setAttachments(options.getAttachments());
477477
request.setMode(options.getMode());
478+
request.setAgentMode(options.getAgentMode());
478479
request.setRequestHeaders(options.getRequestHeaders());
479480

480481
return rpc.invoke("session.send", request, SendMessageResponse.class).thenApply(SendMessageResponse::messageId);

0 commit comments

Comments
 (0)