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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .lastmerge
Original file line number Diff line number Diff line change
@@ -1 +1 @@
b9f746ab1b9c5f31af03a6f8a6cf4e680b3fd6b8
dcd86c189501ce1b46b787ca60d90f3f315f3079
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

> **Upstream sync:** [`github/copilot-sdk@b9f746a`](https://github.com/github/copilot-sdk/commit/b9f746ab1b9c5f31af03a6f8a6cf4e680b3fd6b8)
> **Upstream sync:** [`github/copilot-sdk@dcd86c1`](https://github.com/github/copilot-sdk/commit/dcd86c189501ce1b46b787ca60d90f3f315f3079)

### Added

- `CopilotSession.setModel(String)` — changes the model for an existing session mid-conversation; the new model takes effect for the next message, and conversation history is preserved (upstream: [`bd98e3a`](https://github.com/github/copilot-sdk/commit/bd98e3a))
- `ToolDefinition.createOverride(String, String, Map, ToolHandler)` — creates a tool definition that overrides a built-in CLI tool with the same name (upstream: [`f843c80`](https://github.com/github/copilot-sdk/commit/f843c80))
- `ToolDefinition` record now includes `overridesBuiltInTool` field; when `true`, signals to the CLI that the custom tool intentionally replaces a built-in (upstream: [`f843c80`](https://github.com/github/copilot-sdk/commit/f843c80))
- `CopilotSession.listAgents()` — lists custom agents available for selection (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb))
- `CopilotSession.getCurrentAgent()` — gets the currently selected custom agent (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb))
- `CopilotSession.selectAgent(String)` — selects a custom agent for the session (upstream: [`9d998fb`](https://github.com/github/copilot-sdk/commit/9d998fb))
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/com/github/copilot/sdk/CopilotSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -808,6 +808,28 @@ public CompletableFuture<Void> abort() {
return rpc.invoke("session.abort", Map.of("sessionId", sessionId), Void.class);
}

/**
* Changes the model for this session.
* <p>
* The new model takes effect for the next message. Conversation history is
* preserved.
*
* <pre>{@code
* session.setModel("gpt-4.1").get();
* }</pre>
*
* @param model
* the model ID to switch to (e.g., {@code "gpt-4.1"})
* @return a future that completes when the model switch is acknowledged
* @throws IllegalStateException
* if this session has been terminated
* @since 1.0.11
*/
public CompletableFuture<Void> setModel(String model) {
ensureNotTerminated();
return rpc.invoke("session.model.switchTo", Map.of("sessionId", sessionId, "modelId", model), Void.class);
}

/**
* Lists the custom agents available for selection in this session.
*
Expand Down
33 changes: 31 additions & 2 deletions src/main/java/com/github/copilot/sdk/json/ToolDefinition.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,18 @@
* the JSON Schema defining the tool's parameters
* @param handler
* the handler function to execute when invoked
* @param overridesBuiltInTool
* when {@code true}, indicates that this tool intentionally
* overrides a built-in CLI tool with the same name; {@code null} or
* {@code false} means the tool is purely custom
* @see SessionConfig#setTools(java.util.List)
* @see ToolHandler
* @since 1.0.0
*/
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("description") String description,
@JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler) {
@JsonProperty("parameters") Object parameters, @JsonIgnore ToolHandler handler,
@JsonProperty("overridesBuiltInTool") Boolean overridesBuiltInTool) {

/**
* Creates a tool definition with a JSON schema for parameters.
Expand All @@ -74,6 +79,30 @@ public record ToolDefinition(@JsonProperty("name") String name, @JsonProperty("d
*/
public static ToolDefinition create(String name, String description, Map<String, Object> schema,
ToolHandler handler) {
return new ToolDefinition(name, description, schema, handler);
return new ToolDefinition(name, description, schema, handler, null);
}

/**
* Creates a tool definition that overrides a built-in CLI tool.
* <p>
* Use this factory method when you want your custom tool to replace a built-in
* tool (e.g., {@code grep}, {@code read_file}) with the same name. Setting
* {@code overridesBuiltInTool} to {@code true} signals to the CLI that this is
* intentional.
*
* @param name
* the name of the built-in tool to override
* @param description
* a description of what the tool does
* @param schema
* the JSON Schema as a {@code Map}
* @param handler
* the handler function to execute when invoked
* @return a new tool definition with the override flag set
* @since 1.0.11
*/
public static ToolDefinition createOverride(String name, String description, Map<String, Object> schema,
ToolHandler handler) {
return new ToolDefinition(name, description, schema, handler, true);
}
}
58 changes: 58 additions & 0 deletions src/site/markdown/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ This guide covers advanced scenarios for extending and customizing your Copilot
## Table of Contents

- [Custom Tools](#Custom_Tools)
- [Overriding Built-in Tools](#Overriding_Built-in_Tools)
- [Switching Models Mid-Session](#Switching_Models_Mid-Session)
- [System Messages](#System_Messages)
- [Adding Rules](#Adding_Rules)
- [Full Control](#Full_Control)
Expand Down Expand Up @@ -73,6 +75,62 @@ var session = client.createSession(

See [ToolDefinition](apidocs/com/github/copilot/sdk/json/ToolDefinition.html) Javadoc for schema details.

### Overriding Built-in Tools

You can replace a built-in CLI tool (such as `grep` or `read_file`) with your own implementation
by using `ToolDefinition.createOverride()`. This signals to the CLI that the name collision is
intentional and your custom implementation should be used instead.

```java
var customGrep = ToolDefinition.createOverride(
"grep",
"Project-aware search with custom filtering",
Map.of(
"type", "object",
"properties", Map.of(
"query", Map.of("type", "string", "description", "Search query")
),
"required", List.of("query")
),
invocation -> {
String query = (String) invocation.getArguments().get("query");
// Your custom search logic here
return CompletableFuture.completedFuture("Results for: " + query);
}
);

var session = client.createSession(
new SessionConfig()
.setTools(List.of(customGrep))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
```

---

## Switching Models Mid-Session

You can change the model used by an existing session without losing conversation history.
The new model takes effect starting with the next message sent.

```java
var session = client.createSession(
new SessionConfig()
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();

// Switch to a different model mid-conversation
session.setModel("gpt-4.1").get();

// Next message will use the new model
session.sendAndWait(new MessageOptions().setPrompt("Continue with the new model")).get();
```

The session emits a [`SessionModelChangeEvent`](apidocs/com/github/copilot/sdk/events/SessionModelChangeEvent.html)
when the switch completes, which you can observe with `session.on(SessionModelChangeEvent.class, event -> ...)`.

See [CopilotSession.setModel()](apidocs/com/github/copilot/sdk/CopilotSession.html#setModel(java.lang.String)) Javadoc for details.

---

## System Messages
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -352,4 +352,23 @@ void testTryWithResourcesDoubleClose() throws Exception {
});
}
}

/**
* Verifies that setModel() throws IllegalStateException after session is
* terminated.
*/
@Test
void testSetModelThrowsAfterTermination() throws Exception {
ctx.configureForTest("session", "should_receive_session_events");

try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client.createSession(new SessionConfig()
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get();
session.close();

assertThrows(IllegalStateException.class, () -> {
session.setModel("gpt-4.1");
});
}
}
}
40 changes: 40 additions & 0 deletions src/test/java/com/github/copilot/sdk/ToolsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -320,4 +320,44 @@ void testDeniesCustomToolWhenPermissionDenied(TestInfo testInfo) throws Exceptio
session.close();
}
}

/**
* Verifies that a custom tool can override a built-in CLI tool with the same
* name when {@code overridesBuiltInTool} is set to {@code true}.
*
* @see Snapshot: tools/overrides_built_in_tool_with_custom_tool
*/
@Test
void testOverridesBuiltInToolWithCustomTool() throws Exception {
ctx.configureForTest("tools", "overrides_built_in_tool_with_custom_tool");

var parameters = new HashMap<String, Object>();
var properties = new HashMap<String, Object>();
properties.put("query", Map.of("type", "string", "description", "Search query"));
parameters.put("type", "object");
parameters.put("properties", properties);
parameters.put("required", List.of("query"));

ToolDefinition customGrep = ToolDefinition.createOverride("grep", "A custom grep implementation", parameters,
(invocation) -> {
Map<String, Object> args = invocation.getArguments();
String query = (String) args.get("query");
return CompletableFuture.completedFuture("CUSTOM_GREP_RESULT: " + query);
});

try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(customGrep))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();

AssistantMessageEvent response = session
.sendAndWait(new MessageOptions().setPrompt("Use grep to search for the word 'hello'"))
.get(60, TimeUnit.SECONDS);

assertNotNull(response);
assertTrue(response.getData().content().contains("CUSTOM_GREP_RESULT"),
"Response should contain CUSTOM_GREP_RESULT: " + response.getData().content());

session.close();
}
}
}
Loading