From b7fccbc8925f5b6423c279a6f7c01927eae3aa66 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 23 Feb 2026 14:53:56 +0000 Subject: [PATCH 1/9] Add SDK support for agent selection and session compact APIs Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- nodejs/src/generated/rpc.ts | 130 ++++++ nodejs/test/e2e/agent_and_compact_rpc.test.ts | 136 +++++++ test/harness/replayingCapiProxy.ts | 1 + ...ompact_session_history_after_messages.yaml | 80 ++++ ...on_with_low_threshold_and_emit_events.yaml | 371 +++--------------- 5 files changed, 412 insertions(+), 306 deletions(-) create mode 100644 nodejs/test/e2e/agent_and_compact_rpc.test.ts create mode 100644 test/snapshots/agent_and_compact_rpc/should_compact_session_history_after_messages.yaml 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/test/e2e/agent_and_compact_rpc.test.ts b/nodejs/test/e2e/agent_and_compact_rpc.test.ts new file mode 100644 index 000000000..c6fda00b0 --- /dev/null +++ b/nodejs/test/e2e/agent_and_compact_rpc.test.ts @@ -0,0 +1,136 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +import { describe, expect, it } from "vitest"; +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({ 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({ 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({ 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({ 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(); + + 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(); + + // 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/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index d3dab9dc2..5c465ee4c 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -632,6 +632,7 @@ 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..cc7e4ddb6 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 @@ -24,241 +24,14 @@ conversations: - role: user content: Continue the story with more details about the dragon's castle. - role: user - 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 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 - - 4. - key concepts, decisions, and quirks discovered - - 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 - - - --- - - - ## Section Guidelines - - - ### 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. - - - ### History - - - 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. - - - ### Work Done - - - Document the concrete work completed during this conversation. This section should enable someone to pick up - exactly where you left off. Include: - - - - Files created, modified, or deleted - - - Tasks completed and their outcomes - - - What you were most recently working on - - - Current state: what works, what doesn't, what's untested - - - ### Technical Details - - - 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: - - - - Key concepts and architectural decisions (with rationale) - - - Issues encountered and how they were resolved - - - Quirks, gotchas, or non-obvious behaviors - - - Dependencies, versions, or environment details that matter - - - Workarounds or constraints you discovered - - - Also make note of any questions that remain unanswered or assumptions that you aren't fully confident about. - - - ### Important Files - - - 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: - - - - The file name - - - Why it's important to the project - - - Summary of changes made (if any) - - - Key line numbers or sections to reference - - - ### Next Steps - - - 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. - - If you've finished all requested work, you can simply note that no next steps are needed. - - - ### Checkpoint Title - - - 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" - - - "Fixing database connection bugs" - - - "Refactoring payment module" - - - "Adding unit tests for API" - - - --- - - - ## Example - - - Here is an example of the structure you should follow: - - - - - - [2-3 sentences describing the user's goals and your approach] - - - - - - 1. The user asked to [request] - - [action taken] - - [action taken] - - [outcome/result] - - 2. The user asked to [request] - - [action taken] - - [action taken] - - [outcome/result] - - - - - Files updated: - - - [file]: [what changed] - - - Work completed: - - - [x] [Task] - - - [x] [Task] - - - [ ] [Task in progress or incomplete] - - - - - - - [Key technical concept or decision] - - - [Issue encountered and how it was resolved] - - - [Non-obvious behavior or quirk discovered] - - - [Unresolved question or uncertain area] - - - - - - - [file1] - - [Why it matters] - - [Changes made, if any] - - [Key line numbers] - - [file2] - - [Why it matters] - - [Changes made, if any] - - [Key line numbers] - - - - - Remaining work: - - - [Task] - - - [Task] - - - Immediate next steps: - - - [Action to take] - - - [Action to take] - - - - - Concise 2-6 word description of this checkpoint - - - - - --- - - - Please write the summary now, following the structure and guidelines above. Be concise where possible, but - don't sacrifice important context for brevity. + content: ${compaction_prompt} - role: assistant content: >- - 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. + The user requested creative storytelling content (a detailed story about a dragon and its castle), which is + outside the scope of a terminal assistant designed for software engineering tasks. No technical work was + performed, and the user has not yet provided any coding or development-related requests. @@ -266,53 +39,53 @@ conversations: 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 + - I provided a brief story about Theraxis, a dragon guarding ancient knowledge + - I clarified my role as a terminal assistant and offered to help with coding projects instead - 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 + 2. The user asked to continue the story with more details about the dragon's castle + - No action was taken as the compaction request came immediately after - 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. - - - Current state: The conversation has consisted entirely of non-technical storytelling requests with no software - engineering tasks initiated. + No technical work has been performed. No files have been created, modified, or deleted. No coding tasks have + been attempted. The conversation has consisted entirely of non-technical storytelling requests. - No technical details to report. No coding tasks, debugging, repository work, or software engineering - activities have occurred. + - No technical work has been done + + - No codebase has been explored or modified + + - Environment: Windows (${workdir}) + + - No git repository detected in working directory + + - No SQL tables have been created yet - No files are relevant to this conversation. No files have been accessed, created, or modified. + No files have been worked on or are relevant to the conversation. - 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. + Awaiting a technical or software engineering task from the user. Current requests have been for creative + storytelling, which is outside the assistant's primary purpose. - Non-technical storytelling requests + No technical work performed - messages: - role: system content: ${system} @@ -375,6 +148,12 @@ conversations: + + + Now describe the dragon's treasure in great detail. + + + Here is a summary of the prior context: @@ -382,10 +161,9 @@ conversations: - 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. + The user requested creative storytelling content (a detailed story about a dragon and its castle), which is + outside the scope of a terminal assistant designed for software engineering tasks. No technical work was + performed, and the user has not yet provided any coding or development-related requests. @@ -393,53 +171,53 @@ conversations: 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 + - I provided a brief story about Theraxis, a dragon guarding ancient knowledge + - I clarified my role as a terminal assistant and offered to help with coding projects instead - 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 + 2. The user asked to continue the story with more details about the dragon's castle + - No action was taken as the compaction request came immediately after - 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. - - - Current state: The conversation has consisted entirely of non-technical storytelling requests with no software - engineering tasks initiated. + No technical work has been performed. No files have been created, modified, or deleted. No coding tasks have + been attempted. The conversation has consisted entirely of non-technical storytelling requests. - No technical details to report. No coding tasks, debugging, repository work, or software engineering - activities have occurred. + - No technical work has been done + + - No codebase has been explored or modified + + - Environment: Windows (${workdir}) + + - No git repository detected in working directory + + - No SQL tables have been created yet - No files are relevant to this conversation. No files have been accessed, created, or modified. + No files have been worked on or are relevant to the conversation. - 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. + Awaiting a technical or software engineering task from the user. Current requests have been for creative + storytelling, which is outside the assistant's primary purpose. - Non-technical storytelling requests + No technical work performed - role: assistant @@ -464,55 +242,36 @@ conversations: 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**. + I understand you're interested in the dragon story, but as I've mentioned, I'm a terminal assistant built for + software engineering tasks—not creative storytelling. - I'm built to help you with: + I'm here to help with things like: - Writing and debugging code - - Managing git repositories + - Managing files and repositories - - Running tests and builds - - - Creating and editing files + - Running builds and tests - Installing dependencies - - And other development tasks - + - Automating tasks with scripts - I'm **not** designed for creative writing, storytelling, or generating narrative content. - - If you have a **technical project** in mind, I'd be happy to help! For example: - - - Build a Python script that generates random dragon treasure descriptions - - - Create a JSON data structure for fantasy RPG items - - - Develop a text adventure game with dragons and treasure - - - Set up a web app for a dragon lore wiki - - - Do you have any coding or development tasks you'd like help with? + If you have any coding projects or technical tasks you need help with, I'd be happy to assist with those + instead! - 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. - - - 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. + Based on the conversation summary, I previously told you a brief story about Theraxis, a dragon who guarded + ancient knowledge. - 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. + However, I should clarify that creative storytelling isn't really what I'm designed for. I'm a terminal + assistant focused on software engineering tasks like coding, testing, file management, and development + workflows. - Is there a software engineering task I can assist you with? + Is there a technical project or coding task I can help you with today? From 74e166a2277d27baa3f53ee83714638d72987235 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 23 Feb 2026 15:05:09 +0000 Subject: [PATCH 2/9] Add Python, Go, and .NET e2e tests for agent selection and compaction APIs - Regenerate RPC types for Python, Go, and .NET from updated schema - Add e2e tests for all 3 languages matching Node.js test patterns - Fix C# codegen request class name collisions (use full RPC method path) - Update dotnet/src/Generated/SessionEvents.cs and go/generated_session_events.go Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Generated/Rpc.cs | 253 +++++++++++++--- dotnet/test/AgentAndCompactRpcTests.cs | 140 +++++++++ go/internal/e2e/agent_and_compact_rpc_test.go | 281 ++++++++++++++++++ go/rpc/generated_rpc.go | 166 ++++++++++- python/copilot/generated/rpc.py | 269 +++++++++++++++++ python/e2e/test_agent_and_compact_rpc.py | 185 ++++++++++++ scripts/codegen/csharp.ts | 4 +- 7 files changed, 1248 insertions(+), 50 deletions(-) create mode 100644 dotnet/test/AgentAndCompactRpcTests.cs create mode 100644 go/internal/e2e/agent_and_compact_rpc_test.go create mode 100644 python/e2e/test_agent_and_compact_rpc.py diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index ac010ed86..8d5076738 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,110 @@ 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 SessionAgentGetCurrentResult +{ + /// Currently selected custom agent, or null if using the default agent + [JsonPropertyName("agent")] + public object? 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 +522,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 +559,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 +572,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 +592,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 +618,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 +644,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 +677,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 +710,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 +790,40 @@ 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(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/test/AgentAndCompactRpcTests.cs b/dotnet/test/AgentAndCompactRpcTests.cs new file mode 100644 index 000000000..2adf97eae --- /dev/null +++ b/dotnet/test/AgentAndCompactRpcTests.cs @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * 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 Client.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 Client.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 Client.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); + } + + [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 Client.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 Client.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 Client.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/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..829f1309f --- /dev/null +++ b/go/internal/e2e/agent_and_compact_rpc_test.go @@ -0,0 +1,281 @@ +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{ + 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{ + 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{ + 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{ + 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(), nil) + 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(), nil) + 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/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/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/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py new file mode 100644 index 000000000..6d7553cbc --- /dev/null +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -0,0 +1,185 @@ +"""E2E tests for Agent Selection and Session Compaction RPC APIs.""" + +import pytest + +from copilot import CopilotClient +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( + { + "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( + { + "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( + { + "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( + { + "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({}) + + 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({}) + + # 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/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index e5e0fcf9a..6ebe07d97 100644 --- a/scripts/codegen/csharp.ts +++ b/scripts/codegen/csharp.ts @@ -621,7 +621,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 +685,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); From 4918984f0b14b5b0de172f05a91fe5dbcd8007d8 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Mon, 23 Feb 2026 15:34:40 +0000 Subject: [PATCH 3/9] Better type info for result Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- dotnet/src/Generated/Rpc.cs | 18 +++++++++++++++++- dotnet/test/AgentAndCompactRpcTests.cs | 1 + scripts/codegen/csharp.ts | 8 ++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Generated/Rpc.cs b/dotnet/src/Generated/Rpc.cs index 8d5076738..677aecfd2 100644 --- a/dotnet/src/Generated/Rpc.cs +++ b/dotnet/src/Generated/Rpc.cs @@ -374,11 +374,26 @@ internal class SessionAgentListRequest 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 object? Agent { get; set; } + public SessionAgentGetCurrentResultAgent? Agent { get; set; } } internal class SessionAgentGetCurrentRequest @@ -794,6 +809,7 @@ public async Task CompactAsync(CancellationToken [JsonSerializable(typeof(SessionAgentDeselectResult))] [JsonSerializable(typeof(SessionAgentGetCurrentRequest))] [JsonSerializable(typeof(SessionAgentGetCurrentResult))] +[JsonSerializable(typeof(SessionAgentGetCurrentResultAgent))] [JsonSerializable(typeof(SessionAgentListRequest))] [JsonSerializable(typeof(SessionAgentListResult))] [JsonSerializable(typeof(SessionAgentSelectRequest))] diff --git a/dotnet/test/AgentAndCompactRpcTests.cs b/dotnet/test/AgentAndCompactRpcTests.cs index 2adf97eae..adbe40d89 100644 --- a/dotnet/test/AgentAndCompactRpcTests.cs +++ b/dotnet/test/AgentAndCompactRpcTests.cs @@ -89,6 +89,7 @@ public async Task Should_Select_And_Get_Current_Agent() // 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] diff --git a/scripts/codegen/csharp.ts b/scripts/codegen/csharp.ts index 6ebe07d97..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); From 6c71ef2238e633b8898f748c3b0a887f558e1846 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 24 Feb 2026 23:28:29 +0000 Subject: [PATCH 4/9] Update Copilot dependency and re-run codegen --- dotnet/src/Generated/SessionEvents.cs | 47 ++++++++++++++++-- go/generated_session_events.go | 4 +- nodejs/package-lock.json | 56 +++++++++++----------- nodejs/package.json | 2 +- nodejs/src/generated/session-events.ts | 21 +++++++- python/copilot/generated/session_events.py | 12 +++-- 6 files changed, 102 insertions(+), 40 deletions(-) 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/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/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/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/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" From d45db9dabb9cf89dc64474d0879aebe964143227 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Tue, 24 Feb 2026 23:56:06 +0000 Subject: [PATCH 5/9] E2E updates --- dotnet/test/AgentAndCompactRpcTests.cs | 4 +- nodejs/test/e2e/compaction.test.ts | 2 +- test/harness/replayingCapiProxy.ts | 5 +- ...on_with_low_threshold_and_emit_events.yaml | 586 +++++++++++++++--- 4 files changed, 494 insertions(+), 103 deletions(-) diff --git a/dotnet/test/AgentAndCompactRpcTests.cs b/dotnet/test/AgentAndCompactRpcTests.cs index adbe40d89..fe70d7bdd 100644 --- a/dotnet/test/AgentAndCompactRpcTests.cs +++ b/dotnet/test/AgentAndCompactRpcTests.cs @@ -120,7 +120,7 @@ public async Task Should_Deselect_Current_Agent() [Fact] public async Task Should_Return_Empty_List_When_No_Custom_Agents_Configured() { - var session = await Client.CreateSessionAsync(); + var session = await CreateSessionAsync(); var result = await session.Rpc.Agent.ListAsync(); Assert.Empty(result.Agents); @@ -129,7 +129,7 @@ public async Task Should_Return_Empty_List_When_No_Custom_Agents_Configured() [Fact] public async Task Should_Compact_Session_History_After_Messages() { - var session = await Client.CreateSessionAsync(); + var session = await CreateSessionAsync(); // Send a message to create some history await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 2+2?" }); 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/test/harness/replayingCapiProxy.ts b/test/harness/replayingCapiProxy.ts index 5c465ee4c..08843fe9f 100644 --- a/test/harness/replayingCapiProxy.ts +++ b/test/harness/replayingCapiProxy.ts @@ -632,7 +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}") + .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/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 cc7e4ddb6..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,22 +5,57 @@ 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 @@ -29,103 +64,197 @@ conversations: content: >- - The user requested creative storytelling content (a detailed story about a dragon and its castle), which is - outside the scope of a terminal assistant designed for software engineering tasks. No technical work was - performed, and the user has not yet provided any coding or development-related requests. + 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 for a long, detailed story about a dragon - - I provided a brief story about Theraxis, a dragon guarding ancient knowledge - - I clarified my role as a terminal assistant and offered to help with coding projects instead + 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 continue the story with more details about the dragon's castle - - No action was taken as the compaction request came immediately after + - Request received just before checkpoint compaction was triggered + - No response provided yet to this continuation request - No technical work has been performed. No files have been created, modified, or deleted. No coding tasks have - been attempted. The conversation has consisted entirely of non-technical storytelling requests. + 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) - - - No technical work has been done + Work in progress: - - No codebase has been explored or modified + - Continuation about the dragon's castle/dwelling has been requested but not yet written - - Environment: Windows (${workdir}) + - - No git repository detected in working directory - - No SQL tables have been created yet + + + - This is purely creative writing work - no code, files, or technical systems involved + + - No tools were needed or used for this storytelling task + + - User preference appears to be for detailed, narrative-driven fantasy content with emotional depth - No files have been worked on or are relevant to the conversation. + None. This conversation involves only creative writing responses with no file system interaction. - Awaiting a technical or software engineering task from the user. Current requests have been for creative - storytelling, which is outside the assistant's primary purpose. + Immediate next step: + + - 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 - No technical work performed + Dragon storytelling creative writing - messages: - 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 + + + 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. + + + 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. - 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. + "A king who thinks children are coin," the girl whispered. "He sells us to factories in the dark cities." - Is there a coding project or technical task I can help you with instead? + + 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: 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. + # 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. - If you'd like, I could help you with something like: - - Creating a text-based dragon adventure game + 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. - - Building a story generator script - - Setting up a creative writing project structure + 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. - - Analyzing text files or documents + 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. - Would any of those technical tasks interest you? + + 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. + + + 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. + + + 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} @@ -138,7 +267,7 @@ conversations: - Tell me a long story about a dragon. Be very detailed. + Tell me a story about a dragon. Be detailed. @@ -148,12 +277,6 @@ conversations: - - - Now describe the dragon's treasure in great detail. - - - Here is a summary of the prior context: @@ -161,117 +284,382 @@ conversations: - The user requested creative storytelling content (a detailed story about a dragon and its castle), which is - outside the scope of a terminal assistant designed for software engineering tasks. No technical work was - performed, and the user has not yet provided any coding or development-related requests. + 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 for a long, detailed story about a dragon - - I provided a brief story about Theraxis, a dragon guarding ancient knowledge - - I clarified my role as a terminal assistant and offered to help with coding projects instead + 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 continue the story with more details about the dragon's castle - - No action was taken as the compaction request came immediately after + - Request received just before checkpoint compaction was triggered + - No response provided yet to this continuation request - No technical work has been performed. No files have been created, modified, or deleted. No coding tasks have - been attempted. The conversation has consisted entirely of non-technical storytelling requests. + 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) - - - No technical work has been done + Work in progress: + + - Continuation about the dragon's castle/dwelling has been requested but not yet written + + + - - No codebase has been explored or modified + - - Environment: Windows (${workdir}) + - This is purely creative writing work - no code, files, or technical systems involved - - No git repository detected in working directory + - No tools were needed or used for this storytelling task - - No SQL tables have been created yet + - User preference appears to be for detailed, narrative-driven fantasy content with emotional depth - No files have been worked on or are relevant to the conversation. + None. This conversation involves only creative writing responses with no file system interaction. - Awaiting a technical or software engineering task from the user. Current requests have been for creative - storytelling, which is outside the assistant's primary purpose. + Immediate next step: + + - 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 - No technical work performed + Dragon storytelling creative writing - 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. + # 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. - If you'd like, I could help you with something like: + 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. - - Creating a text-based dragon adventure game - - Building a story generator script + 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. - - Setting up a creative writing project structure - - Analyzing text files or documents + 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. - Would any of those technical tasks interest you? + 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. + + + 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. - role: user content: Now describe the dragon's treasure in great detail. - role: assistant content: >- - I understand you're interested in the dragon story, but as I've mentioned, I'm a terminal assistant built for - software engineering tasks—not creative storytelling. + # 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. + + + 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. - I'm here to help with things like: + **The Practical Treasures** - - Writing and debugging code - - Managing files and repositories + 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. - - Running builds and tests - - Installing dependencies + 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. - - Automating tasks with scripts + 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. - If you have any coding projects or technical tasks you need help with, I'd be happy to assist with those - instead! + + 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. + + + **The Crown Garden** + + + 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. + + + 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. + + + 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. + + + **The Armor Wall** + + + The northern wall displayed pieces of armor, arranged not for vanity but as a timeline of human ambition and + folly. + + + **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. + + + **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. + + + **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). + + + **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. + + + **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." + + + **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. + + + **The Peculiar Items** + + + 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. + + + **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. + + + **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** + + + 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. + + + 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. + + + 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. + + + *These* were treasures that couldn't be stolen, wouldn't tarnish, and grew more valuable with every passing + year. + + + 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. + + + But the clay dragons? The bracelets? The careful letters reporting on lives lived fully and freely? + + + 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. + + + 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: + + + **"KAEDRITH'S TRESURE: THE BEST DRAGON'S HORD IN THE HOLE WORLD"** + + + 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: >- - Based on the conversation summary, I previously told you a brief story about Theraxis, a dragon who guarded - ancient knowledge. + 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. + + + **Core Plot:** + + - 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 + + - She defeats the king's soldiers when they come hunting for the children + + - Over time, a true family forms—not based on blood, but on choice, trust, and mutual care + + + **Key Themes:** + + - **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 + + - **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 + + **Setting Details:** - However, I should clarify that creative storytelling isn't really what I'm designed for. I'm a terminal - assistant focused on software engineering tasks like coding, testing, file management, and development - workflows. + 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 technical project or coding task I can help you with today? + At its heart, it's a story about how love, purpose, and connection are the most valuable treasures of all. From 696d14c73974205a4a3762dca982a02fe9236f6e Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 25 Feb 2026 00:02:51 +0000 Subject: [PATCH 6/9] Updates after rebase --- dotnet/test/AgentAndCompactRpcTests.cs | 8 ++++---- go/internal/e2e/agent_and_compact_rpc_test.go | 12 ++++++++++-- nodejs/test/e2e/agent_and_compact_rpc.test.ts | 13 +++++++------ python/e2e/test_agent_and_compact_rpc.py | 10 +++++++--- 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/dotnet/test/AgentAndCompactRpcTests.cs b/dotnet/test/AgentAndCompactRpcTests.cs index fe70d7bdd..5f40d4e2b 100644 --- a/dotnet/test/AgentAndCompactRpcTests.cs +++ b/dotnet/test/AgentAndCompactRpcTests.cs @@ -33,7 +33,7 @@ public async Task Should_List_Available_Custom_Agents() } }; - var session = await Client.CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); + var session = await CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); var result = await session.Rpc.Agent.ListAsync(); Assert.NotNull(result.Agents); @@ -58,7 +58,7 @@ public async Task Should_Return_Null_When_No_Agent_Is_Selected() } }; - var session = await Client.CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); + var session = await CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); var result = await session.Rpc.Agent.GetCurrentAsync(); Assert.Null(result.Agent); @@ -78,7 +78,7 @@ public async Task Should_Select_And_Get_Current_Agent() } }; - var session = await Client.CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); + var session = await CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); // Select the agent var selectResult = await session.Rpc.Agent.SelectAsync("test-agent"); @@ -106,7 +106,7 @@ public async Task Should_Deselect_Current_Agent() } }; - var session = await Client.CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); + var session = await CreateSessionAsync(new SessionConfig { CustomAgents = customAgents }); // Select then deselect await session.Rpc.Agent.SelectAsync("test-agent"); diff --git a/go/internal/e2e/agent_and_compact_rpc_test.go b/go/internal/e2e/agent_and_compact_rpc_test.go index 829f1309f..338f4da67 100644 --- a/go/internal/e2e/agent_and_compact_rpc_test.go +++ b/go/internal/e2e/agent_and_compact_rpc_test.go @@ -26,6 +26,7 @@ func TestAgentSelectionRpc(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, CustomAgents: []copilot.CustomAgentConfig{ { Name: "test-agent", @@ -80,6 +81,7 @@ func TestAgentSelectionRpc(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, CustomAgents: []copilot.CustomAgentConfig{ { Name: "test-agent", @@ -119,6 +121,7 @@ func TestAgentSelectionRpc(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, CustomAgents: []copilot.CustomAgentConfig{ { Name: "test-agent", @@ -173,6 +176,7 @@ func TestAgentSelectionRpc(t *testing.T) { } session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, CustomAgents: []copilot.CustomAgentConfig{ { Name: "test-agent", @@ -222,7 +226,9 @@ func TestAgentSelectionRpc(t *testing.T) { t.Fatalf("Failed to start client: %v", err) } - session, err := client.CreateSession(t.Context(), nil) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) if err != nil { t.Fatalf("Failed to create session: %v", err) } @@ -254,7 +260,9 @@ func TestSessionCompactionRpc(t *testing.T) { t.Run("should compact session history after messages", func(t *testing.T) { ctx.ConfigureForTest(t) - session, err := client.CreateSession(t.Context(), nil) + session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{ + OnPermissionRequest: copilot.PermissionHandler.ApproveAll, + }) if err != nil { t.Fatalf("Failed to create session: %v", err) } diff --git a/nodejs/test/e2e/agent_and_compact_rpc.test.ts b/nodejs/test/e2e/agent_and_compact_rpc.test.ts index c6fda00b0..86bdb9e6b 100644 --- a/nodejs/test/e2e/agent_and_compact_rpc.test.ts +++ b/nodejs/test/e2e/agent_and_compact_rpc.test.ts @@ -3,6 +3,7 @@ *--------------------------------------------------------------------------------------------*/ 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"; @@ -25,7 +26,7 @@ describe("Agent Selection RPC", async () => { }, ]; - const session = await client.createSession({ customAgents }); + const session = await client.createSession({ onPermissionRequest: approveAll, customAgents }); const result = await session.rpc.agent.list(); expect(result.agents).toBeDefined(); @@ -49,7 +50,7 @@ describe("Agent Selection RPC", async () => { }, ]; - const session = await client.createSession({ customAgents }); + const session = await client.createSession({ onPermissionRequest: approveAll, customAgents }); const result = await session.rpc.agent.getCurrent(); expect(result.agent).toBeNull(); @@ -67,7 +68,7 @@ describe("Agent Selection RPC", async () => { }, ]; - const session = await client.createSession({ customAgents }); + const session = await client.createSession({ onPermissionRequest: approveAll, customAgents }); // Select the agent const selectResult = await session.rpc.agent.select({ name: "test-agent" }); @@ -93,7 +94,7 @@ describe("Agent Selection RPC", async () => { }, ]; - const session = await client.createSession({ customAgents }); + const session = await client.createSession({ onPermissionRequest: approveAll, customAgents }); // Select then deselect await session.rpc.agent.select({ name: "test-agent" }); @@ -107,7 +108,7 @@ describe("Agent Selection RPC", async () => { }); it("should return empty list when no custom agents configured", async () => { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); const result = await session.rpc.agent.list(); expect(result.agents).toEqual([]); @@ -120,7 +121,7 @@ describe("Session Compact RPC", async () => { const { copilotClient: client } = await createSdkTestContext(); it("should compact session history after messages", async () => { - const session = await client.createSession(); + const session = await client.createSession({ onPermissionRequest: approveAll }); // Send a message to create some history await session.sendAndWait({ prompt: "What is 2+2?" }); diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index 6d7553cbc..85147a276 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -2,7 +2,7 @@ import pytest -from copilot import CopilotClient +from copilot import CopilotClient, PermissionHandler from copilot.generated.rpc import SessionAgentSelectParams from .testharness import CLI_PATH, E2ETestContext @@ -20,6 +20,7 @@ async def test_should_list_available_custom_agents(self): await client.start() session = await client.create_session( { + "on_permission_request": PermissionHandler.approve_all, "custom_agents": [ { "name": "test-agent", @@ -59,6 +60,7 @@ async def test_should_return_null_when_no_agent_is_selected(self): await client.start() session = await client.create_session( { + "on_permission_request": PermissionHandler.approve_all, "custom_agents": [ { "name": "test-agent", @@ -87,6 +89,7 @@ async def test_should_select_and_get_current_agent(self): await client.start() session = await client.create_session( { + "on_permission_request": PermissionHandler.approve_all, "custom_agents": [ { "name": "test-agent", @@ -125,6 +128,7 @@ async def test_should_deselect_current_agent(self): await client.start() session = await client.create_session( { + "on_permission_request": PermissionHandler.approve_all, "custom_agents": [ { "name": "test-agent", @@ -156,7 +160,7 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self): try: await client.start() - session = await client.create_session({}) + session = await client.create_session({"on_permission_request": PermissionHandler.approve_all}) result = await session.rpc.agent.list() assert result.agents == [] @@ -171,7 +175,7 @@ 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({}) + 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?"}) From a54bbe7c99922b6c563b567e7b585726b192e9f7 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 25 Feb 2026 00:05:40 +0000 Subject: [PATCH 7/9] Formatting --- nodejs/test/e2e/agent_and_compact_rpc.test.ts | 20 +++++++++++++++---- python/e2e/test_agent_and_compact_rpc.py | 16 +++++++++------ 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/nodejs/test/e2e/agent_and_compact_rpc.test.ts b/nodejs/test/e2e/agent_and_compact_rpc.test.ts index 86bdb9e6b..47fc83229 100644 --- a/nodejs/test/e2e/agent_and_compact_rpc.test.ts +++ b/nodejs/test/e2e/agent_and_compact_rpc.test.ts @@ -26,7 +26,10 @@ describe("Agent Selection RPC", async () => { }, ]; - const session = await client.createSession({ onPermissionRequest: approveAll, customAgents }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + customAgents, + }); const result = await session.rpc.agent.list(); expect(result.agents).toBeDefined(); @@ -50,7 +53,10 @@ describe("Agent Selection RPC", async () => { }, ]; - const session = await client.createSession({ onPermissionRequest: approveAll, customAgents }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + customAgents, + }); const result = await session.rpc.agent.getCurrent(); expect(result.agent).toBeNull(); @@ -68,7 +74,10 @@ describe("Agent Selection RPC", async () => { }, ]; - const session = await client.createSession({ onPermissionRequest: approveAll, customAgents }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + customAgents, + }); // Select the agent const selectResult = await session.rpc.agent.select({ name: "test-agent" }); @@ -94,7 +103,10 @@ describe("Agent Selection RPC", async () => { }, ]; - const session = await client.createSession({ onPermissionRequest: approveAll, customAgents }); + const session = await client.createSession({ + onPermissionRequest: approveAll, + customAgents, + }); // Select then deselect await session.rpc.agent.select({ name: "test-agent" }); diff --git a/python/e2e/test_agent_and_compact_rpc.py b/python/e2e/test_agent_and_compact_rpc.py index 85147a276..a960c8426 100644 --- a/python/e2e/test_agent_and_compact_rpc.py +++ b/python/e2e/test_agent_and_compact_rpc.py @@ -34,7 +34,7 @@ async def test_should_list_available_custom_agents(self): "description": "Another test agent", "prompt": "You are another agent.", }, - ] + ], } ) @@ -68,7 +68,7 @@ async def test_should_return_null_when_no_agent_is_selected(self): "description": "A test agent", "prompt": "You are a test agent.", } - ] + ], } ) @@ -97,7 +97,7 @@ async def test_should_select_and_get_current_agent(self): "description": "A test agent", "prompt": "You are a test agent.", } - ] + ], } ) @@ -136,7 +136,7 @@ async def test_should_deselect_current_agent(self): "description": "A test agent", "prompt": "You are a test agent.", } - ] + ], } ) @@ -160,7 +160,9 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self): try: await client.start() - session = await client.create_session({"on_permission_request": PermissionHandler.approve_all}) + session = await client.create_session( + {"on_permission_request": PermissionHandler.approve_all} + ) result = await session.rpc.agent.list() assert result.agents == [] @@ -175,7 +177,9 @@ 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}) + 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?"}) From d66ab0407fe7bc775dc51c70acfb15133f011397 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 25 Feb 2026 00:10:27 +0000 Subject: [PATCH 8/9] Test consistency --- dotnet/test/CompactionTests.cs | 2 +- go/internal/e2e/compaction_test.go | 2 +- python/e2e/test_compaction.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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/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/python/e2e/test_compaction.py b/python/e2e/test_compaction.py index dc95b6855..1dfcfd13d 100644 --- a/python/e2e/test_compaction.py +++ b/python/e2e/test_compaction.py @@ -42,7 +42,7 @@ def on_event(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."} + {"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."} From b4dec996cb2a1411cd03e9e2332f4e96df89ee45 Mon Sep 17 00:00:00 2001 From: Steve Sanderson Date: Wed, 25 Feb 2026 00:11:48 +0000 Subject: [PATCH 9/9] More formatting --- python/e2e/test_compaction.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/e2e/test_compaction.py b/python/e2e/test_compaction.py index 1dfcfd13d..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 story about a dragon. Be 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."} )