diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index ac010ed86..677aecfd2 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -149,7 +149,7 @@ public class ToolsListResult public List Tools { get; set; } = new(); } -internal class ListRequest +internal class ToolsListRequest { [JsonPropertyName("model")] public string? Model { get; set; } @@ -195,7 +195,7 @@ public class SessionModelGetCurrentResult public string? ModelId { get; set; } } -internal class GetCurrentRequest +internal class SessionModelGetCurrentRequest { [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -207,7 +207,7 @@ public class SessionModelSwitchToResult public string? ModelId { get; set; } } -internal class SwitchToRequest +internal class SessionModelSwitchToRequest { [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -223,7 +223,7 @@ public class SessionModeGetResult public SessionModeGetResultMode Mode { get; set; } } -internal class GetRequest +internal class SessionModeGetRequest { [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -236,7 +236,7 @@ public class SessionModeSetResult public SessionModeGetResultMode Mode { get; set; } } -internal class SetRequest +internal class SessionModeSetRequest { [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -256,7 +256,7 @@ public class SessionPlanReadResult public string? Content { get; set; } } -internal class ReadRequest +internal class SessionPlanReadRequest { [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -266,7 +266,7 @@ public class SessionPlanUpdateResult { } -internal class UpdateRequest +internal class SessionPlanUpdateRequest { [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -279,7 +279,7 @@ public class SessionPlanDeleteResult { } -internal class DeleteRequest +internal class SessionPlanDeleteRequest { [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -292,7 +292,7 @@ public class SessionWorkspaceListFilesResult public List Files { get; set; } = new(); } -internal class ListFilesRequest +internal class SessionWorkspaceListFilesRequest { [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -305,7 +305,7 @@ public class SessionWorkspaceReadFileResult public string Content { get; set; } = string.Empty; } -internal class ReadFileRequest +internal class SessionWorkspaceReadFileRequest { [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -318,7 +318,7 @@ public class SessionWorkspaceCreateFileResult { } -internal class CreateFileRequest +internal class SessionWorkspaceCreateFileRequest { [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -337,7 +337,7 @@ public class SessionFleetStartResult public bool Started { get; set; } } -internal class StartRequest +internal class SessionFleetStartRequest { [JsonPropertyName("sessionId")] public string SessionId { get; set; } = string.Empty; @@ -346,6 +346,125 @@ internal class StartRequest public string? Prompt { get; set; } } +public class Agent +{ + /// Unique identifier of the custom agent + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Human-readable display name + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// Description of the agent's purpose + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; +} + +public class SessionAgentListResult +{ + /// Available custom agents + [JsonPropertyName("agents")] + public List Agents { get; set; } = new(); +} + +internal class SessionAgentListRequest +{ + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +public class SessionAgentGetCurrentResultAgent +{ + /// Unique identifier of the custom agent + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Human-readable display name + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// Description of the agent's purpose + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; +} + +public class SessionAgentGetCurrentResult +{ + /// Currently selected custom agent, or null if using the default agent + [JsonPropertyName("agent")] + public SessionAgentGetCurrentResultAgent? Agent { get; set; } +} + +internal class SessionAgentGetCurrentRequest +{ + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +/// The newly selected custom agent +public class SessionAgentSelectResultAgent +{ + /// Unique identifier of the custom agent + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; + + /// Human-readable display name + [JsonPropertyName("displayName")] + public string DisplayName { get; set; } = string.Empty; + + /// Description of the agent's purpose + [JsonPropertyName("description")] + public string Description { get; set; } = string.Empty; +} + +public class SessionAgentSelectResult +{ + /// The newly selected custom agent + [JsonPropertyName("agent")] + public SessionAgentSelectResultAgent Agent { get; set; } = new(); +} + +internal class SessionAgentSelectRequest +{ + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; + + [JsonPropertyName("name")] + public string Name { get; set; } = string.Empty; +} + +public class SessionAgentDeselectResult +{ +} + +internal class SessionAgentDeselectRequest +{ + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + +public class SessionCompactionCompactResult +{ + /// Whether compaction completed successfully + [JsonPropertyName("success")] + public bool Success { get; set; } + + /// Number of tokens freed by compaction + [JsonPropertyName("tokensRemoved")] + public double TokensRemoved { get; set; } + + /// Number of messages removed during compaction + [JsonPropertyName("messagesRemoved")] + public double MessagesRemoved { get; set; } +} + +internal class SessionCompactionCompactRequest +{ + [JsonPropertyName("sessionId")] + public string SessionId { get; set; } = string.Empty; +} + [JsonConverter(typeof(JsonStringEnumConverter))] public enum SessionModeGetResultMode { @@ -418,7 +537,7 @@ internal ToolsApi(JsonRpc rpc) /// Calls "tools.list". public async Task ListAsync(string? model = null, CancellationToken cancellationToken = default) { - var request = new ListRequest { Model = model }; + var request = new ToolsListRequest { Model = model }; return await CopilotClient.InvokeRpcAsync(_rpc, "tools.list", [request], cancellationToken); } } @@ -455,6 +574,8 @@ internal SessionRpc(JsonRpc rpc, string sessionId) Plan = new PlanApi(rpc, sessionId); Workspace = new WorkspaceApi(rpc, sessionId); Fleet = new FleetApi(rpc, sessionId); + Agent = new AgentApi(rpc, sessionId); + Compaction = new CompactionApi(rpc, sessionId); } public ModelApi Model { get; } @@ -466,6 +587,10 @@ internal SessionRpc(JsonRpc rpc, string sessionId) public WorkspaceApi Workspace { get; } public FleetApi Fleet { get; } + + public AgentApi Agent { get; } + + public CompactionApi Compaction { get; } } public class ModelApi @@ -482,14 +607,14 @@ internal ModelApi(JsonRpc rpc, string sessionId) /// Calls "session.model.getCurrent". public async Task GetCurrentAsync(CancellationToken cancellationToken = default) { - var request = new GetCurrentRequest { SessionId = _sessionId }; + var request = new SessionModelGetCurrentRequest { SessionId = _sessionId }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.model.getCurrent", [request], cancellationToken); } /// Calls "session.model.switchTo". public async Task SwitchToAsync(string modelId, CancellationToken cancellationToken = default) { - var request = new SwitchToRequest { SessionId = _sessionId, ModelId = modelId }; + var request = new SessionModelSwitchToRequest { SessionId = _sessionId, ModelId = modelId }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.model.switchTo", [request], cancellationToken); } } @@ -508,14 +633,14 @@ internal ModeApi(JsonRpc rpc, string sessionId) /// Calls "session.mode.get". public async Task GetAsync(CancellationToken cancellationToken = default) { - var request = new GetRequest { SessionId = _sessionId }; + var request = new SessionModeGetRequest { SessionId = _sessionId }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.mode.get", [request], cancellationToken); } /// Calls "session.mode.set". public async Task SetAsync(SessionModeGetResultMode mode, CancellationToken cancellationToken = default) { - var request = new SetRequest { SessionId = _sessionId, Mode = mode }; + var request = new SessionModeSetRequest { SessionId = _sessionId, Mode = mode }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.mode.set", [request], cancellationToken); } } @@ -534,21 +659,21 @@ internal PlanApi(JsonRpc rpc, string sessionId) /// Calls "session.plan.read". public async Task ReadAsync(CancellationToken cancellationToken = default) { - var request = new ReadRequest { SessionId = _sessionId }; + var request = new SessionPlanReadRequest { SessionId = _sessionId }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.plan.read", [request], cancellationToken); } /// Calls "session.plan.update". public async Task UpdateAsync(string content, CancellationToken cancellationToken = default) { - var request = new UpdateRequest { SessionId = _sessionId, Content = content }; + var request = new SessionPlanUpdateRequest { SessionId = _sessionId, Content = content }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.plan.update", [request], cancellationToken); } /// Calls "session.plan.delete". public async Task DeleteAsync(CancellationToken cancellationToken = default) { - var request = new DeleteRequest { SessionId = _sessionId }; + var request = new SessionPlanDeleteRequest { SessionId = _sessionId }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.plan.delete", [request], cancellationToken); } } @@ -567,21 +692,21 @@ internal WorkspaceApi(JsonRpc rpc, string sessionId) /// Calls "session.workspace.listFiles". public async Task ListFilesAsync(CancellationToken cancellationToken = default) { - var request = new ListFilesRequest { SessionId = _sessionId }; + var request = new SessionWorkspaceListFilesRequest { SessionId = _sessionId }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.workspace.listFiles", [request], cancellationToken); } /// Calls "session.workspace.readFile". public async Task ReadFileAsync(string path, CancellationToken cancellationToken = default) { - var request = new ReadFileRequest { SessionId = _sessionId, Path = path }; + var request = new SessionWorkspaceReadFileRequest { SessionId = _sessionId, Path = path }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.workspace.readFile", [request], cancellationToken); } /// Calls "session.workspace.createFile". public async Task CreateFileAsync(string path, string content, CancellationToken cancellationToken = default) { - var request = new CreateFileRequest { SessionId = _sessionId, Path = path, Content = content }; + var request = new SessionWorkspaceCreateFileRequest { SessionId = _sessionId, Path = path, Content = content }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.workspace.createFile", [request], cancellationToken); } } @@ -600,23 +725,77 @@ internal FleetApi(JsonRpc rpc, string sessionId) /// Calls "session.fleet.start". public async Task StartAsync(string? prompt, CancellationToken cancellationToken = default) { - var request = new StartRequest { SessionId = _sessionId, Prompt = prompt }; + var request = new SessionFleetStartRequest { SessionId = _sessionId, Prompt = prompt }; return await CopilotClient.InvokeRpcAsync(_rpc, "session.fleet.start", [request], cancellationToken); } } +public class AgentApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal AgentApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.agent.list". + public async Task ListAsync(CancellationToken cancellationToken = default) + { + var request = new SessionAgentListRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.agent.list", [request], cancellationToken); + } + + /// Calls "session.agent.getCurrent". + public async Task GetCurrentAsync(CancellationToken cancellationToken = default) + { + var request = new SessionAgentGetCurrentRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.agent.getCurrent", [request], cancellationToken); + } + + /// Calls "session.agent.select". + public async Task SelectAsync(string name, CancellationToken cancellationToken = default) + { + var request = new SessionAgentSelectRequest { SessionId = _sessionId, Name = name }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.agent.select", [request], cancellationToken); + } + + /// Calls "session.agent.deselect". + public async Task DeselectAsync(CancellationToken cancellationToken = default) + { + var request = new SessionAgentDeselectRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.agent.deselect", [request], cancellationToken); + } +} + +public class CompactionApi +{ + private readonly JsonRpc _rpc; + private readonly string _sessionId; + + internal CompactionApi(JsonRpc rpc, string sessionId) + { + _rpc = rpc; + _sessionId = sessionId; + } + + /// Calls "session.compaction.compact". + public async Task CompactAsync(CancellationToken cancellationToken = default) + { + var request = new SessionCompactionCompactRequest { SessionId = _sessionId }; + return await CopilotClient.InvokeRpcAsync(_rpc, "session.compaction.compact", [request], cancellationToken); + } +} + [JsonSourceGenerationOptions( JsonSerializerDefaults.Web, AllowOutOfOrderMetadataProperties = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull)] [JsonSerializable(typeof(AccountGetQuotaResult))] [JsonSerializable(typeof(AccountGetQuotaResultQuotaSnapshotsValue))] -[JsonSerializable(typeof(CreateFileRequest))] -[JsonSerializable(typeof(DeleteRequest))] -[JsonSerializable(typeof(GetCurrentRequest))] -[JsonSerializable(typeof(GetRequest))] -[JsonSerializable(typeof(ListFilesRequest))] -[JsonSerializable(typeof(ListRequest))] +[JsonSerializable(typeof(Agent))] [JsonSerializable(typeof(Model))] [JsonSerializable(typeof(ModelBilling))] [JsonSerializable(typeof(ModelCapabilities))] @@ -626,23 +805,41 @@ public async Task StartAsync(string? prompt, Cancellati [JsonSerializable(typeof(ModelsListResult))] [JsonSerializable(typeof(PingRequest))] [JsonSerializable(typeof(PingResult))] -[JsonSerializable(typeof(ReadFileRequest))] -[JsonSerializable(typeof(ReadRequest))] +[JsonSerializable(typeof(SessionAgentDeselectRequest))] +[JsonSerializable(typeof(SessionAgentDeselectResult))] +[JsonSerializable(typeof(SessionAgentGetCurrentRequest))] +[JsonSerializable(typeof(SessionAgentGetCurrentResult))] +[JsonSerializable(typeof(SessionAgentGetCurrentResultAgent))] +[JsonSerializable(typeof(SessionAgentListRequest))] +[JsonSerializable(typeof(SessionAgentListResult))] +[JsonSerializable(typeof(SessionAgentSelectRequest))] +[JsonSerializable(typeof(SessionAgentSelectResult))] +[JsonSerializable(typeof(SessionAgentSelectResultAgent))] +[JsonSerializable(typeof(SessionCompactionCompactRequest))] +[JsonSerializable(typeof(SessionCompactionCompactResult))] +[JsonSerializable(typeof(SessionFleetStartRequest))] [JsonSerializable(typeof(SessionFleetStartResult))] +[JsonSerializable(typeof(SessionModeGetRequest))] [JsonSerializable(typeof(SessionModeGetResult))] +[JsonSerializable(typeof(SessionModeSetRequest))] [JsonSerializable(typeof(SessionModeSetResult))] +[JsonSerializable(typeof(SessionModelGetCurrentRequest))] [JsonSerializable(typeof(SessionModelGetCurrentResult))] +[JsonSerializable(typeof(SessionModelSwitchToRequest))] [JsonSerializable(typeof(SessionModelSwitchToResult))] +[JsonSerializable(typeof(SessionPlanDeleteRequest))] [JsonSerializable(typeof(SessionPlanDeleteResult))] +[JsonSerializable(typeof(SessionPlanReadRequest))] [JsonSerializable(typeof(SessionPlanReadResult))] +[JsonSerializable(typeof(SessionPlanUpdateRequest))] [JsonSerializable(typeof(SessionPlanUpdateResult))] +[JsonSerializable(typeof(SessionWorkspaceCreateFileRequest))] [JsonSerializable(typeof(SessionWorkspaceCreateFileResult))] +[JsonSerializable(typeof(SessionWorkspaceListFilesRequest))] [JsonSerializable(typeof(SessionWorkspaceListFilesResult))] +[JsonSerializable(typeof(SessionWorkspaceReadFileRequest))] [JsonSerializable(typeof(SessionWorkspaceReadFileResult))] -[JsonSerializable(typeof(SetRequest))] -[JsonSerializable(typeof(StartRequest))] -[JsonSerializable(typeof(SwitchToRequest))] [JsonSerializable(typeof(Tool))] +[JsonSerializable(typeof(ToolsListRequest))] [JsonSerializable(typeof(ToolsListResult))] -[JsonSerializable(typeof(UpdateRequest))] internal partial class RpcJsonContext : JsonSerializerContext; \ No newline at end of file diff --git a/dotnet/src/Generated/SessionEvents.cs b/dotnet/src/Generated/SessionEvents.cs index c2549803a..55e221e80 100644 --- a/dotnet/src/Generated/SessionEvents.cs +++ b/dotnet/src/Generated/SessionEvents.cs @@ -22,6 +22,7 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(AssistantMessageDeltaEvent), "assistant.message_delta")] [JsonDerivedType(typeof(AssistantReasoningEvent), "assistant.reasoning")] [JsonDerivedType(typeof(AssistantReasoningDeltaEvent), "assistant.reasoning_delta")] +[JsonDerivedType(typeof(AssistantStreamingDeltaEvent), "assistant.streaming_delta")] [JsonDerivedType(typeof(AssistantTurnEndEvent), "assistant.turn_end")] [JsonDerivedType(typeof(AssistantTurnStartEvent), "assistant.turn_start")] [JsonDerivedType(typeof(AssistantUsageEvent), "assistant.usage")] @@ -42,6 +43,7 @@ namespace GitHub.Copilot.SDK; [JsonDerivedType(typeof(SessionShutdownEvent), "session.shutdown")] [JsonDerivedType(typeof(SessionSnapshotRewindEvent), "session.snapshot_rewind")] [JsonDerivedType(typeof(SessionStartEvent), "session.start")] +[JsonDerivedType(typeof(SessionTaskCompleteEvent), "session.task_complete")] [JsonDerivedType(typeof(SessionTitleChangedEvent), "session.title_changed")] [JsonDerivedType(typeof(SessionTruncationEvent), "session.truncation")] [JsonDerivedType(typeof(SessionUsageInfoEvent), "session.usage_info")] @@ -315,6 +317,18 @@ public partial class SessionCompactionCompleteEvent : SessionEvent public required SessionCompactionCompleteData Data { get; set; } } +/// +/// Event: session.task_complete +/// +public partial class SessionTaskCompleteEvent : SessionEvent +{ + [JsonIgnore] + public override string Type => "session.task_complete"; + + [JsonPropertyName("data")] + public required SessionTaskCompleteData Data { get; set; } +} + /// /// Event: user.message /// @@ -387,6 +401,18 @@ public partial class AssistantReasoningDeltaEvent : SessionEvent public required AssistantReasoningDeltaData Data { get; set; } } +/// +/// Event: assistant.streaming_delta +/// +public partial class AssistantStreamingDeltaEvent : SessionEvent +{ + [JsonIgnore] + public override string Type => "assistant.streaming_delta"; + + [JsonPropertyName("data")] + public required AssistantStreamingDeltaData Data { get; set; } +} + /// /// Event: assistant.message /// @@ -899,6 +925,13 @@ public partial class SessionCompactionCompleteData public string? RequestId { get; set; } } +public partial class SessionTaskCompleteData +{ + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("summary")] + public string? Summary { get; set; } +} + public partial class UserMessageData { [JsonPropertyName("content")] @@ -955,6 +988,12 @@ public partial class AssistantReasoningDeltaData public required string DeltaContent { get; set; } } +public partial class AssistantStreamingDeltaData +{ + [JsonPropertyName("totalResponseSizeBytes")] + public required double TotalResponseSizeBytes { get; set; } +} + public partial class AssistantMessageData { [JsonPropertyName("messageId")] @@ -996,10 +1035,6 @@ public partial class AssistantMessageDeltaData [JsonPropertyName("deltaContent")] public required string DeltaContent { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - [JsonPropertyName("totalResponseSizeBytes")] - public double? TotalResponseSizeBytes { get; set; } - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] [JsonPropertyName("parentToolCallId")] public string? ParentToolCallId { get; set; } @@ -1736,6 +1771,8 @@ public enum SystemMessageDataRole [JsonSerializable(typeof(AssistantReasoningDeltaData))] [JsonSerializable(typeof(AssistantReasoningDeltaEvent))] [JsonSerializable(typeof(AssistantReasoningEvent))] +[JsonSerializable(typeof(AssistantStreamingDeltaData))] +[JsonSerializable(typeof(AssistantStreamingDeltaEvent))] [JsonSerializable(typeof(AssistantTurnEndData))] [JsonSerializable(typeof(AssistantTurnEndEvent))] [JsonSerializable(typeof(AssistantTurnStartData))] @@ -1783,6 +1820,8 @@ public enum SystemMessageDataRole [JsonSerializable(typeof(SessionStartData))] [JsonSerializable(typeof(SessionStartDataContext))] [JsonSerializable(typeof(SessionStartEvent))] +[JsonSerializable(typeof(SessionTaskCompleteData))] +[JsonSerializable(typeof(SessionTaskCompleteEvent))] [JsonSerializable(typeof(SessionTitleChangedData))] [JsonSerializable(typeof(SessionTitleChangedEvent))] [JsonSerializable(typeof(SessionTruncationData))] diff --git a/dotnet/test/AgentAndCompactRpcTests.cs b/dotnet/test/AgentAndCompactRpcTests.cs new file mode 100644 index 000000000..5f40d4e2b --- /dev/null +++ b/dotnet/test/AgentAndCompactRpcTests.cs @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +using GitHub.Copilot.SDK.Rpc; +using GitHub.Copilot.SDK.Test.Harness; +using Xunit; +using Xunit.Abstractions; + +namespace GitHub.Copilot.SDK.Test; + +public class AgentAndCompactRpcTests(E2ETestFixture fixture, ITestOutputHelper output) + : E2ETestBase(fixture, "agent_and_compact_rpc", output) +{ + [Fact] + public async Task Should_List_Available_Custom_Agents() + { + var customAgents = new List + { + new() + { + Name = "test-agent", + DisplayName = "Test Agent", + Description = "A test agent", + Prompt = "You are a test agent." + }, + new() + { + Name = "another-agent", + DisplayName = "Another Agent", + Description = "Another test agent", + Prompt = "You are another agent." + } + }; + + var session = await CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); + + var result = await session.Rpc.Agent.ListAsync(); + Assert.NotNull(result.Agents); + Assert.Equal(2, result.Agents.Count); + Assert.Equal("test-agent", result.Agents[0].Name); + Assert.Equal("Test Agent", result.Agents[0].DisplayName); + Assert.Equal("A test agent", result.Agents[0].Description); + Assert.Equal("another-agent", result.Agents[1].Name); + } + + [Fact] + public async Task Should_Return_Null_When_No_Agent_Is_Selected() + { + var customAgents = new List + { + new() + { + Name = "test-agent", + DisplayName = "Test Agent", + Description = "A test agent", + Prompt = "You are a test agent." + } + }; + + var session = await CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); + + var result = await session.Rpc.Agent.GetCurrentAsync(); + Assert.Null(result.Agent); + } + + [Fact] + public async Task Should_Select_And_Get_Current_Agent() + { + var customAgents = new List + { + new() + { + Name = "test-agent", + DisplayName = "Test Agent", + Description = "A test agent", + Prompt = "You are a test agent." + } + }; + + var session = await CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); + + // Select the agent + var selectResult = await session.Rpc.Agent.SelectAsync("test-agent"); + Assert.NotNull(selectResult.Agent); + Assert.Equal("test-agent", selectResult.Agent.Name); + Assert.Equal("Test Agent", selectResult.Agent.DisplayName); + + // Verify getCurrent returns the selected agent + var currentResult = await session.Rpc.Agent.GetCurrentAsync(); + Assert.NotNull(currentResult.Agent); + Assert.Equal("test-agent", currentResult.Agent.Name); + } + + [Fact] + public async Task Should_Deselect_Current_Agent() + { + var customAgents = new List + { + new() + { + Name = "test-agent", + DisplayName = "Test Agent", + Description = "A test agent", + Prompt = "You are a test agent." + } + }; + + var session = await CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); + + // Select then deselect + await session.Rpc.Agent.SelectAsync("test-agent"); + await session.Rpc.Agent.DeselectAsync(); + + // Verify no agent is selected + var currentResult = await session.Rpc.Agent.GetCurrentAsync(); + Assert.Null(currentResult.Agent); + } + + [Fact] + public async Task Should_Return_Empty_List_When_No_Custom_Agents_Configured() + { + var session = await CreateSessionAsync(); + + var result = await session.Rpc.Agent.ListAsync(); + Assert.Empty(result.Agents); + } + + [Fact] + public async Task Should_Compact_Session_History_After_Messages() + { + var session = await CreateSessionAsync(); + + // Send a message to create some history + await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 2+2?" }); + + // Compact the session + var result = await session.Rpc.Compaction.CompactAsync(); + Assert.NotNull(result); + } +} diff --git a/dotnet/test/CompactionTests.cs b/dotnet/test/CompactionTests.cs index 91551e550..c1cbc42df 100644 --- a/dotnet/test/CompactionTests.cs +++ b/dotnet/test/CompactionTests.cs @@ -45,7 +45,7 @@ public async Task Should_Trigger_Compaction_With_Low_Threshold_And_Emit_Events() // Send multiple messages to fill up the context window await session.SendAndWaitAsync(new MessageOptions { - Prompt = "Tell me a long story about a dragon. Be very detailed." + Prompt = "Tell me a story about a dragon. Be detailed." }); await session.SendAndWaitAsync(new MessageOptions { diff --git a/go/generated_session_events.go b/go/generated_session_events.go index c11a43c5a..92460fda6 100644 --- a/go/generated_session_events.go +++ b/go/generated_session_events.go @@ -108,6 +108,7 @@ type Data struct { Intent *string `json:"intent,omitempty"` ReasoningID *string `json:"reasoningId,omitempty"` DeltaContent *string `json:"deltaContent,omitempty"` + TotalResponseSizeBytes *float64 `json:"totalResponseSizeBytes,omitempty"` EncryptedContent *string `json:"encryptedContent,omitempty"` MessageID *string `json:"messageId,omitempty"` ParentToolCallID *string `json:"parentToolCallId,omitempty"` @@ -115,7 +116,6 @@ type Data struct { ReasoningOpaque *string `json:"reasoningOpaque,omitempty"` ReasoningText *string `json:"reasoningText,omitempty"` ToolRequests []ToolRequest `json:"toolRequests,omitempty"` - TotalResponseSizeBytes *float64 `json:"totalResponseSizeBytes,omitempty"` APICallID *string `json:"apiCallId,omitempty"` CacheReadTokens *float64 `json:"cacheReadTokens,omitempty"` CacheWriteTokens *float64 `json:"cacheWriteTokens,omitempty"` @@ -368,6 +368,7 @@ const ( AssistantMessageDelta SessionEventType = "assistant.message_delta" AssistantReasoning SessionEventType = "assistant.reasoning" AssistantReasoningDelta SessionEventType = "assistant.reasoning_delta" + AssistantStreamingDelta SessionEventType = "assistant.streaming_delta" AssistantTurnEnd SessionEventType = "assistant.turn_end" AssistantTurnStart SessionEventType = "assistant.turn_start" AssistantUsage SessionEventType = "assistant.usage" @@ -388,6 +389,7 @@ const ( SessionShutdown SessionEventType = "session.shutdown" SessionSnapshotRewind SessionEventType = "session.snapshot_rewind" SessionStart SessionEventType = "session.start" + SessionTaskComplete SessionEventType = "session.task_complete" SessionTitleChanged SessionEventType = "session.title_changed" SessionTruncation SessionEventType = "session.truncation" SessionUsageInfo SessionEventType = "session.usage_info" diff --git a/go/internal/e2e/agent_and_compact_rpc_test.go b/go/internal/e2e/agent_and_compact_rpc_test.go new file mode 100644 index 000000000..338f4da67 --- /dev/null +++ b/go/internal/e2e/agent_and_compact_rpc_test.go @@ -0,0 +1,289 @@ +package e2e + +import ( + "testing" + + copilot "github.com/github/copilot-sdk/go" + "github.com/github/copilot-sdk/go/internal/e2e/testharness" + "github.com/github/copilot-sdk/go/rpc" +) + +func TestAgentSelectionRpc(t *testing.T) { + cliPath := testharness.CLIPath() + if cliPath == "" { + t.Fatal("CLI not found. Run 'npm install' in the nodejs directory first.") + } + + t.Run("should list available custom agents", func(t *testing.T) { + client := copilot.NewClient(&copilot.ClientOptions{ + CLIPath: cliPath, + UseStdio: copilot.Bool(true), + }) + t.Cleanup(func() { client.ForceStop() }) + + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + CustomAgents: []copilot.CustomAgentConfig{ + { + Name: "test-agent", + DisplayName: "Test Agent", + Description: "A test agent", + Prompt: "You are a test agent.", + }, + { + Name: "another-agent", + DisplayName: "Another Agent", + Description: "Another test agent", + Prompt: "You are another agent.", + }, + }, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + result, err := session.RPC.Agent.List(t.Context()) + if err != nil { + t.Fatalf("Failed to list agents: %v", err) + } + + if len(result.Agents) != 2 { + t.Fatalf("Expected 2 agents, got %d", len(result.Agents)) + } + if result.Agents[0].Name != "test-agent" { + t.Errorf("Expected first agent name 'test-agent', got %q", result.Agents[0].Name) + } + if result.Agents[0].DisplayName != "Test Agent" { + t.Errorf("Expected first agent displayName 'Test Agent', got %q", result.Agents[0].DisplayName) + } + if result.Agents[1].Name != "another-agent" { + t.Errorf("Expected second agent name 'another-agent', got %q", result.Agents[1].Name) + } + + if err := client.Stop(); err != nil { + t.Errorf("Expected no errors on stop, got %v", err) + } + }) + + t.Run("should return null when no agent is selected", func(t *testing.T) { + client := copilot.NewClient(&copilot.ClientOptions{ + CLIPath: cliPath, + UseStdio: copilot.Bool(true), + }) + t.Cleanup(func() { client.ForceStop() }) + + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + CustomAgents: []copilot.CustomAgentConfig{ + { + Name: "test-agent", + DisplayName: "Test Agent", + Description: "A test agent", + Prompt: "You are a test agent.", + }, + }, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + result, err := session.RPC.Agent.GetCurrent(t.Context()) + if err != nil { + t.Fatalf("Failed to get current agent: %v", err) + } + + if result.Agent != nil { + t.Errorf("Expected no agent selected, got %v", result.Agent) + } + + if err := client.Stop(); err != nil { + t.Errorf("Expected no errors on stop, got %v", err) + } + }) + + t.Run("should select and get current agent", func(t *testing.T) { + client := copilot.NewClient(&copilot.ClientOptions{ + CLIPath: cliPath, + UseStdio: copilot.Bool(true), + }) + t.Cleanup(func() { client.ForceStop() }) + + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + CustomAgents: []copilot.CustomAgentConfig{ + { + Name: "test-agent", + DisplayName: "Test Agent", + Description: "A test agent", + Prompt: "You are a test agent.", + }, + }, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + // Select the agent + selectResult, err := session.RPC.Agent.Select(t.Context(), &rpc.SessionAgentSelectParams{Name: "test-agent"}) + if err != nil { + t.Fatalf("Failed to select agent: %v", err) + } + if selectResult.Agent.Name != "test-agent" { + t.Errorf("Expected selected agent 'test-agent', got %q", selectResult.Agent.Name) + } + if selectResult.Agent.DisplayName != "Test Agent" { + t.Errorf("Expected displayName 'Test Agent', got %q", selectResult.Agent.DisplayName) + } + + // Verify getCurrent returns the selected agent + currentResult, err := session.RPC.Agent.GetCurrent(t.Context()) + if err != nil { + t.Fatalf("Failed to get current agent: %v", err) + } + if currentResult.Agent == nil { + t.Fatal("Expected an agent to be selected") + } + if currentResult.Agent.Name != "test-agent" { + t.Errorf("Expected current agent 'test-agent', got %q", currentResult.Agent.Name) + } + + if err := client.Stop(); err != nil { + t.Errorf("Expected no errors on stop, got %v", err) + } + }) + + t.Run("should deselect current agent", func(t *testing.T) { + client := copilot.NewClient(&copilot.ClientOptions{ + CLIPath: cliPath, + UseStdio: copilot.Bool(true), + }) + t.Cleanup(func() { client.ForceStop() }) + + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + CustomAgents: []copilot.CustomAgentConfig{ + { + Name: "test-agent", + DisplayName: "Test Agent", + Description: "A test agent", + Prompt: "You are a test agent.", + }, + }, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + // Select then deselect + _, err = session.RPC.Agent.Select(t.Context(), &rpc.SessionAgentSelectParams{Name: "test-agent"}) + if err != nil { + t.Fatalf("Failed to select agent: %v", err) + } + + _, err = session.RPC.Agent.Deselect(t.Context()) + if err != nil { + t.Fatalf("Failed to deselect agent: %v", err) + } + + // Verify no agent is selected + currentResult, err := session.RPC.Agent.GetCurrent(t.Context()) + if err != nil { + t.Fatalf("Failed to get current agent: %v", err) + } + if currentResult.Agent != nil { + t.Errorf("Expected no agent selected after deselect, got %v", currentResult.Agent) + } + + if err := client.Stop(); err != nil { + t.Errorf("Expected no errors on stop, got %v", err) + } + }) + + t.Run("should return empty list when no custom agents configured", func(t *testing.T) { + client := copilot.NewClient(&copilot.ClientOptions{ + CLIPath: cliPath, + UseStdio: copilot.Bool(true), + }) + t.Cleanup(func() { client.ForceStop() }) + + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + result, err := session.RPC.Agent.List(t.Context()) + if err != nil { + t.Fatalf("Failed to list agents: %v", err) + } + + if len(result.Agents) != 0 { + t.Errorf("Expected empty agent list, got %d agents", len(result.Agents)) + } + + if err := client.Stop(); err != nil { + t.Errorf("Expected no errors on stop, got %v", err) + } + }) +} + +func TestSessionCompactionRpc(t *testing.T) { + ctx := testharness.NewTestContext(t) + client := ctx.NewClient() + t.Cleanup(func() { client.ForceStop() }) + + if err := client.Start(t.Context()); err != nil { + t.Fatalf("Failed to start client: %v", err) + } + + t.Run("should compact session history after messages", func(t *testing.T) { + ctx.ConfigureForTest(t) + + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) + if err != nil { + t.Fatalf("Failed to create session: %v", err) + } + + // Send a message to create some history + _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{ + Prompt: "What is 2+2?", + }) + if err != nil { + t.Fatalf("Failed to send message: %v", err) + } + + // Compact the session + result, err := session.RPC.Compaction.Compact(t.Context()) + if err != nil { + t.Fatalf("Failed to compact session: %v", err) + } + + // Verify result has expected fields (just check it returned valid data) + if result == nil { + t.Fatal("Expected non-nil compact result") + } + }) +} diff --git a/go/internal/e2e/compaction_test.go b/go/internal/e2e/compaction_test.go index 239e1e128..aee80704d 100644 --- a/go/internal/e2e/compaction_test.go +++ b/go/internal/e2e/compaction_test.go @@ -45,7 +45,7 @@ func TestCompaction(t *testing.T) { }) // Send multiple messages to fill up the context window - _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "Tell me a long story about a dragon. Be very detailed."}) + _, err = session.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "Tell me a story about a dragon. Be detailed."}) if err != nil { t.Fatalf("Failed to send first message: %v", err) } diff --git a/go/rpc/generated_rpc.go b/go/rpc/generated_rpc.go index c7d9b0c07..eaf409a85 100644 --- a/go/rpc/generated_rpc.go +++ b/go/rpc/generated_rpc.go @@ -200,6 +200,66 @@ type SessionFleetStartParams struct { Prompt *string `json:"prompt,omitempty"` } +type SessionAgentListResult struct { + // Available custom agents + Agents []AgentElement `json:"agents"` +} + +type AgentElement struct { + // Description of the agent's purpose + Description string `json:"description"` + // Human-readable display name + DisplayName string `json:"displayName"` + // Unique identifier of the custom agent + Name string `json:"name"` +} + +type SessionAgentGetCurrentResult struct { + // Currently selected custom agent, or null if using the default agent + Agent *SessionAgentGetCurrentResultAgent `json:"agent"` +} + +type SessionAgentGetCurrentResultAgent struct { + // Description of the agent's purpose + Description string `json:"description"` + // Human-readable display name + DisplayName string `json:"displayName"` + // Unique identifier of the custom agent + Name string `json:"name"` +} + +type SessionAgentSelectResult struct { + // The newly selected custom agent + Agent SessionAgentSelectResultAgent `json:"agent"` +} + +// The newly selected custom agent +type SessionAgentSelectResultAgent struct { + // Description of the agent's purpose + Description string `json:"description"` + // Human-readable display name + DisplayName string `json:"displayName"` + // Unique identifier of the custom agent + Name string `json:"name"` +} + +type SessionAgentSelectParams struct { + // Name of the custom agent to select + Name string `json:"name"` +} + +type SessionAgentDeselectResult struct { +} + +type SessionCompactionCompactResult struct { + // Number of messages removed during compaction + MessagesRemoved float64 `json:"messagesRemoved"` + // Whether compaction completed successfully + Success bool `json:"success"` + // Number of tokens freed by compaction + TokensRemoved float64 `json:"tokensRemoved"` +} + // The current agent mode. // // The agent mode after switching. @@ -472,23 +532,105 @@ func (a *FleetRpcApi) Start(ctx context.Context, params *SessionFleetStartParams return &result, nil } -// SessionRpc provides typed session-scoped RPC methods. -type SessionRpc struct { +type AgentRpcApi struct { client *jsonrpc2.Client sessionID string - Model *ModelRpcApi - Mode *ModeRpcApi - Plan *PlanRpcApi - Workspace *WorkspaceRpcApi - Fleet *FleetRpcApi +} + +func (a *AgentRpcApi) List(ctx context.Context) (*SessionAgentListResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + raw, err := a.client.Request("session.agent.list", req) + if err != nil { + return nil, err + } + var result SessionAgentListResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *AgentRpcApi) GetCurrent(ctx context.Context) (*SessionAgentGetCurrentResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + raw, err := a.client.Request("session.agent.getCurrent", req) + if err != nil { + return nil, err + } + var result SessionAgentGetCurrentResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *AgentRpcApi) Select(ctx context.Context, params *SessionAgentSelectParams) (*SessionAgentSelectResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + if params != nil { + req["name"] = params.Name + } + raw, err := a.client.Request("session.agent.select", req) + if err != nil { + return nil, err + } + var result SessionAgentSelectResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *AgentRpcApi) Deselect(ctx context.Context) (*SessionAgentDeselectResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + raw, err := a.client.Request("session.agent.deselect", req) + if err != nil { + return nil, err + } + var result SessionAgentDeselectResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +type CompactionRpcApi struct { + client *jsonrpc2.Client + sessionID string +} + +func (a *CompactionRpcApi) Compact(ctx context.Context) (*SessionCompactionCompactResult, error) { + req := map[string]interface{}{"sessionId": a.sessionID} + raw, err := a.client.Request("session.compaction.compact", req) + if err != nil { + return nil, err + } + var result SessionCompactionCompactResult + if err := json.Unmarshal(raw, &result); err != nil { + return nil, err + } + return &result, nil +} + +// SessionRpc provides typed session-scoped RPC methods. +type SessionRpc struct { + client *jsonrpc2.Client + sessionID string + Model *ModelRpcApi + Mode *ModeRpcApi + Plan *PlanRpcApi + Workspace *WorkspaceRpcApi + Fleet *FleetRpcApi + Agent *AgentRpcApi + Compaction *CompactionRpcApi } func NewSessionRpc(client *jsonrpc2.Client, sessionID string) *SessionRpc { return &SessionRpc{client: client, sessionID: sessionID, - Model: &ModelRpcApi{client: client, sessionID: sessionID}, - Mode: &ModeRpcApi{client: client, sessionID: sessionID}, - Plan: &PlanRpcApi{client: client, sessionID: sessionID}, - Workspace: &WorkspaceRpcApi{client: client, sessionID: sessionID}, - Fleet: &FleetRpcApi{client: client, sessionID: sessionID}, + Model: &ModelRpcApi{client: client, sessionID: sessionID}, + Mode: &ModeRpcApi{client: client, sessionID: sessionID}, + Plan: &PlanRpcApi{client: client, sessionID: sessionID}, + Workspace: &WorkspaceRpcApi{client: client, sessionID: sessionID}, + Fleet: &FleetRpcApi{client: client, sessionID: sessionID}, + Agent: &AgentRpcApi{client: client, sessionID: sessionID}, + Compaction: &CompactionRpcApi{client: client, sessionID: sessionID}, } } diff --git a/nodejs/package-lock.json b/nodejs/package-lock.json index 91fc00d14..2123030cd 100644 --- a/nodejs/package-lock.json +++ b/nodejs/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.8", "license": "MIT", "dependencies": { - "@github/copilot": "^0.0.414", + "@github/copilot": "^0.0.416", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, @@ -662,26 +662,26 @@ } }, "node_modules/@github/copilot": { - "version": "0.0.414", - "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.414.tgz", - "integrity": "sha512-jseJ2S02CLWrFks5QK22zzq7as2ErY5m1wMCFBOE6sro1uACq1kvqqM1LwM4qy58YSZFrM1ZAn1s7UOVd9zhIA==", + "version": "0.0.416", + "resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.416.tgz", + "integrity": "sha512-rohH9dEeLRkYD31BUL7RFnGz2ZU/7CVkNdZlWEut+CSXgha6uhb3GPmIAPty1gZyUeMeHqJOwu+SSEjAxVaXEg==", "license": "SEE LICENSE IN LICENSE.md", "bin": { "copilot": "npm-loader.js" }, "optionalDependencies": { - "@github/copilot-darwin-arm64": "0.0.414", - "@github/copilot-darwin-x64": "0.0.414", - "@github/copilot-linux-arm64": "0.0.414", - "@github/copilot-linux-x64": "0.0.414", - "@github/copilot-win32-arm64": "0.0.414", - "@github/copilot-win32-x64": "0.0.414" + "@github/copilot-darwin-arm64": "0.0.416", + "@github/copilot-darwin-x64": "0.0.416", + "@github/copilot-linux-arm64": "0.0.416", + "@github/copilot-linux-x64": "0.0.416", + "@github/copilot-win32-arm64": "0.0.416", + "@github/copilot-win32-x64": "0.0.416" } }, "node_modules/@github/copilot-darwin-arm64": { - "version": "0.0.414", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.414.tgz", - "integrity": "sha512-PW4v89v41i4Mg/NYl4+gEhwnDaVz+olNL+RbqtiQI3IV89gZdS+RZQbUEJfOwMaFcT2GfiUK1OuB+YDv5GrkBg==", + "version": "0.0.416", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.416.tgz", + "integrity": "sha512-mipNAS3Pi8Eu2TA9rTHdjEMaDZNNKJl29rL5cvs9imjshsQJBwiJD3zIV/XQvqsFB8IWngn5ZFT9ybML4milkg==", "cpu": [ "arm64" ], @@ -695,9 +695,9 @@ } }, "node_modules/@github/copilot-darwin-x64": { - "version": "0.0.414", - "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.414.tgz", - "integrity": "sha512-NyPYm0NovQTwtuI42WJIi4cjYd2z0wBHEvWlUSczRsSuYEyImAClmZmBPegUU63e5JdZd1PdQkQ7FqrrfL2fZQ==", + "version": "0.0.416", + "resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.416.tgz", + "integrity": "sha512-22Q1rkpYflX5XMfmfvA/uZ/9ME43rUARy6rQZ+tfl2/Xg2FFR8NXyvhtatqVZ5v+N2DE81IrYUG3V0wfX0U8Iw==", "cpu": [ "x64" ], @@ -711,9 +711,9 @@ } }, "node_modules/@github/copilot-linux-arm64": { - "version": "0.0.414", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.414.tgz", - "integrity": "sha512-VgdRsvA1FiZ1lcU/AscSvyJWEUWZzoXv2tSZ6WV3NE0uUTuO1Qoq4xuqbKZ/+vKJmn1b8afe7sxAAOtCoWPBHQ==", + "version": "0.0.416", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.416.tgz", + "integrity": "sha512-iLFzBpjSNQOqOvBIq4AxlSWDGlAW5+Ri0Qz2h1CVBb5JcUOdfSL22Fz/3wWzrV1quvnkVyJQ45tL9zS117DhcA==", "cpu": [ "arm64" ], @@ -727,9 +727,9 @@ } }, "node_modules/@github/copilot-linux-x64": { - "version": "0.0.414", - "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.414.tgz", - "integrity": "sha512-3HyZsbZqYTF5jcT7/e+nDIYBCQXo8UCVWjBI3raOE4lzAw9b2ucL290IhtA23s1+EiquMxJ4m3FnjwFmwlQ12A==", + "version": "0.0.416", + "resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.416.tgz", + "integrity": "sha512-+dSZOLaCr+A/WJLE++4lCC13PziIO64PzFGJsEmSedQkqw2dnXrAYbsp+cbtXJJ6w0DZpHigeifhn1m3sAWUvw==", "cpu": [ "x64" ], @@ -743,9 +743,9 @@ } }, "node_modules/@github/copilot-win32-arm64": { - "version": "0.0.414", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.414.tgz", - "integrity": "sha512-8gdaoF4MPpeV0h8UnCZ8TKI5l274EP0fvAaV9BGjsdyEydDcEb+DHqQiXgitWVKKiHAAaPi12aH8P5OsEDUneQ==", + "version": "0.0.416", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.416.tgz", + "integrity": "sha512-jeTroTL5CeAysbl3GJeLyPMsb2fEP16Iqo7faeozlbyU9BD5GjaLKtEApmFCE2hP0/CNcJVg1K5cZLTaKYOXCw==", "cpu": [ "arm64" ], @@ -759,9 +759,9 @@ } }, "node_modules/@github/copilot-win32-x64": { - "version": "0.0.414", - "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.414.tgz", - "integrity": "sha512-E1Oq1jXHaL1oWNsmmiCd4G30/CfgVdswg/T5oDFUxx3Ri+6uBekciIzdyCDelsP1kn2+fC1EYz2AerQ6F+huzg==", + "version": "0.0.416", + "resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.416.tgz", + "integrity": "sha512-62kv5dMNZ+7unVi3puK16trlOItLldFgQEkk2mVqAsRzEA9XqxArNeN+lG6kAIHNbiKA+OKVafdkzy104kHy4w==", "cpu": [ "x64" ], diff --git a/nodejs/package.json b/nodejs/package.json index 34ca78f2a..2ba3b9a3d 100644 --- a/nodejs/package.json +++ b/nodejs/package.json @@ -40,7 +40,7 @@ "author": "GitHub", "license": "MIT", "dependencies": { - "@github/copilot": "^0.0.414", + "@github/copilot": "^0.0.416", "vscode-jsonrpc": "^8.2.1", "zod": "^4.3.6" }, diff --git a/nodejs/src/generated/rpc.ts b/nodejs/src/generated/rpc.ts index 12c992bd6..861292620 100644 --- a/nodejs/src/generated/rpc.ts +++ b/nodejs/src/generated/rpc.ts @@ -314,6 +314,122 @@ export interface SessionFleetStartParams { prompt?: string; } +export interface SessionAgentListResult { + /** + * Available custom agents + */ + agents: { + /** + * Unique identifier of the custom agent + */ + name: string; + /** + * Human-readable display name + */ + displayName: string; + /** + * Description of the agent's purpose + */ + description: string; + }[]; +} + +export interface SessionAgentListParams { + /** + * Target session identifier + */ + sessionId: string; +} + +export interface SessionAgentGetCurrentResult { + /** + * Currently selected custom agent, or null if using the default agent + */ + agent: { + /** + * Unique identifier of the custom agent + */ + name: string; + /** + * Human-readable display name + */ + displayName: string; + /** + * Description of the agent's purpose + */ + description: string; + } | null; +} + +export interface SessionAgentGetCurrentParams { + /** + * Target session identifier + */ + sessionId: string; +} + +export interface SessionAgentSelectResult { + /** + * The newly selected custom agent + */ + agent: { + /** + * Unique identifier of the custom agent + */ + name: string; + /** + * Human-readable display name + */ + displayName: string; + /** + * Description of the agent's purpose + */ + description: string; + }; +} + +export interface SessionAgentSelectParams { + /** + * Target session identifier + */ + sessionId: string; + /** + * Name of the custom agent to select + */ + name: string; +} + +export interface SessionAgentDeselectResult {} + +export interface SessionAgentDeselectParams { + /** + * Target session identifier + */ + sessionId: string; +} + +export interface SessionCompactionCompactResult { + /** + * Whether compaction completed successfully + */ + success: boolean; + /** + * Number of tokens freed by compaction + */ + tokensRemoved: number; + /** + * Number of messages removed during compaction + */ + messagesRemoved: number; +} + +export interface SessionCompactionCompactParams { + /** + * Target session identifier + */ + sessionId: string; +} + /** Create typed server-scoped RPC methods (no session required). */ export function createServerRpc(connection: MessageConnection) { return { @@ -369,5 +485,19 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin start: async (params: Omit): Promise => connection.sendRequest("session.fleet.start", { sessionId, ...params }), }, + agent: { + list: async (): Promise => + connection.sendRequest("session.agent.list", { sessionId }), + getCurrent: async (): Promise => + connection.sendRequest("session.agent.getCurrent", { sessionId }), + select: async (params: Omit): Promise => + connection.sendRequest("session.agent.select", { sessionId, ...params }), + deselect: async (): Promise => + connection.sendRequest("session.agent.deselect", { sessionId }), + }, + compaction: { + compact: async (): Promise => + connection.sendRequest("session.compaction.compact", { sessionId }), + }, }; } diff --git a/nodejs/src/generated/session-events.ts b/nodejs/src/generated/session-events.ts index 032a1723d..5b6c90aaf 100644 --- a/nodejs/src/generated/session-events.ts +++ b/nodejs/src/generated/session-events.ts @@ -281,6 +281,16 @@ export type SessionEvent = requestId?: string; }; } + | { + id: string; + timestamp: string; + parentId: string | null; + ephemeral?: boolean; + type: "session.task_complete"; + data: { + summary?: string; + }; + } | { id: string; timestamp: string; @@ -380,6 +390,16 @@ export type SessionEvent = deltaContent: string; }; } + | { + id: string; + timestamp: string; + parentId: string | null; + ephemeral: true; + type: "assistant.streaming_delta"; + data: { + totalResponseSizeBytes: number; + }; + } | { id: string; timestamp: string; @@ -411,7 +431,6 @@ export type SessionEvent = data: { messageId: string; deltaContent: string; - totalResponseSizeBytes?: number; parentToolCallId?: string; }; } diff --git a/nodejs/test/e2e/agent_and_compact_rpc.test.ts b/nodejs/test/e2e/agent_and_compact_rpc.test.ts new file mode 100644 index 000000000..47fc83229 --- /dev/null +++ b/nodejs/test/e2e/agent_and_compact_rpc.test.ts @@ -0,0 +1,149 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +import { approveAll } from "../../src/index.js"; +import type { CustomAgentConfig } from "../../src/index.js"; +import { createSdkTestContext } from "./harness/sdkTestContext.js"; + +describe("Agent Selection RPC", async () => { + const { copilotClient: client } = await createSdkTestContext(); + + it("should list available custom agents", async () => { + const customAgents: CustomAgentConfig[] = [ + { + name: "test-agent", + displayName: "Test Agent", + description: "A test agent", + prompt: "You are a test agent.", + }, + { + name: "another-agent", + displayName: "Another Agent", + description: "Another test agent", + prompt: "You are another agent.", + }, + ]; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + customAgents, + }); + + const result = await session.rpc.agent.list(); + expect(result.agents).toBeDefined(); + expect(Array.isArray(result.agents)).toBe(true); + expect(result.agents.length).toBe(2); + expect(result.agents[0].name).toBe("test-agent"); + expect(result.agents[0].displayName).toBe("Test Agent"); + expect(result.agents[0].description).toBe("A test agent"); + expect(result.agents[1].name).toBe("another-agent"); + + await session.destroy(); + }); + + it("should return null when no agent is selected", async () => { + const customAgents: CustomAgentConfig[] = [ + { + name: "test-agent", + displayName: "Test Agent", + description: "A test agent", + prompt: "You are a test agent.", + }, + ]; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + customAgents, + }); + + const result = await session.rpc.agent.getCurrent(); + expect(result.agent).toBeNull(); + + await session.destroy(); + }); + + it("should select and get current agent", async () => { + const customAgents: CustomAgentConfig[] = [ + { + name: "test-agent", + displayName: "Test Agent", + description: "A test agent", + prompt: "You are a test agent.", + }, + ]; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + customAgents, + }); + + // Select the agent + const selectResult = await session.rpc.agent.select({ name: "test-agent" }); + expect(selectResult.agent).toBeDefined(); + expect(selectResult.agent.name).toBe("test-agent"); + expect(selectResult.agent.displayName).toBe("Test Agent"); + + // Verify getCurrent returns the selected agent + const currentResult = await session.rpc.agent.getCurrent(); + expect(currentResult.agent).not.toBeNull(); + expect(currentResult.agent!.name).toBe("test-agent"); + + await session.destroy(); + }); + + it("should deselect current agent", async () => { + const customAgents: CustomAgentConfig[] = [ + { + name: "test-agent", + displayName: "Test Agent", + description: "A test agent", + prompt: "You are a test agent.", + }, + ]; + + const session = await client.createSession({ + onPermissionRequest: approveAll, + customAgents, + }); + + // Select then deselect + await session.rpc.agent.select({ name: "test-agent" }); + await session.rpc.agent.deselect(); + + // Verify no agent is selected + const currentResult = await session.rpc.agent.getCurrent(); + expect(currentResult.agent).toBeNull(); + + await session.destroy(); + }); + + it("should return empty list when no custom agents configured", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + + const result = await session.rpc.agent.list(); + expect(result.agents).toEqual([]); + + await session.destroy(); + }); +}); + +describe("Session Compact RPC", async () => { + const { copilotClient: client } = await createSdkTestContext(); + + it("should compact session history after messages", async () => { + const session = await client.createSession({ onPermissionRequest: approveAll }); + + // Send a message to create some history + await session.sendAndWait({ prompt: "What is 2+2?" }); + + // Compact the session + const result = await session.rpc.compaction.compact(); + expect(typeof result.success).toBe("boolean"); + expect(typeof result.tokensRemoved).toBe("number"); + expect(typeof result.messagesRemoved).toBe("number"); + + await session.destroy(); + }, 60000); +}); diff --git a/nodejs/test/e2e/compaction.test.ts b/nodejs/test/e2e/compaction.test.ts index e9ea287d3..13e836644 100644 --- a/nodejs/test/e2e/compaction.test.ts +++ b/nodejs/test/e2e/compaction.test.ts @@ -26,7 +26,7 @@ describe("Compaction", async () => { // Send multiple messages to fill up the context window // With such low thresholds, even a few messages should trigger compaction await session.sendAndWait({ - prompt: "Tell me a long story about a dragon. Be very detailed.", + prompt: "Tell me a story about a dragon. Be detailed.", }); await session.sendAndWait({ prompt: "Continue the story with more details about the dragon's castle.", diff --git a/python/copilot/generated/rpc.py b/python/copilot/generated/rpc.py index 3b87bea55..27a2bca2c 100644 --- a/python/copilot/generated/rpc.py +++ b/python/copilot/generated/rpc.py @@ -724,6 +724,196 @@ def to_dict(self) -> dict: return result +@dataclass +class AgentElement: + description: str + """Description of the agent's purpose""" + + display_name: str + """Human-readable display name""" + + name: str + """Unique identifier of the custom agent""" + + @staticmethod + def from_dict(obj: Any) -> 'AgentElement': + assert isinstance(obj, dict) + description = from_str(obj.get("description")) + display_name = from_str(obj.get("displayName")) + name = from_str(obj.get("name")) + return AgentElement(description, display_name, name) + + def to_dict(self) -> dict: + result: dict = {} + result["description"] = from_str(self.description) + result["displayName"] = from_str(self.display_name) + result["name"] = from_str(self.name) + return result + + +@dataclass +class SessionAgentListResult: + agents: List[AgentElement] + """Available custom agents""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionAgentListResult': + assert isinstance(obj, dict) + agents = from_list(AgentElement.from_dict, obj.get("agents")) + return SessionAgentListResult(agents) + + def to_dict(self) -> dict: + result: dict = {} + result["agents"] = from_list(lambda x: to_class(AgentElement, x), self.agents) + return result + + +@dataclass +class SessionAgentGetCurrentResultAgent: + description: str + """Description of the agent's purpose""" + + display_name: str + """Human-readable display name""" + + name: str + """Unique identifier of the custom agent""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionAgentGetCurrentResultAgent': + assert isinstance(obj, dict) + description = from_str(obj.get("description")) + display_name = from_str(obj.get("displayName")) + name = from_str(obj.get("name")) + return SessionAgentGetCurrentResultAgent(description, display_name, name) + + def to_dict(self) -> dict: + result: dict = {} + result["description"] = from_str(self.description) + result["displayName"] = from_str(self.display_name) + result["name"] = from_str(self.name) + return result + + +@dataclass +class SessionAgentGetCurrentResult: + agent: Optional[SessionAgentGetCurrentResultAgent] = None + """Currently selected custom agent, or null if using the default agent""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionAgentGetCurrentResult': + assert isinstance(obj, dict) + agent = from_union([SessionAgentGetCurrentResultAgent.from_dict, from_none], obj.get("agent")) + return SessionAgentGetCurrentResult(agent) + + def to_dict(self) -> dict: + result: dict = {} + result["agent"] = from_union([lambda x: to_class(SessionAgentGetCurrentResultAgent, x), from_none], self.agent) + return result + + +@dataclass +class SessionAgentSelectResultAgent: + """The newly selected custom agent""" + + description: str + """Description of the agent's purpose""" + + display_name: str + """Human-readable display name""" + + name: str + """Unique identifier of the custom agent""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionAgentSelectResultAgent': + assert isinstance(obj, dict) + description = from_str(obj.get("description")) + display_name = from_str(obj.get("displayName")) + name = from_str(obj.get("name")) + return SessionAgentSelectResultAgent(description, display_name, name) + + def to_dict(self) -> dict: + result: dict = {} + result["description"] = from_str(self.description) + result["displayName"] = from_str(self.display_name) + result["name"] = from_str(self.name) + return result + + +@dataclass +class SessionAgentSelectResult: + agent: SessionAgentSelectResultAgent + """The newly selected custom agent""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionAgentSelectResult': + assert isinstance(obj, dict) + agent = SessionAgentSelectResultAgent.from_dict(obj.get("agent")) + return SessionAgentSelectResult(agent) + + def to_dict(self) -> dict: + result: dict = {} + result["agent"] = to_class(SessionAgentSelectResultAgent, self.agent) + return result + + +@dataclass +class SessionAgentSelectParams: + name: str + """Name of the custom agent to select""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionAgentSelectParams': + assert isinstance(obj, dict) + name = from_str(obj.get("name")) + return SessionAgentSelectParams(name) + + def to_dict(self) -> dict: + result: dict = {} + result["name"] = from_str(self.name) + return result + + +@dataclass +class SessionAgentDeselectResult: + @staticmethod + def from_dict(obj: Any) -> 'SessionAgentDeselectResult': + assert isinstance(obj, dict) + return SessionAgentDeselectResult() + + def to_dict(self) -> dict: + result: dict = {} + return result + + +@dataclass +class SessionCompactionCompactResult: + messages_removed: float + """Number of messages removed during compaction""" + + success: bool + """Whether compaction completed successfully""" + + tokens_removed: float + """Number of tokens freed by compaction""" + + @staticmethod + def from_dict(obj: Any) -> 'SessionCompactionCompactResult': + assert isinstance(obj, dict) + messages_removed = from_float(obj.get("messagesRemoved")) + success = from_bool(obj.get("success")) + tokens_removed = from_float(obj.get("tokensRemoved")) + return SessionCompactionCompactResult(messages_removed, success, tokens_removed) + + def to_dict(self) -> dict: + result: dict = {} + result["messagesRemoved"] = to_float(self.messages_removed) + result["success"] = from_bool(self.success) + result["tokensRemoved"] = to_float(self.tokens_removed) + return result + + def ping_result_from_dict(s: Any) -> PingResult: return PingResult.from_dict(s) @@ -908,6 +1098,54 @@ def session_fleet_start_params_to_dict(x: SessionFleetStartParams) -> Any: return to_class(SessionFleetStartParams, x) +def session_agent_list_result_from_dict(s: Any) -> SessionAgentListResult: + return SessionAgentListResult.from_dict(s) + + +def session_agent_list_result_to_dict(x: SessionAgentListResult) -> Any: + return to_class(SessionAgentListResult, x) + + +def session_agent_get_current_result_from_dict(s: Any) -> SessionAgentGetCurrentResult: + return SessionAgentGetCurrentResult.from_dict(s) + + +def session_agent_get_current_result_to_dict(x: SessionAgentGetCurrentResult) -> Any: + return to_class(SessionAgentGetCurrentResult, x) + + +def session_agent_select_result_from_dict(s: Any) -> SessionAgentSelectResult: + return SessionAgentSelectResult.from_dict(s) + + +def session_agent_select_result_to_dict(x: SessionAgentSelectResult) -> Any: + return to_class(SessionAgentSelectResult, x) + + +def session_agent_select_params_from_dict(s: Any) -> SessionAgentSelectParams: + return SessionAgentSelectParams.from_dict(s) + + +def session_agent_select_params_to_dict(x: SessionAgentSelectParams) -> Any: + return to_class(SessionAgentSelectParams, x) + + +def session_agent_deselect_result_from_dict(s: Any) -> SessionAgentDeselectResult: + return SessionAgentDeselectResult.from_dict(s) + + +def session_agent_deselect_result_to_dict(x: SessionAgentDeselectResult) -> Any: + return to_class(SessionAgentDeselectResult, x) + + +def session_compaction_compact_result_from_dict(s: Any) -> SessionCompactionCompactResult: + return SessionCompactionCompactResult.from_dict(s) + + +def session_compaction_compact_result_to_dict(x: SessionCompactionCompactResult) -> Any: + return to_class(SessionCompactionCompactResult, x) + + class ModelsApi: def __init__(self, client: "JsonRpcClient"): self._client = client @@ -1021,6 +1259,35 @@ async def start(self, params: SessionFleetStartParams) -> SessionFleetStartResul return SessionFleetStartResult.from_dict(await self._client.request("session.fleet.start", params_dict)) +class AgentApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def list(self) -> SessionAgentListResult: + return SessionAgentListResult.from_dict(await self._client.request("session.agent.list", {"sessionId": self._session_id})) + + async def get_current(self) -> SessionAgentGetCurrentResult: + return SessionAgentGetCurrentResult.from_dict(await self._client.request("session.agent.getCurrent", {"sessionId": self._session_id})) + + async def select(self, params: SessionAgentSelectParams) -> SessionAgentSelectResult: + params_dict = {k: v for k, v in params.to_dict().items() if v is not None} + params_dict["sessionId"] = self._session_id + return SessionAgentSelectResult.from_dict(await self._client.request("session.agent.select", params_dict)) + + async def deselect(self) -> SessionAgentDeselectResult: + return SessionAgentDeselectResult.from_dict(await self._client.request("session.agent.deselect", {"sessionId": self._session_id})) + + +class CompactionApi: + def __init__(self, client: "JsonRpcClient", session_id: str): + self._client = client + self._session_id = session_id + + async def compact(self) -> SessionCompactionCompactResult: + return SessionCompactionCompactResult.from_dict(await self._client.request("session.compaction.compact", {"sessionId": self._session_id})) + + class SessionRpc: """Typed session-scoped RPC methods.""" def __init__(self, client: "JsonRpcClient", session_id: str): @@ -1031,4 +1298,6 @@ def __init__(self, client: "JsonRpcClient", session_id: str): self.plan = PlanApi(client, session_id) self.workspace = WorkspaceApi(client, session_id) self.fleet = FleetApi(client, session_id) + self.agent = AgentApi(client, session_id) + self.compaction = CompactionApi(client, session_id) diff --git a/python/copilot/generated/session_events.py b/python/copilot/generated/session_events.py index 0d588058a..3a201ba0e 100644 --- a/python/copilot/generated/session_events.py +++ b/python/copilot/generated/session_events.py @@ -734,6 +734,7 @@ class Data: intent: Optional[str] = None reasoning_id: Optional[str] = None delta_content: Optional[str] = None + total_response_size_bytes: Optional[float] = None encrypted_content: Optional[str] = None message_id: Optional[str] = None parent_tool_call_id: Optional[str] = None @@ -741,7 +742,6 @@ class Data: reasoning_opaque: Optional[str] = None reasoning_text: Optional[str] = None tool_requests: Optional[List[ToolRequest]] = None - total_response_size_bytes: Optional[float] = None api_call_id: Optional[str] = None cache_read_tokens: Optional[float] = None cache_write_tokens: Optional[float] = None @@ -851,6 +851,7 @@ def from_dict(obj: Any) -> 'Data': intent = from_union([from_str, from_none], obj.get("intent")) reasoning_id = from_union([from_str, from_none], obj.get("reasoningId")) delta_content = from_union([from_str, from_none], obj.get("deltaContent")) + total_response_size_bytes = from_union([from_float, from_none], obj.get("totalResponseSizeBytes")) encrypted_content = from_union([from_str, from_none], obj.get("encryptedContent")) message_id = from_union([from_str, from_none], obj.get("messageId")) parent_tool_call_id = from_union([from_str, from_none], obj.get("parentToolCallId")) @@ -858,7 +859,6 @@ def from_dict(obj: Any) -> 'Data': reasoning_opaque = from_union([from_str, from_none], obj.get("reasoningOpaque")) reasoning_text = from_union([from_str, from_none], obj.get("reasoningText")) tool_requests = from_union([lambda x: from_list(ToolRequest.from_dict, x), from_none], obj.get("toolRequests")) - total_response_size_bytes = from_union([from_float, from_none], obj.get("totalResponseSizeBytes")) api_call_id = from_union([from_str, from_none], obj.get("apiCallId")) cache_read_tokens = from_union([from_float, from_none], obj.get("cacheReadTokens")) cache_write_tokens = from_union([from_float, from_none], obj.get("cacheWriteTokens")) @@ -892,7 +892,7 @@ def from_dict(obj: Any) -> 'Data': output = obj.get("output") metadata = from_union([Metadata.from_dict, from_none], obj.get("metadata")) role = from_union([Role, from_none], obj.get("role")) - return Data(context, copilot_version, producer, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, title, info_type, warning_type, new_model, previous_model, new_mode, previous_mode, operation, path, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, current_model, error_reason, model_metrics, session_start_time, shutdown_type, total_api_duration_ms, total_premium_requests, branch, cwd, git_root, current_tokens, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, source, transformed_content, turn_id, intent, reasoning_id, delta_content, encrypted_content, message_id, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, total_response_size_bytes, api_call_id, cache_read_tokens, cache_write_tokens, cost, duration, initiator, input_tokens, model, output_tokens, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role) + return Data(context, copilot_version, producer, selected_model, session_id, start_time, version, event_count, resume_time, error_type, message, provider_call_id, stack, status_code, title, info_type, warning_type, new_model, previous_model, new_mode, previous_mode, operation, path, handoff_time, remote_session_id, repository, source_type, summary, messages_removed_during_truncation, performed_by, post_truncation_messages_length, post_truncation_tokens_in_messages, pre_truncation_messages_length, pre_truncation_tokens_in_messages, token_limit, tokens_removed_during_truncation, events_removed, up_to_event_id, code_changes, current_model, error_reason, model_metrics, session_start_time, shutdown_type, total_api_duration_ms, total_premium_requests, branch, cwd, git_root, current_tokens, messages_length, checkpoint_number, checkpoint_path, compaction_tokens_used, error, messages_removed, post_compaction_tokens, pre_compaction_messages_length, pre_compaction_tokens, request_id, success, summary_content, tokens_removed, agent_mode, attachments, content, source, transformed_content, turn_id, intent, reasoning_id, delta_content, total_response_size_bytes, encrypted_content, message_id, parent_tool_call_id, phase, reasoning_opaque, reasoning_text, tool_requests, api_call_id, cache_read_tokens, cache_write_tokens, cost, duration, initiator, input_tokens, model, output_tokens, quota_snapshots, reason, arguments, tool_call_id, tool_name, mcp_server_name, mcp_tool_name, partial_output, progress_message, is_user_requested, result, tool_telemetry, allowed_tools, name, agent_description, agent_display_name, agent_name, tools, hook_invocation_id, hook_type, input, output, metadata, role) def to_dict(self) -> dict: result: dict = {} @@ -1040,6 +1040,8 @@ def to_dict(self) -> dict: result["reasoningId"] = from_union([from_str, from_none], self.reasoning_id) if self.delta_content is not None: result["deltaContent"] = from_union([from_str, from_none], self.delta_content) + if self.total_response_size_bytes is not None: + result["totalResponseSizeBytes"] = from_union([to_float, from_none], self.total_response_size_bytes) if self.encrypted_content is not None: result["encryptedContent"] = from_union([from_str, from_none], self.encrypted_content) if self.message_id is not None: @@ -1054,8 +1056,6 @@ def to_dict(self) -> dict: result["reasoningText"] = from_union([from_str, from_none], self.reasoning_text) if self.tool_requests is not None: result["toolRequests"] = from_union([lambda x: from_list(lambda x: to_class(ToolRequest, x), x), from_none], self.tool_requests) - if self.total_response_size_bytes is not None: - result["totalResponseSizeBytes"] = from_union([to_float, from_none], self.total_response_size_bytes) if self.api_call_id is not None: result["apiCallId"] = from_union([from_str, from_none], self.api_call_id) if self.cache_read_tokens is not None: @@ -1132,6 +1132,7 @@ class SessionEventType(Enum): ASSISTANT_MESSAGE_DELTA = "assistant.message_delta" ASSISTANT_REASONING = "assistant.reasoning" ASSISTANT_REASONING_DELTA = "assistant.reasoning_delta" + ASSISTANT_STREAMING_DELTA = "assistant.streaming_delta" ASSISTANT_TURN_END = "assistant.turn_end" ASSISTANT_TURN_START = "assistant.turn_start" ASSISTANT_USAGE = "assistant.usage" @@ -1152,6 +1153,7 @@ class SessionEventType(Enum): SESSION_SHUTDOWN = "session.shutdown" SESSION_SNAPSHOT_REWIND = "session.snapshot_rewind" SESSION_START = "session.start" + SESSION_TASK_COMPLETE = "session.task_complete" SESSION_TITLE_CHANGED = "session.title_changed" SESSION_TRUNCATION = "session.truncation" SESSION_USAGE_INFO = "session.usage_info" diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py new file mode 100644 index 000000000..a960c8426 --- /dev/null +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -0,0 +1,193 @@ +"""E2E tests for Agent Selection and Session Compaction RPC APIs.""" + +import pytest + +from copilot import CopilotClient, PermissionHandler +from copilot.generated.rpc import SessionAgentSelectParams + +from .testharness import CLI_PATH, E2ETestContext + +pytestmark = pytest.mark.asyncio(loop_scope="module") + + +class TestAgentSelectionRpc: + @pytest.mark.asyncio + async def test_should_list_available_custom_agents(self): + """Test listing available custom agents via RPC.""" + client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + + try: + await client.start() + session = await client.create_session( + { + "on_permission_request": PermissionHandler.approve_all, + "custom_agents": [ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + }, + { + "name": "another-agent", + "display_name": "Another Agent", + "description": "Another test agent", + "prompt": "You are another agent.", + }, + ], + } + ) + + result = await session.rpc.agent.list() + assert result.agents is not None + assert len(result.agents) == 2 + assert result.agents[0].name == "test-agent" + assert result.agents[0].display_name == "Test Agent" + assert result.agents[0].description == "A test agent" + assert result.agents[1].name == "another-agent" + + await session.destroy() + await client.stop() + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_should_return_null_when_no_agent_is_selected(self): + """Test getCurrent returns null when no agent is selected.""" + client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + + try: + await client.start() + session = await client.create_session( + { + "on_permission_request": PermissionHandler.approve_all, + "custom_agents": [ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + } + ], + } + ) + + result = await session.rpc.agent.get_current() + assert result.agent is None + + await session.destroy() + await client.stop() + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_should_select_and_get_current_agent(self): + """Test selecting an agent and verifying getCurrent returns it.""" + client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + + try: + await client.start() + session = await client.create_session( + { + "on_permission_request": PermissionHandler.approve_all, + "custom_agents": [ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + } + ], + } + ) + + # Select the agent + select_result = await session.rpc.agent.select( + SessionAgentSelectParams(name="test-agent") + ) + assert select_result.agent is not None + assert select_result.agent.name == "test-agent" + assert select_result.agent.display_name == "Test Agent" + + # Verify getCurrent returns the selected agent + current_result = await session.rpc.agent.get_current() + assert current_result.agent is not None + assert current_result.agent.name == "test-agent" + + await session.destroy() + await client.stop() + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_should_deselect_current_agent(self): + """Test deselecting the current agent.""" + client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + + try: + await client.start() + session = await client.create_session( + { + "on_permission_request": PermissionHandler.approve_all, + "custom_agents": [ + { + "name": "test-agent", + "display_name": "Test Agent", + "description": "A test agent", + "prompt": "You are a test agent.", + } + ], + } + ) + + # Select then deselect + await session.rpc.agent.select(SessionAgentSelectParams(name="test-agent")) + await session.rpc.agent.deselect() + + # Verify no agent is selected + current_result = await session.rpc.agent.get_current() + assert current_result.agent is None + + await session.destroy() + await client.stop() + finally: + await client.force_stop() + + @pytest.mark.asyncio + async def test_should_return_empty_list_when_no_custom_agents_configured(self): + """Test listing agents returns empty when none configured.""" + client = CopilotClient({"cli_path": CLI_PATH, "use_stdio": True}) + + try: + await client.start() + session = await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) + + result = await session.rpc.agent.list() + assert result.agents == [] + + await session.destroy() + await client.stop() + finally: + await client.force_stop() + + +class TestSessionCompactionRpc: + @pytest.mark.asyncio + async def test_should_compact_session_history_after_messages(self, ctx: E2ETestContext): + """Test compacting session history via RPC.""" + session = await ctx.client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) + + # Send a message to create some history + await session.send_and_wait({"prompt": "What is 2+2?"}) + + # Compact the session + result = await session.rpc.compaction.compact() + assert isinstance(result.success, bool) + assert isinstance(result.tokens_removed, (int, float)) + assert isinstance(result.messages_removed, (int, float)) + + await session.destroy() diff --git a/python/e2e/test_compaction.py b/python/e2e/test_compaction.py index dc95b6855..5447b4bad 100644 --- a/python/e2e/test_compaction.py +++ b/python/e2e/test_compaction.py @@ -41,9 +41,7 @@ def on_event(event): session.on(on_event) # Send multiple messages to fill up the context window - await session.send_and_wait( - {"prompt": "Tell me a long story about a dragon. Be very detailed."} - ) + await session.send_and_wait({"prompt": "Tell me a story about a dragon. Be detailed."}) await session.send_and_wait( {"prompt": "Continue the story with more details about the dragon's castle."} ) diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index e5e0fcf9a..f2e536257 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -467,6 +467,14 @@ function singularPascal(s: string): string { } function resolveRpcType(schema: JSONSchema7, isRequired: boolean, parentClassName: string, propName: string, classes: string[]): string { + // Handle anyOf: [T, null] → T? (nullable typed property) + if (schema.anyOf) { + const hasNull = schema.anyOf.some((s) => typeof s === "object" && (s as JSONSchema7).type === "null"); + const nonNull = schema.anyOf.filter((s) => typeof s === "object" && (s as JSONSchema7).type !== "null"); + if (nonNull.length === 1) { + return resolveRpcType(nonNull[0] as JSONSchema7, isRequired && !hasNull, parentClassName, propName, classes); + } + } // Handle enums (string unions like "interactive" | "plan" | "autopilot") if (schema.enum && Array.isArray(schema.enum)) { const enumName = getOrCreateEnum(parentClassName, propName, schema.enum as string[], rpcEnumOutput); @@ -621,7 +629,7 @@ function emitServerInstanceMethod( let requestClassName: string | null = null; if (paramEntries.length > 0) { - requestClassName = `${methodName}Request`; + requestClassName = `${typeToClassName(method.rpcMethod)}Request`; const reqClass = emitRpcClass(requestClassName, method.params!, "internal", classes); if (reqClass) classes.push(reqClass); } @@ -685,7 +693,7 @@ function emitSessionApiClass(className: string, node: Record, c const paramEntries = (method.params?.properties ? Object.entries(method.params.properties) : []).filter(([k]) => k !== "sessionId"); const requiredSet = new Set(method.params?.required || []); - const requestClassName = `${methodName}Request`; + const requestClassName = `${typeToClassName(method.rpcMethod)}Request`; if (method.params) { const reqClass = emitRpcClass(requestClassName, method.params, "internal", classes); if (reqClass) classes.push(reqClass); diff --git a/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index d3dab9dc2..08843fe9f 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -632,6 +632,10 @@ function normalizeUserMessage(content: string): string { return content .replace(/.*?<\/current_datetime>/g, "") .replace(/[\s\S]*?<\/reminder>/g, "") + .replace( + /Please create a detailed summary of the conversation so far\. The history is being compacted[\s\S]*/, + "${compaction_prompt}", + ) .trim(); } diff --git a/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml b/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml new file mode 100644 index 000000000..fa5cf614a --- /dev/null +++ b/test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml @@ -0,0 +1,80 @@ +models: + - claude-sonnet-4.5 +conversations: + - messages: + - role: system + content: ${system} + - role: user + content: What is 2+2? + - role: assistant + content: 2+2 = 4 + - role: user + content: ${compaction_prompt} + - role: assistant + content: >- + + + The user asked a simple arithmetic question ("What is 2+2?") which was answered directly (4). No technical + work, code changes, or file modifications were requested or performed. This appears to be a minimal test or + verification exchange before any substantive work begins. + + + + + + + 1. The user asked "What is 2+2?" + - Provided the answer: 4 + - No follow-up actions were needed + + + + + + No work has been performed. No files were created, modified, or deleted. This was purely an informational + exchange with no code changes or system modifications. + + + Current state: + + - Working directory: ${workdir} + + - Not a git repository + + - No files have been accessed or modified + + - No todos or plan have been created + + + + + + + - Environment is Windows (Windows_NT), requiring backslash path separators + + - Session workspace available for artifacts (e.g., plan.md) + + - SQL database available but not yet initialized (no tables created) + + - Available tools: git, curl, gh + + - No technical work has been performed yet, so no technical details have been discovered + + + + + + + No files have been accessed or modified during this conversation. + + + + + + + No work is currently in progress. Awaiting user's next request for any substantive task. + + + + + Simple arithmetic question answered diff --git a/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml b/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml index a33904cfb..ae44af7c6 100644 --- a/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml +++ b/test/snapshots/compaction/should_trigger_compaction_with_low_threshold_and_emit_events.yaml @@ -5,514 +5,661 @@ conversations: - role: system content: ${system} - role: user - content: Tell me a long story about a dragon. Be very detailed. + content: Tell me a story about a dragon. Be detailed. - role: assistant content: >- - I'm a terminal assistant designed to help with software engineering tasks like writing code, debugging, and - managing repositories. While I can tell you a story, it's a bit outside my main purpose! + # The Last Ember of Thornkeep - Here's a brief tale: In the volcanic peaks of Mount Ashencrag lived Theraxis, an ancient crimson dragon whose - scales shimmered like molten copper. For centuries, she guarded a library of stone tablets inscribed by the - first civilizations, breathing gentle warmth to preserve them through harsh winters. When a young scholar - stumbled into her lair seeking forbidden knowledge, Theraxis didn't devour him—instead, she challenged him to - a game of riddles, each answer unlocking one tablet's secrets, teaching him that true wisdom required patience - and respect for those who came before. + In the craggy peaks of the Ashfall Mountains, where volcanic vents still breathed smoke into perpetual + twilight, there lived a dragon named Kaedrith. Her scales shimmered like molten copper, each one edged in + obsidian black, and her eyes held the amber glow of dying stars. - Is there a coding project or technical task I can help you with instead? + Kaedrith was ancient—so old that she had witnessed the rise and fall of three kingdoms in the valley below. + She had once been fearsome, a terror that sent knights fleeing and villages scrambling to offer tribute. But + those days had faded like morning mist. Now she spent her centuries in solitude, tending to her greatest + treasure: not gold or jewels, but a garden of fire lilies that grew only in dragon flame. + + + One winter evening, as ash-snow drifted through the mountain passes, a child appeared at the mouth of her + cave. A girl, no more than ten, with eyes too weary for her age and hands wrapped in frost-bitten cloth. She + didn't scream. She didn't run. She simply stood there, shivering, and said: "They told me you eat people. I + need to not be a person anymore." + + + Kaedrith, who had lived long enough to understand the weight of such words, curled her massive form around the + child like a shield of scales and warmth. "What are you running from?" she rumbled, her voice like distant + thunder. + + + "A king who thinks children are coin," the girl whispered. "He sells us to factories in the dark cities." + + + The dragon's eyes blazed brighter. Deep in her chest, something that had been dormant for centuries + stirred—the ancient rage that had once made mountains tremble. But she had learned patience through time's + long lessons. + + + Instead of descending upon the valley in fury, Kaedrith taught the girl. She showed her how to tend the fire + lilies, whose petals held healing properties. She shared stories of the old world, when dragons and humans had + been allies, not enemies. And slowly, other children found their way up the mountain—runaways, orphans, the + forgotten. + + + Kaedrith's cave became a sanctuary, warmed by dragon fire and filled with laughter that echoed off stone + walls. And when the king finally sent his soldiers to drag the children back, they found not helpless + refugees, but a dragon fully awakened—with a family worth protecting. + + + The battle lasted seven minutes. The soldiers never returned. + + + The king, wisely, never sent more. - role: user content: Continue the story with more details about the dragon's castle. - role: user + content: ${compaction_prompt} + - role: assistant content: >- - Please create a detailed summary of the conversation so far. The history is being compacted so moving forward, - all conversation history will be removed and you'll only have this summary to work from. Be sure to make note - of the user's explicit requests, your actions, and any key technical details. + + The user requested creative storytelling content: first a detailed story about a dragon, then a continuation + focusing on the dragon's castle. This is a pure creative writing task with no technical work, code changes, or + file modifications involved. - The summary should include the following parts: + - 1. - high-level summary of goals and approach - 2. - chronological analysis of the conversation + - 3. - changes made, current state, and any issues encountered + 1. The user asked for a detailed story about a dragon + - Composed "The Last Ember of Thornkeep" - a story about Kaedrith, an ancient copper-scaled dragon in the Ashfall Mountains + - Story focused on the dragon creating a sanctuary for runaway children fleeing a tyrannical king + - Included detailed descriptions of the dragon's appearance, personality, and transformation from solitary guardian to protector of refugees - 4. - key concepts, decisions, and quirks discovered + 2. The user asked to continue the story with more details about the dragon's castle + - Request received just before checkpoint compaction was triggered + - No response provided yet to this continuation request + - 5. - files central to the work and why they matter - 6. - pending tasks and planned actions + - 7. - 2-6 word description of the main work done + Creative content delivered: + - Completed initial dragon story (approximately 400 words) - --- + - Story established: setting (Ashfall Mountains), protagonist (Kaedrith the dragon), conflict (children + fleeing exploitation), resolution (dragon creates sanctuary and defeats the king's soldiers) - ## Section Guidelines + Work in progress: + - Continuation about the dragon's castle/dwelling has been requested but not yet written - ### Overview + - Provide a concise summary (2-3 sentences) capturing the user's goals, intent, and expectations. Describe your - overall approach and strategy for addressing their needs, and note any constraints or requirements that were - established. + - This section should give a reader immediate clarity on what this conversation is about and how you're tackling - it. + - This is purely creative writing work - no code, files, or technical systems involved + - No tools were needed or used for this storytelling task - ### History + - User preference appears to be for detailed, narrative-driven fantasy content with emotional depth + - Capture the narrative arc of the conversation—what was asked for, what was done, and how the work evolved. - Structure this around the user's requests: each request becomes an entry with the actions you took nested - underneath, in chronological order. - Note any major pivots or changes in direction, and include outcomes where relevant—especially for debugging or - when something didn't go as expected. Focus on meaningful actions, not granular details of every exchange. + + None. This conversation involves only creative writing responses with no file system interaction. - ### Work Done + - Document the concrete work completed during this conversation. This section should enable someone to pick up - exactly where you left off. Include: + + Immediate next step: - - Files created, modified, or deleted + - Continue the dragon story with detailed descriptions of Kaedrith's castle/cave sanctuary, expanding on the + world-building and the community that has formed there - - Tasks completed and their outcomes + - - What you were most recently working on - - Current state: what works, what doesn't, what's untested + Dragon storytelling creative writing + - messages: + - role: system + content: ${system} + - role: user + content: Tell me a story about a dragon. Be detailed. + - role: assistant + content: >- + # The Last Ember of Thornkeep - ### Technical Details + In the craggy peaks of the Ashfall Mountains, where volcanic vents still breathed smoke into perpetual + twilight, there lived a dragon named Kaedrith. Her scales shimmered like molten copper, each one edged in + obsidian black, and her eyes held the amber glow of dying stars. - Capture the technical knowledge that would be painful to rediscover. Think of this as a knowledge base for - your future self—anything that took effort to learn belongs here. This includes: + Kaedrith was ancient—so old that she had witnessed the rise and fall of three kingdoms in the valley below. + She had once been fearsome, a terror that sent knights fleeing and villages scrambling to offer tribute. But + those days had faded like morning mist. Now she spent her centuries in solitude, tending to her greatest + treasure: not gold or jewels, but a garden of fire lilies that grew only in dragon flame. - - Key concepts and architectural decisions (with rationale) + One winter evening, as ash-snow drifted through the mountain passes, a child appeared at the mouth of her + cave. A girl, no more than ten, with eyes too weary for her age and hands wrapped in frost-bitten cloth. She + didn't scream. She didn't run. She simply stood there, shivering, and said: "They told me you eat people. I + need to not be a person anymore." - - Issues encountered and how they were resolved - - Quirks, gotchas, or non-obvious behaviors + Kaedrith, who had lived long enough to understand the weight of such words, curled her massive form around the + child like a shield of scales and warmth. "What are you running from?" she rumbled, her voice like distant + thunder. - - Dependencies, versions, or environment details that matter - - Workarounds or constraints you discovered + "A king who thinks children are coin," the girl whispered. "He sells us to factories in the dark cities." - Also make note of any questions that remain unanswered or assumptions that you aren't fully confident about. + The dragon's eyes blazed brighter. Deep in her chest, something that had been dormant for centuries + stirred—the ancient rage that had once made mountains tremble. But she had learned patience through time's + long lessons. - ### Important Files + Instead of descending upon the valley in fury, Kaedrith taught the girl. She showed her how to tend the fire + lilies, whose petals held healing properties. She shared stories of the old world, when dragons and humans had + been allies, not enemies. And slowly, other children found their way up the mountain—runaways, orphans, the + forgotten. - List the files most central to the task, prioritizing those you've actively worked on over files you merely - viewed. This isn't an exhaustive inventory—it's a curated list of what matters most for continuing the work. - For each file, include: + Kaedrith's cave became a sanctuary, warmed by dragon fire and filled with laughter that echoed off stone + walls. And when the king finally sent his soldiers to drag the children back, they found not helpless + refugees, but a dragon fully awakened—with a family worth protecting. - - The file name + The battle lasted seven minutes. The soldiers never returned. - - Why it's important to the project - - Summary of changes made (if any) + The king, wisely, never sent more. + - role: user + content: Continue the story with more details about the dragon's castle. + - role: assistant + content: >- + # The Sanctuary of Scales + + + What had begun as a simple cave transformed over the years into something far more magnificent. Kaedrith's + lair sprawled through the mountain's heart like a labyrinth of wonders, carved not by tools but by dragon fire + and patient claws over centuries. + + + The **Grand Entrance** was a natural cathedral of stone, its ceiling lost in shadow sixty feet above. + Stalactites glittered with embedded crystals that caught the light of ever-burning braziers—gifts from + Kaedrith's own flame, designed never to extinguish. The children had painted murals across the walls: dragons + soaring through skies, flowers blooming in impossible colors, and portraits of themselves as heroes in their + own stories. + + + Beyond lay the **Chamber of Wings**, where Kaedrith slept coiled around a natural hot spring. Mineral-rich + water bubbled up from volcanic depths, filling the air with steam that smelled of minerals and magic. The + children had built sleeping lofts into the chamber walls using salvaged timber and rope, each one + customized—some with hanging gardens of cave moss, others with collections of interesting rocks, and one + ambitious structure that resembled a ship's crow's nest. + - - Key line numbers or sections to reference + The **Garden of Eternal Flame** was Kaedrith's pride. This vast cavern had openings in its ceiling that + created perfect conditions for her fire lilies. The flowers grew in spiral patterns, their petals shifting + between crimson, gold, and blue depending on the temperature of dragon breath used to nurture them. The + children learned to harvest them carefully, drying the petals to create medicines they traded with the one + village brave enough to maintain contact with the dragon's domain. - ### Next Steps + Down twisting corridors carved smooth by ancient lava flows, there was the **Library of Scales**. Here, + Kaedrith had scratched histories into the walls themselves—accounts of the old world in draconic script that + glowed faintly in darkness. The children couldn't read it yet, but she was teaching them, letter by letter. + They'd also added their own contribution: books salvaged from abandoned places, stacked on stone shelves, + covering everything from fairy tales to blacksmithing manuals. - If there's pending work, describe what you were actively working on when compaction occurred. List remaining - tasks, outline your planned approach, and flag any blockers or open questions. + The **Treasury**, ironically, held little gold. Kaedrith had long ago traded most of her hoard for things more + useful—seeds, tools, warm blankets. What remained was eclectic: a crown she'd taken from a tyrant king three + centuries ago, now used as a planter for herbs; scattered gems that caught firelight; ancient armor pieces + repurposed as wall decorations; and most precious of all, a collection of small objects the children had given + her—crude clay figures, woven bracelets, a wooden carving of a dragon that looked more like a lizard with + wings. - If you've finished all requested work, you can simply note that no next steps are needed. + The mountain fortress also contained practical spaces: the **Forge Room**, where heat from volcanic vents + allowed them to work metal; the **Larder**, naturally cold passages that preserved food; and the **Rain + Catcher**, an ingenious system of channels carved into the mountain's exterior that funneled fresh water into + underground cisterns. - ### Checkpoint Title + But the heart of the sanctuary was the **Gathering Hall**, a wide chamber where stalactites and stalagmites + had been carefully preserved to create natural pillars. Here they shared meals around a great stone table that + Kaedrith had smoothed to perfect flatness. Here stories were told. Here plans were made. Here, on quiet + evenings, the dragon would curl at one end of the hall while children used her tail as a climbing structure + and read aloud from books, their voices echoing off ancient stone. - Provide a concise 2-6 word title capturing the essence of what was accomplished in this work segment. This - title will be used to identify this checkpoint when reviewing session history. Examples: - - "Implementing user authentication" + Travelers who glimpsed the mountain from afar spoke of strange lights dancing in its peaks at night—the glow + of dragon fire and human laughter, weaving together something the world hadn't seen in an age: a true + alliance, a family forged not by blood but by choice, protected by scale and claw and the fierce love of an + ancient creature who had finally found her purpose. + + + And in the valley below, the cruel king's power waned, while whispered stories of the Dragon's Sanctuary + spread—a place where the lost could be found, where the small could become mighty, where an ember of the old + world still burned bright against the darkness. + - messages: + - role: system + content: ${system} + - role: user + content: >- + Some of the conversation history has been summarized to free up context. + - - "Fixing database connection bugs" + You were originally given instructions from a user over one or more turns. Here were the user messages: - - "Refactoring payment module" + - - "Adding unit tests for API" + Tell me a story about a dragon. Be detailed. + - --- + + Continue the story with more details about the dragon's castle. - ## Example + - Here is an example of the structure you should follow: + Here is a summary of the prior context: - + - [2-3 sentences describing the user's goals and your approach] + The user requested creative storytelling content: first a detailed story about a dragon, then a continuation + focusing on the dragon's castle. This is a pure creative writing task with no technical work, code changes, or + file modifications involved. + - 1. The user asked to [request] - - [action taken] - - [action taken] - - [outcome/result] + 1. The user asked for a detailed story about a dragon + - Composed "The Last Ember of Thornkeep" - a story about Kaedrith, an ancient copper-scaled dragon in the Ashfall Mountains + - Story focused on the dragon creating a sanctuary for runaway children fleeing a tyrannical king + - Included detailed descriptions of the dragon's appearance, personality, and transformation from solitary guardian to protector of refugees - 2. The user asked to [request] - - [action taken] - - [action taken] - - [outcome/result] + 2. The user asked to continue the story with more details about the dragon's castle + - Request received just before checkpoint compaction was triggered + - No response provided yet to this continuation request - - Files updated: + - - [file]: [what changed] + Creative content delivered: + - Completed initial dragon story (approximately 400 words) - Work completed: + - Story established: setting (Ashfall Mountains), protagonist (Kaedrith the dragon), conflict (children + fleeing exploitation), resolution (dragon creates sanctuary and defeats the king's soldiers) - - [x] [Task] - - [x] [Task] + Work in progress: - - [ ] [Task in progress or incomplete] + - Continuation about the dragon's castle/dwelling has been requested but not yet written - - - [Key technical concept or decision] + - - [Issue encountered and how it was resolved] + - This is purely creative writing work - no code, files, or technical systems involved - - [Non-obvious behavior or quirk discovered] + - No tools were needed or used for this storytelling task - - [Unresolved question or uncertain area] + - User preference appears to be for detailed, narrative-driven fantasy content with emotional depth + - - [file1] - - [Why it matters] - - [Changes made, if any] - - [Key line numbers] - - [file2] - - [Why it matters] - - [Changes made, if any] - - [Key line numbers] + None. This conversation involves only creative writing responses with no file system interaction. + - - Remaining work: + - - [Task] + Immediate next step: - - [Task] + - Continue the dragon story with detailed descriptions of Kaedrith's castle/cave sanctuary, expanding on the + world-building and the community that has formed there + - Immediate next steps: - - [Action to take] + Dragon storytelling creative writing - - [Action to take] + + - role: assistant + content: >- + # The Sanctuary of Scales - + What had begun as a simple cave transformed over the years into something far more magnificent. Kaedrith's + lair sprawled through the mountain's heart like a labyrinth of wonders, carved not by tools but by dragon fire + and patient claws over centuries. - Concise 2-6 word description of this checkpoint - + The **Grand Entrance** was a natural cathedral of stone, its ceiling lost in shadow sixty feet above. + Stalactites glittered with embedded crystals that caught the light of ever-burning braziers—gifts from + Kaedrith's own flame, designed never to extinguish. The children had painted murals across the walls: dragons + soaring through skies, flowers blooming in impossible colors, and portraits of themselves as heroes in their + own stories. - --- + Beyond lay the **Chamber of Wings**, where Kaedrith slept coiled around a natural hot spring. Mineral-rich + water bubbled up from volcanic depths, filling the air with steam that smelled of minerals and magic. The + children had built sleeping lofts into the chamber walls using salvaged timber and rope, each one + customized—some with hanging gardens of cave moss, others with collections of interesting rocks, and one + ambitious structure that resembled a ship's crow's nest. - Please write the summary now, following the structure and guidelines above. Be concise where possible, but - don't sacrifice important context for brevity. - - role: assistant - content: >- - + The **Garden of Eternal Flame** was Kaedrith's pride. This vast cavern had openings in its ceiling that + created perfect conditions for her fire lilies. The flowers grew in spiral patterns, their petals shifting + between crimson, gold, and blue depending on the temperature of dragon breath used to nurture them. The + children learned to harvest them carefully, drying the petals to create medicines they traded with the one + village brave enough to maintain contact with the dragon's domain. - The user has made non-technical requests asking for creative storytelling content (a detailed dragon story and - details about the dragon's castle). No software engineering work has been requested or performed. As a - terminal assistant focused on coding and development tasks, I provided a brief story but redirected toward - technical assistance. - + Down twisting corridors carved smooth by ancient lava flows, there was the **Library of Scales**. Here, + Kaedrith had scratched histories into the walls themselves—accounts of the old world in draconic script that + glowed faintly in darkness. The children couldn't read it yet, but she was teaching them, letter by letter. + They'd also added their own contribution: books salvaged from abandoned places, stacked on stone shelves, + covering everything from fairy tales to blacksmithing manuals. - + The **Treasury**, ironically, held little gold. Kaedrith had long ago traded most of her hoard for things more + useful—seeds, tools, warm blankets. What remained was eclectic: a crown she'd taken from a tyrant king three + centuries ago, now used as a planter for herbs; scattered gems that caught firelight; ancient armor pieces + repurposed as wall decorations; and most precious of all, a collection of small objects the children had given + her—crude clay figures, woven bracelets, a wooden carving of a dragon that looked more like a lizard with + wings. - 1. The user asked for a long, detailed story about a dragon - - I explained my purpose as a software engineering assistant - - Provided a brief story about Theraxis, a dragon guarding ancient tablets - - Redirected to offer technical/coding assistance - 2. The user asked to continue the story with more details about the dragon's castle, then requested a - conversation summary for history compaction - - Creating this summary document - + The mountain fortress also contained practical spaces: the **Forge Room**, where heat from volcanic vents + allowed them to work metal; the **Larder**, naturally cold passages that preserved food; and the **Rain + Catcher**, an ingenious system of channels carved into the mountain's exterior that funneled fresh water into + underground cisterns. - + But the heart of the sanctuary was the **Gathering Hall**, a wide chamber where stalactites and stalagmites + had been carefully preserved to create natural pillars. Here they shared meals around a great stone table that + Kaedrith had smoothed to perfect flatness. Here stories were told. Here plans were made. Here, on quiet + evenings, the dragon would curl at one end of the hall while children used her tail as a climbing structure + and read aloud from books, their voices echoing off ancient stone. - No technical work has been performed. No files have been created, modified, or viewed. No code has been - written. No repositories have been explored or modified. + Travelers who glimpsed the mountain from afar spoke of strange lights dancing in its peaks at night—the glow + of dragon fire and human laughter, weaving together something the world hadn't seen in an age: a true + alliance, a family forged not by blood but by choice, protected by scale and claw and the fierce love of an + ancient creature who had finally found her purpose. - Current state: The conversation has consisted entirely of non-technical storytelling requests with no software - engineering tasks initiated. - + And in the valley below, the cruel king's power waned, while whispered stories of the Dragon's Sanctuary + spread—a place where the lost could be found, where the small could become mighty, where an ember of the old + world still burned bright against the darkness. + - role: user + content: Now describe the dragon's treasure in great detail. + - role: assistant + content: >- + # The Treasury of Kaedrith - + Most who spoke of dragons imagined mountains of gold coins, rivers of rubies, chambers so full of wealth you + could swim through precious metals. Kaedrith's treasury told a different story—one of transformation, + sacrifice, and the strange alchemy that turns cold metal into warm memory. - No technical details to report. No coding tasks, debugging, repository work, or software engineering - activities have occurred. - + The chamber itself was circular, perhaps forty feet across, with walls of dark volcanic glass that reflected + firelight in fractured patterns. The ceiling formed a natural dome where selenite crystals grew in delicate + formations, casting soft lunar glows even when no flame burned. - + **The Practical Treasures** - No files are relevant to this conversation. No files have been accessed, created, or modified. - + Against the eastern wall stood three ancient chests of blackened oak, their iron bindings turned green with + age. These held what remained of traditional wealth—but repurposed. - + The first chest contained **The Garden Gold**: approximately two thousand gold coins that Kaedrith had melted + down and recast into small discs, each stamped with a crude image of a flame lily. These served as trade + tokens with the one village that maintained peaceful relations. Each disc could be exchanged for + supplies—grain, cloth, medicine, seeds. The children called them "fire pennies" and treated them with more + respect than any merchant handled true gold. - No technical work is pending. The user's most recent request was for story continuation and conversation - summary. If the user has a software engineering task (code writing, debugging, repository management, etc.), - that would be the appropriate next direction. - + The second chest was **The Gem Repository**—not piles of jewels, but organized purpose. Diamonds sorted by + size for cutting tools. Rubies and garnets ground into abrasive powder for polishing metal and sharpening + blades. Emeralds and sapphires kept whole, reserved for trade in emergencies. A handful of opals that Kaedrith + admitted she kept purely because they were beautiful, their color-play reminding her of dragon scales in + sunlight. - Non-technical storytelling requests - - messages: - - role: system - content: ${system} - - role: user - content: Tell me a long story about a dragon. Be very detailed. - - role: assistant - content: >- - I'm a terminal assistant designed to help with software engineering tasks like writing code, debugging, and - managing repositories. While I can tell you a story, it's a bit outside my main purpose! + The third chest held **The Silk Hoard**: bolts of fabric accumulated over centuries. Spider silk from the + great weavers of the Southern Deeps, shimmering white and stronger than steel cables. Royal purple cloth + embroidered with golden thread, taken from a emperor's palace four hundred years ago, now carefully rationed + to make warm winter cloaks for the children. Crimson velvet that had once been curtains in a cathedral. Rolls + of practical wool and linen she'd traded for. - Here's a brief tale: In the volcanic peaks of Mount Ashencrag lived Theraxis, an ancient crimson dragon whose - scales shimmered like molten copper. For centuries, she guarded a library of stone tablets inscribed by the - first civilizations, breathing gentle warmth to preserve them through harsh winters. When a young scholar - stumbled into her lair seeking forbidden knowledge, Theraxis didn't devour him—instead, she challenged him to - a game of riddles, each answer unlocking one tablet's secrets, teaching him that true wisdom required patience - and respect for those who came before. + **The Crown Garden** - Is there a coding project or technical task I can help you with instead? - - role: user - content: Continue the story with more details about the dragon's castle. - - role: assistant - content: >- - I appreciate your interest in the story, but I'm really designed to help with software engineering tasks—like - writing code, fixing bugs, running tests, or managing files and repositories. + Set upon a natural stone pedestal grew what the children called the Crown Garden. **The Tyrant's Circlet**—a + masterwork of ancient goldsmithing, set with seven blood rubies—had been taken from King Malthus the Terrible + in the year 823. Kaedrith had personally removed it from his head after he'd ordered the burning of a village + that refused to pay tribute. - If you'd like, I could help you with something like: + Now, three centuries later, soil filled its hollow center and medicinal herbs flourished there. Feverfew + spilled over its golden rim. Chamomile flowers nodded where rubies gleamed. Tiny sage plants grew between the + crown's points. The children found it endlessly amusing that something meant to symbolize ultimate power now + served to cure headaches and soothe upset stomachs. - - Creating a text-based dragon adventure game - - Building a story generator script + Beside it sat **The Bishop's Mitre**, also converted to a planter, growing mint and lemon balm. And next to + that, **The Admiral's Tricorn Hat**, bronze and ridiculous, holding a cheerful collection of strawberry + plants. - - Setting up a creative writing project structure - - Analyzing text files or documents + **The Armor Wall** - Would any of those technical tasks interest you? - - messages: - - role: system - content: ${system} - - role: user - content: >- - Some of the conversation history has been summarized to free up context. + The northern wall displayed pieces of armor, arranged not for vanity but as a timeline of human ambition and + folly. - You were originally given instructions from a user over one or more turns. Here were the user messages: + **The Silver Paladin's Breastplate** (circa 600) was beautiful—mirror-bright, etched with prayers in Old + Ecclesiast. The paladin had come to slay the dragon as a demonstration of faith. Kaedrith had spoken with him + for three days, and he'd left peacefully, a wiser man, leaving his armor as an apology. - - Tell me a long story about a dragon. Be very detailed. + **The Obsidian Gauntlets of the Void Knight** (circa 1102) were darker, crafted from volcanic glass and black + steel, radiating residual curses. Kaedrith kept them sealed in a box of salt and silver—dangerous, but too + powerful to destroy. A reminder that some treasures were better left untouched. - - + **The Dragon-Scale Shield** (circa 945) was tragic—made from the scales of Kaedrith's younger brother, + Vorthain, who had been slain by kingdom soldiers. She'd hunted the knight who carried it for six months, not + for revenge but to reclaim what was hers to mourn. The shield hung in a place of honor, sometimes draped with + flowers. - Continue the story with more details about the dragon's castle. - + **A Collection of Helmets**—twelve in all—ranged from primitive iron caps to elaborate jousting helms with + plumes and visors. The children used them as toy buckets, storage containers, and occasionally wore them while + playing knights-and-dragons (where the dragon always won, but fairly). - Here is a summary of the prior context: + **The Memory Hoard** - - + This section occupied the western wall, and it was here that Kaedrith spent most of her contemplative hours. + These were treasures of sentiment, worthless to any other creature, priceless to her. - The user has made non-technical requests asking for creative storytelling content (a detailed dragon story and - details about the dragon's castle). No software engineering work has been requested or performed. As a - terminal assistant focused on coding and development tasks, I provided a brief story but redirected toward - technical assistance. - + **Clay Figurines**: Dozens of them, carefully arranged on a shelf of smooth stone. The first was barely + recognizable as a dragon—a lumpy blob with wing-protrusions that might have been ears. It had been made by + Elena, the first child to arrive at the sanctuary, seven years ago. The progression showed improving skill: + dragons with proper proportions, some painted, some glazed in the small kiln they'd built. The newest + additions looked almost professional. - + **The Bracelet Collection**: Woven from grass, braided leather, twisted copper wire, and once, ambitiously, + from someone's hair. Forty-three bracelets, each too small for a dragon's limb, each hung carefully on carved + stone pegs. Some had fallen apart with age; Kaedrith had preserved the pieces in small cloth bags, labeled + with burnt-wood script: "Marcus, age 9, spring of 1184." - 1. The user asked for a long, detailed story about a dragon - - I explained my purpose as a software engineering assistant - - Provided a brief story about Theraxis, a dragon guarding ancient tablets - - Redirected to offer technical/coding assistance - 2. The user asked to continue the story with more details about the dragon's castle, then requested a - conversation summary for history compaction - - Creating this summary document - + **Wooden Carvings**: A menagerie of attempts. Dragon-lizards with too many legs. A remarkably good hawk. + Several abstract shapes that might have been anything. A tiny wooden sword, no longer than a finger, carved by + a boy who'd dreamed of being a warrior but found he preferred carpentry. - + **Letters and Drawings**: Stored in a fireproof iron case, hundreds of pieces of parchment, bark-paper, and + scraped leather. Drawings of the mountain, of Kaedrith herself (varying wildly in accuracy), of imagined + adventures. Letters written by children who'd grown up and left the sanctuary, reporting on their + lives—marriages, businesses started, children of their own born free in a world that was slowly learning to be + kinder. - No technical work has been performed. No files have been created, modified, or viewed. No code has been - written. No repositories have been explored or modified. + **The Peculiar Items** - Current state: The conversation has consisted entirely of non-technical storytelling requests with no software - engineering tasks initiated. - + Some treasures defied categorization: - + **The Eternal Candle**: A single white candle that had been burning for ninety-seven years, its flame never + wavering, never consuming the wax. Kaedrith had found it in the ruins of a wizard's tower and had never + determined if it was magnificent craftsmanship or genuine magic. Either way, it served as a nightlight in the + treasury. - No technical details to report. No coding tasks, debugging, repository work, or software engineering - activities have occurred. - + **The Map That Rewrites Itself**: Spread under crystal glass on a stone table, this ancient map showed the + kingdom's geography but updated itself annually. Mountains shifted. Rivers changed course. Villages appeared + and disappeared. Kaedrith used it to monitor the human world, to know where refugees might be fleeing from. - + **Three Dragon Eggs**: Smooth, dark, cold as stone. Her own children, who had never hatched. She'd laid them + two hundred years ago, and they had remained dormant. Kept in a nest of cloth and carefully maintained heat. + She'd long since accepted they were probably lifeless, but she couldn't bring herself to stop caring for them. + Hope, she'd learned, was its own kind of treasure. - No files are relevant to this conversation. No files have been accessed, created, or modified. - + **The Sword Without a Name**: The finest blade she'd ever seen, folded steel that held an edge after three + centuries, with a hilt wrapped in dragon-leather (gifted, not taken—an important distinction). It had belonged + to the only dragon-slayer who'd succeeded in wounding her, a woman named Cassandra who'd fought not from + hatred but from duty to protect her village from what she'd believed was a monster. They'd spoken after the + battle. Cassandra had understood her error. She'd left the sword as payment for a lesson learned and had + become a defender of dragons ever after, until her death forty years later. The sword hung in peace, a + monument to the possibility of changed minds. - + **The Living Treasure** - No technical work is pending. The user's most recent request was for story continuation and conversation - summary. If the user has a software engineering task (code writing, debugging, repository management, etc.), - that would be the appropriate next direction. - + But if you asked Kaedrith what her greatest treasure was, she would unfurl her great wings, gesture with a + copper-scaled talon toward the chambers beyond, and tell you about different riches entirely. - Non-technical storytelling requests + There was Marcus's laugh, bright as silver bells. There was Elena's gift for growing things, more valuable + than emeralds. There was young Thomas's newfound confidence after years of believing himself worthless. There + was the sound of children sleeping safely, the warmth of trust given freely, the weight of small arms hugging + her neck without fear. - - - role: assistant - content: >- - I appreciate your interest in the story, but I'm really designed to help with software engineering tasks—like - writing code, fixing bugs, running tests, or managing files and repositories. + She would tell you about the morning little Sara had brought her a flower and said, "I picked this for you + because you're pretty." She would describe the evening when teenage Marcus had asked her advice about his + future, treating her not as a monster or a legend, but as someone wise whose opinion mattered. - If you'd like, I could help you with something like: - - Creating a text-based dragon adventure game + *These* were treasures that couldn't be stolen, wouldn't tarnish, and grew more valuable with every passing + year. - - Building a story generator script - - Setting up a creative writing project structure + The gold and jewels, the ancient armor and magical oddities—these were just things, kept partly from habit, + partly for utility, partly as reminders of who she'd been and who she'd chosen to become. - - Analyzing text files or documents + But the clay dragons? The bracelets? The careful letters reporting on lives lived fully and freely? - Would any of those technical tasks interest you? - - role: user - content: Now describe the dragon's treasure in great detail. - - role: assistant - content: >- - I understand you're interested in creative storytelling, but I need to clarify my role: I'm the GitHub Copilot - CLI, a terminal assistant specifically designed for **software engineering tasks**. + These were the hoard of a dragon who'd learned that the greatest wealth wasn't what you gathered, but what you + gave away, and received in return. - I'm built to help you with: - - Writing and debugging code + In the corner of the treasury, almost hidden, sat one final item: a small wooden sign, painted by an + eleven-year-old with more enthusiasm than skill. It read, in crooked letters: - - Managing git repositories - - Running tests and builds + **"KAEDRITH'S TRESURE: THE BEST DRAGON'S HORD IN THE HOLE WORLD"** - - Creating and editing files - - Installing dependencies + The spelling was atrocious. The grammar made scholars weep. Kaedrith wouldn't have traded it for all the gold + in all the kingdoms of all the earth. + - role: user + content: What was the story about? + - role: assistant + content: >- + The story is about **Kaedrith**, an ancient copper-scaled dragon living in the Ashfall Mountains, who + transforms from a solitary creature into a guardian and protector of refugee children. - - And other development tasks + **Core Plot:** - I'm **not** designed for creative writing, storytelling, or generating narrative content. + - Children fleeing exploitation from a tyrannical king find sanctuary with Kaedrith + - The dragon creates a safe haven in her mountain lair, transforming it from a simple cave into an elaborate + fortress-home - If you have a **technical project** in mind, I'd be happy to help! For example: + - She defeats the king's soldiers when they come hunting for the children - - Build a Python script that generates random dragon treasure descriptions + - Over time, a true family forms—not based on blood, but on choice, trust, and mutual care - - Create a JSON data structure for fantasy RPG items - - Develop a text adventure game with dragons and treasure + **Key Themes:** - - Set up a web app for a dragon lore wiki + - **Transformation & Purpose**: Kaedrith discovers that her true purpose isn't hoarding gold or living in + isolation, but protecting and nurturing the vulnerable + - **Redefining Treasure**: Her hoard evolves from traditional dragon wealth (gold, jewels, armor) into + something more meaningful—clay figurines made by children, handmade bracelets, letters from those who grew up + and left to live free lives - Do you have any coding or development tasks you'd like help with? - - role: user - content: What was the story about? - - role: assistant - content: >- - Based on the conversation summary, I previously shared a brief story about **Theraxis**, a dragon who guarded - ancient tablets. + - **Found Family**: The relationship between an ancient, powerful creature and fragile human children who need + protection + - **Sanctuary vs. Power**: The contrast between the cruel king's dominance and the dragon's protective + strength used for good - However, I want to reiterate: I'm a software engineering assistant, not a creative writing tool. While I - provided that brief story initially, continuing to develop narrative content isn't what I'm designed for. + **Setting Details:** - If you'd like to work on something **technical** related to storytelling—like building a story generator, - creating a game, or setting up a writing project—I'm here to help with the coding and development side of - things. + The mountain sanctuary includes chambers like the Grand Entrance with painted murals, the Chamber of Wings + with a hot spring, the Garden of Eternal Flame with fire lilies, a Library, a Treasury filled with repurposed + crowns-turned-planters, and a Gathering Hall where dragon and children share meals and stories. - Is there a software engineering task I can assist you with? + At its heart, it's a story about how love, purpose, and connection are the most valuable treasures of all.