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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions dotnet/src/Client.cs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,8 @@ public async Task<CopilotSession> CreateSessionAsync(SessionConfig? config = nul
config?.OnPermissionRequest != null ? true : null,
config?.Streaming == true ? true : null,
config?.McpServers,
config?.CustomAgents);
config?.CustomAgents,
config?.ConfigDir);

var response = await connection.Rpc.InvokeWithCancellationAsync<CreateSessionResponse>(
"session.create", [request], cancellationToken);
Expand Down Expand Up @@ -922,7 +923,8 @@ private record CreateSessionRequest(
bool? RequestPermission,
bool? Streaming,
Dictionary<string, object>? McpServers,
List<CustomAgentConfig>? CustomAgents);
List<CustomAgentConfig>? CustomAgents,
string? ConfigDir);

private record ToolDefinition(
string Name,
Expand Down
7 changes: 7 additions & 0 deletions dotnet/src/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,13 @@ public class SessionConfig
{
public string? SessionId { get; set; }
public string? Model { get; set; }

/// <summary>
/// Override the default configuration directory location.
/// When specified, the session will use this directory for storing config and state.
/// </summary>
public string? ConfigDir { get; set; }

public ICollection<AIFunction>? Tools { get; set; }
public SystemMessageConfig? SystemMessage { get; set; }
public List<string>? AvailableTools { get; set; }
Expand Down
21 changes: 21 additions & 0 deletions dotnet/test/SessionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -322,4 +322,25 @@ public async Task Should_Receive_Session_Events()

await session.DisposeAsync();
}

[Fact]
public async Task Should_Create_Session_With_Custom_Config_Dir()
{
var customConfigDir = Path.Combine(Ctx.HomeDir, "custom-config");
var session = await Client.CreateSessionAsync(new SessionConfig { ConfigDir = customConfigDir });

Assert.Matches(@"^[a-f0-9-]+$", session.SessionId);

// Verify config.json was written to the custom config dir
var configPath = Path.Combine(customConfigDir, "github-copilot", "config.json");
Assert.True(File.Exists(configPath), $"Expected config.json at {configPath}");
var configContent = System.Text.Json.JsonDocument.Parse(await File.ReadAllTextAsync(configPath));
Assert.Equal(session.SessionId, configContent.RootElement.GetProperty("sessionId").GetString());

// Session should work normally with custom config dir
await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" });
var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session);
Assert.NotNull(assistantMessage);
Assert.Contains("2", assistantMessage!.Data.Content);
}
}
4 changes: 4 additions & 0 deletions go/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,10 @@ func (c *Client) CreateSession(config *SessionConfig) (*Session, error) {
}
params["customAgents"] = customAgents
}
// Add config directory override
if config.ConfigDir != "" {
params["configDir"] = config.ConfigDir
}
}

result, err := c.client.Request("session.create", params)
Expand Down
48 changes: 48 additions & 0 deletions go/e2e/session_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package e2e

import (
"encoding/json"
"os"
"regexp"
"strings"
"testing"
Expand Down Expand Up @@ -687,6 +689,52 @@ func TestSession(t *testing.T) {
t.Errorf("Expected assistant message to contain '300', got %v", assistantMessage.Data.Content)
}
})

t.Run("should create session with custom config dir", func(t *testing.T) {
ctx.ConfigureForTest(t)

customConfigDir := ctx.HomeDir + "/custom-config"
session, err := client.CreateSession(&copilot.SessionConfig{
ConfigDir: customConfigDir,
})
if err != nil {
t.Fatalf("Failed to create session with custom config dir: %v", err)
}

matched, _ := regexp.MatchString(`^[a-f0-9-]+$`, session.SessionID)
if !matched {
t.Errorf("Expected session ID to match UUID pattern, got %q", session.SessionID)
}

// Verify config.json was written to the custom config dir
configPath := customConfigDir + "/github-copilot/config.json"
configData, err := os.ReadFile(configPath)
if err != nil {
t.Fatalf("Failed to read config.json at %s: %v", configPath, err)
}
var configContent map[string]interface{}
if err := json.Unmarshal(configData, &configContent); err != nil {
t.Fatalf("Failed to parse config.json: %v", err)
}
if configContent["sessionId"] != session.SessionID {
t.Errorf("Expected config.json sessionId to be %q, got %v", session.SessionID, configContent["sessionId"])
}

// Session should work normally with custom config dir
_, err = session.Send(copilot.MessageOptions{Prompt: "What is 1+1?"})
if err != nil {
t.Fatalf("Failed to send message: %v", err)
}

assistantMessage, err := testharness.GetFinalAssistantMessage(session, 60*time.Second)
if err != nil {
t.Fatalf("Failed to get assistant message: %v", err)
}

if assistantMessage.Data.Content == nil || !strings.Contains(*assistantMessage.Data.Content, "2") {
t.Errorf("Expected assistant message to contain '2', got %v", assistantMessage.Data.Content)
}
})
}

func getSystemMessage(exchange testharness.ParsedHttpExchange) string {
Expand Down
3 changes: 3 additions & 0 deletions go/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ type SessionConfig struct {
SessionID string
// Model to use for this session
Model string
// ConfigDir overrides the default configuration directory location.
// When specified, the session will use this directory for storing config and state.
ConfigDir string
// Tools exposes caller-implemented tools to the CLI
Tools []Tool
// SystemMessage configures system message customization
Expand Down
1 change: 1 addition & 0 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,7 @@ export class CopilotClient {
streaming: config.streaming,
mcpServers: config.mcpServers,
customAgents: config.customAgents,
configDir: config.configDir,
});

const sessionId = (response as { sessionId: string }).sessionId;
Expand Down
6 changes: 6 additions & 0 deletions nodejs/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,12 @@ export interface SessionConfig {
*/
model?: string;

/**
* Override the default configuration directory location.
* When specified, the session will use this directory for storing config and state.
*/
configDir?: string;

/**
* Tools exposed to the CLI server
*/
Expand Down
21 changes: 21 additions & 0 deletions nodejs/test/e2e/session.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as fs from "node:fs";
import { describe, expect, it, onTestFinished } from "vitest";
import { ParsedHttpExchange } from "../../../test/harness/replayingCapiProxy.js";
import { CopilotClient } from "../../src/index.js";
Expand Down Expand Up @@ -334,6 +335,26 @@
const assistantMessage = await getFinalAssistantMessage(session);
expect(assistantMessage.data.content).toContain("300");
});

it("should create session with custom config dir", async () => {
const customConfigDir = `${homeDir}/custom-config`;
const session = await client.createSession({
configDir: customConfigDir,
});

expect(session.sessionId).toMatch(/^[a-f0-9-]+$/);

// Verify config.json was written to the custom config dir
const configPath = `${customConfigDir}/github-copilot/config.json`;
expect(fs.existsSync(configPath)).toBe(true);

Check failure on line 349 in nodejs/test/e2e/session.test.ts

View workflow job for this annotation

GitHub Actions / Node.js SDK Tests (macos-latest)

test/e2e/session.test.ts > Sessions > should create session with custom config dir

AssertionError: expected false to be true // Object.is equality - Expected + Received - true + false ❯ test/e2e/session.test.ts:349:43

Check failure on line 349 in nodejs/test/e2e/session.test.ts

View workflow job for this annotation

GitHub Actions / Node.js SDK Tests (windows-latest)

test/e2e/session.test.ts > Sessions > should create session with custom config dir

AssertionError: expected false to be true // Object.is equality - Expected + Received - true + false ❯ test/e2e/session.test.ts:349:43

Check failure on line 349 in nodejs/test/e2e/session.test.ts

View workflow job for this annotation

GitHub Actions / Node.js SDK Tests (ubuntu-latest)

test/e2e/session.test.ts > Sessions > should create session with custom config dir

AssertionError: expected false to be true // Object.is equality - Expected + Received - true + false ❯ test/e2e/session.test.ts:349:43
const configContent = JSON.parse(fs.readFileSync(configPath, "utf-8"));
expect(configContent).toHaveProperty("sessionId", session.sessionId);

// Session should work normally with custom config dir
await session.send({ prompt: "What is 1+1?" });
const assistantMessage = await getFinalAssistantMessage(session);
expect(assistantMessage.data.content).toContain("2");
});
});

function getSystemMessage(exchange: ParsedHttpExchange): string | undefined {
Expand Down
5 changes: 5 additions & 0 deletions python/copilot/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,11 @@ async def create_session(self, config: Optional[SessionConfig] = None) -> Copilo
self._convert_custom_agent_to_wire_format(agent) for agent in custom_agents
]

# Add config directory override if provided
config_dir = cfg.get("config_dir")
if config_dir:
payload["configDir"] = config_dir

if not self._client:
raise RuntimeError("Client not connected")
response = await self._client.request("session.create", payload)
Expand Down
3 changes: 3 additions & 0 deletions python/copilot/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ class SessionConfig(TypedDict, total=False):
mcp_servers: Dict[str, MCPServerConfig]
# Custom agent configurations for the session
custom_agents: List[CustomAgentConfig]
# Override the default configuration directory location.
# When specified, the session will use this directory for storing config and state.
config_dir: str


# Azure-specific provider options
Expand Down
21 changes: 21 additions & 0 deletions python/e2e/test_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,27 @@ def on_event(event):
assistant_message = await get_final_assistant_message(session)
assert "300" in assistant_message.data.content

async def test_should_create_session_with_custom_config_dir(self, ctx: E2ETestContext):
import json
import os

custom_config_dir = os.path.join(ctx.home_dir, "custom-config")
session = await ctx.client.create_session({"config_dir": custom_config_dir})

assert session.session_id

# Verify config.json was written to the custom config dir
config_path = os.path.join(custom_config_dir, "github-copilot", "config.json")
assert os.path.exists(config_path), f"Expected config.json at {config_path}"
with open(config_path) as f:
config_content = json.load(f)
assert config_content.get("sessionId") == session.session_id

# Session should work normally with custom config dir
await session.send({"prompt": "What is 1+1?"})
assistant_message = await get_final_assistant_message(session)
assert "2" in assistant_message.data.content


def _get_system_message(exchange: dict) -> str:
messages = exchange.get("request", {}).get("messages", [])
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
models:
- claude-sonnet-4.5
conversations:
- messages:
- role: system
content: ${system}
- role: user
content: What is 1+1?
- role: assistant
content: 1 + 1 = 2
Loading