Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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(TestInfo testInfo) 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