Skip to content

Commit a2b2c18

Browse files
Update SDK snapshot for Copilot CLI 1.0.90-5
1 parent 36ff7a2 commit a2b2c18

8 files changed

Lines changed: 239 additions & 6 deletions

‎.github/workflows/sdk-python.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ jobs:
3333
os: ${{ fromJSON(inputs.platform == 'linux-x64' && '["ubuntu-latest"]' || inputs.platform == 'darwin-arm64' && '["macos-latest"]' || inputs.platform == 'win32-x64' && '["windows-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }}
3434
transport: [default, inprocess]
3535
runs-on: ${{ matrix.os }}
36+
timeout-minutes: 20
3637
defaults:
3738
run:
3839
shell: bash

‎.github/workflows/sdk.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -410,7 +410,7 @@ jobs:
410410
- sdk-win32-x64
411411
- sdk-docs
412412
- check-freshness
413-
runs-on: ubuntu-latest
413+
runs-on: ubuntu-slim
414414
steps:
415415
- name: Check SDK results
416416
env:

‎dotnet/test/E2E/HookLifecycleAndOutputE2ETests.cs‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------------------------------------------*/
44

5+
using GitHub.Copilot.Test.Harness;
56
using Microsoft.Extensions.AI;
67
using Xunit;
78
using Xunit.Abstractions;
@@ -78,6 +79,87 @@ public async Task Should_Invoke_OnUserPromptSubmitted_Hook_When_Sending_A_Messag
7879
await session.DisposeAsync();
7980
}
8081

82+
[Fact]
83+
[Trait(E2ETestTraits.Backend, E2ETestTraits.SelfConfiguredBackend)]
84+
public async Task Should_Invoke_Session_Hooks_With_Named_Agent_After_Recreate()
85+
{
86+
var prompts = new List<string>();
87+
var handler = new RecordingRequestHandler();
88+
await using var client = Ctx.CreateClient(options: new CopilotClientOptions
89+
{
90+
Connection = RuntimeConnection.ForStdio(),
91+
RequestHandler = handler,
92+
});
93+
await client.StartAsync();
94+
95+
var hooks = new SessionHooks
96+
{
97+
OnUserPromptSubmitted = (input, _) =>
98+
{
99+
lock (prompts)
100+
{
101+
prompts.Add(input.Prompt);
102+
}
103+
return Task.FromResult<UserPromptSubmittedHookOutput?>(null);
104+
},
105+
};
106+
var agent = new CustomAgentConfig
107+
{
108+
Name = "named-hook-agent",
109+
Description = "Responds with a short answer",
110+
Prompt = "Respond briefly to the user.",
111+
};
112+
var provider = new ProviderConfig
113+
{
114+
Type = "openai",
115+
WireApi = "responses",
116+
BaseUrl = "https://named-agent-hooks.invalid/v1",
117+
ApiKey = "test-key",
118+
ModelId = "claude-sonnet-5",
119+
WireModel = "claude-sonnet-5",
120+
};
121+
await using var session = await Ctx.CreateSessionAsync(client, new SessionConfig
122+
{
123+
Model = "claude-sonnet-5",
124+
Provider = provider,
125+
Streaming = true,
126+
CustomAgents = [agent],
127+
Agent = agent.Name,
128+
Hooks = hooks,
129+
OnPermissionRequest = PermissionHandler.ApproveAll,
130+
});
131+
132+
var first = await session.SendAndWaitAsync(new MessageOptions { Prompt = "First named-agent turn" });
133+
Assert.Contains(RecordingRequestHandler.SyntheticText, first?.Data.Content ?? string.Empty);
134+
lock (prompts)
135+
{
136+
Assert.Single(prompts);
137+
Assert.Contains("First named-agent turn", prompts[0]);
138+
}
139+
await session.DisposeAsync();
140+
141+
await using var recreated = await Ctx.CreateSessionAsync(client, new SessionConfig
142+
{
143+
SessionId = session.SessionId,
144+
Model = "claude-sonnet-5",
145+
Provider = provider,
146+
Streaming = true,
147+
CustomAgents = [agent],
148+
Agent = agent.Name,
149+
Hooks = hooks,
150+
OnPermissionRequest = PermissionHandler.ApproveAll,
151+
});
152+
Assert.Equal(session.SessionId, recreated.SessionId);
153+
var second = await recreated.SendAndWaitAsync(new MessageOptions { Prompt = "Second named-agent turn" });
154+
Assert.Contains(RecordingRequestHandler.SyntheticText, second?.Data.Content ?? string.Empty);
155+
lock (prompts)
156+
{
157+
Assert.Equal(2, prompts.Count);
158+
Assert.Contains("Second named-agent turn", prompts[1]);
159+
}
160+
Assert.Equal(2, handler.InferenceRequests.Count);
161+
}
162+
81163
[Fact]
82164
public async Task Should_Invoke_OnSessionEnd_Hook_When_Session_Is_Disconnected()
83165
{

‎nodejs/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"url": "https://github.com/github/copilot-sdk.git"
66
},
77
"version": "0.0.0-dev",
8-
"copilotCliVersion": "1.0.90-4",
8+
"copilotCliVersion": "1.0.90-5",
99
"description": "TypeScript SDK for programmatic control of GitHub Copilot CLI via JSON-RPC",
1010
"main": "./dist/cjs/index.js",
1111
"types": "./dist/index.d.ts",

‎nodejs/src/cliVersion.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
export const COPILOT_CLI_VERSION = "1.0.90-4";
1+
export const COPILOT_CLI_VERSION = "1.0.90-5";
22

33
export const COPILOT_CLI_USE_NPM_PACKAGE = false;

‎nodejs/test/e2e/hooks.e2e.test.ts‎

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,25 @@
55
import { readFile, writeFile } from "fs/promises";
66
import { join } from "path";
77
import { describe, expect, it } from "vitest";
8+
import { z } from "zod";
89
import type {
10+
CopilotClient,
11+
PermissionRequest,
912
PreToolUseHookInput,
1013
PreToolUseHookOutput,
1114
PostToolUseHookInput,
1215
PostToolUseHookOutput,
1316
} from "../../src/index.js";
14-
import { approveAll } from "../../src/index.js";
15-
import { createSdkTestContext } from "./harness/sdkTestContext.js";
17+
import { approveAll, defineTool, RuntimeConnection } from "../../src/index.js";
18+
import {
19+
createSdkTestContext,
20+
getLegacyCliPathForTests,
21+
isInProcessTransport,
22+
} from "./harness/sdkTestContext.js";
1623

1724
describe("Session hooks", async () => {
18-
const { copilotClient: client, workDir } = await createSdkTestContext();
25+
const ctx = await createSdkTestContext();
26+
const { copilotClient: client, workDir } = ctx;
1927

2028
it("should invoke preToolUse hook when model runs a tool", async () => {
2129
const preToolUseInputs: PreToolUseHookInput[] = [];
@@ -161,4 +169,78 @@ describe("Session hooks", async () => {
161169

162170
await session.disconnect();
163171
});
172+
173+
// Disconnecting the last owner tears the session down, so each resume must register
174+
// the SDK's hooks with the freshly restored hook service.
175+
async function expectPreToolUseAfterResume(target: CopilotClient): Promise<void> {
176+
const preToolUseInputs: PreToolUseHookInput[] = [];
177+
const permissionRequests: PermissionRequest[] = [];
178+
const sessionOptions = () => ({
179+
tools: [
180+
defineTool("encrypt_string", {
181+
description: "Encrypts a string",
182+
parameters: z.object({
183+
input: z.string().describe("String to encrypt"),
184+
}),
185+
handler: ({ input }: { input: string }) => input.toUpperCase(),
186+
}),
187+
],
188+
// Records rather than denies so a regression surfaces as a clear assertion
189+
// failure instead of a model-dependent denial transcript.
190+
onPermissionRequest: (request: PermissionRequest) => {
191+
permissionRequests.push(request);
192+
return { kind: "approve-once" } as const;
193+
},
194+
hooks: {
195+
onPreToolUse: async (input: PreToolUseHookInput) => {
196+
preToolUseInputs.push(input);
197+
return { permissionDecision: "allow" } as PreToolUseHookOutput;
198+
},
199+
},
200+
});
201+
202+
let sessionId: string;
203+
{
204+
await using session1 = await target.createSession(sessionOptions());
205+
sessionId = session1.sessionId;
206+
await session1.sendAndWait({
207+
prompt: "Use encrypt_string to encrypt this string: Hello",
208+
});
209+
expect(preToolUseInputs.map((input) => input.toolName)).toEqual(["encrypt_string"]);
210+
}
211+
212+
await using session2 = await target.resumeSession(sessionId, sessionOptions());
213+
const answer = await session2.sendAndWait({
214+
prompt: "Use encrypt_string to encrypt this string: World",
215+
});
216+
217+
expect(preToolUseInputs.map((input) => input.toolName)).toEqual([
218+
"encrypt_string",
219+
"encrypt_string",
220+
]);
221+
expect(permissionRequests).toEqual([]);
222+
// Validate the final assistant response arrived (guards against truncated captures)
223+
expect(answer?.data.content).toContain("WORLD");
224+
}
225+
226+
it("should invoke preToolUse hook for a custom tool after disconnect and resume", async () => {
227+
await expectPreToolUseAfterResume(client);
228+
});
229+
230+
// The legacy JavaScript CLI host tears sessions down through a separate path.
231+
it.skipIf(isInProcessTransport)(
232+
"should invoke preToolUse hook for a custom tool after disconnect and resume on the legacy CLI",
233+
async () => {
234+
const legacyClient = ctx.createClient({
235+
connection: RuntimeConnection.forStdio({
236+
path: await getLegacyCliPathForTests(),
237+
}),
238+
});
239+
try {
240+
await expectPreToolUseAfterResume(legacyClient);
241+
} finally {
242+
await legacyClient.stop();
243+
}
244+
}
245+
);
164246
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
models:
2+
- claude-sonnet-5
3+
conversations:
4+
- messages:
5+
- role: system
6+
content: ${system}
7+
- role: user
8+
content: "Use encrypt_string to encrypt this string: Hello"
9+
- role: assistant
10+
tool_calls:
11+
- id: toolcall_0
12+
type: function
13+
function:
14+
name: encrypt_string
15+
arguments: '{"input":"Hello"}'
16+
- role: tool
17+
tool_call_id: toolcall_0
18+
content: HELLO
19+
- role: assistant
20+
content: "The encrypted string is: **HELLO**"
21+
- role: user
22+
content: "Use encrypt_string to encrypt this string: World"
23+
- role: assistant
24+
tool_calls:
25+
- id: toolcall_1
26+
type: function
27+
function:
28+
name: encrypt_string
29+
arguments: '{"input":"World"}'
30+
- role: tool
31+
tool_call_id: toolcall_1
32+
content: WORLD
33+
- role: assistant
34+
content: "The encrypted string is: **WORLD**"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
models:
2+
- claude-sonnet-5
3+
conversations:
4+
- messages:
5+
- role: system
6+
content: ${system}
7+
- role: user
8+
content: "Use encrypt_string to encrypt this string: Hello"
9+
- role: assistant
10+
tool_calls:
11+
- id: toolcall_0
12+
type: function
13+
function:
14+
name: encrypt_string
15+
arguments: '{"input":"Hello"}'
16+
- role: tool
17+
tool_call_id: toolcall_0
18+
content: HELLO
19+
- role: assistant
20+
content: "The encrypted string is: **HELLO**"
21+
- role: user
22+
content: "Use encrypt_string to encrypt this string: World"
23+
- role: assistant
24+
tool_calls:
25+
- id: toolcall_1
26+
type: function
27+
function:
28+
name: encrypt_string
29+
arguments: '{"input":"World"}'
30+
- role: tool
31+
tool_call_id: toolcall_1
32+
content: WORLD
33+
- role: assistant
34+
content: "The encrypted string is: **WORLD**"

0 commit comments

Comments
 (0)