Skip to content

Commit b7fccbc

Browse files
Add SDK support for agent selection and session compact APIs
Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent bce4de0 commit b7fccbc

File tree

5 files changed

+412
-306
lines changed

5 files changed

+412
-306
lines changed

nodejs/src/generated/rpc.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,122 @@ export interface SessionFleetStartParams {
314314
prompt?: string;
315315
}
316316

317+
export interface SessionAgentListResult {
318+
/**
319+
* Available custom agents
320+
*/
321+
agents: {
322+
/**
323+
* Unique identifier of the custom agent
324+
*/
325+
name: string;
326+
/**
327+
* Human-readable display name
328+
*/
329+
displayName: string;
330+
/**
331+
* Description of the agent's purpose
332+
*/
333+
description: string;
334+
}[];
335+
}
336+
337+
export interface SessionAgentListParams {
338+
/**
339+
* Target session identifier
340+
*/
341+
sessionId: string;
342+
}
343+
344+
export interface SessionAgentGetCurrentResult {
345+
/**
346+
* Currently selected custom agent, or null if using the default agent
347+
*/
348+
agent: {
349+
/**
350+
* Unique identifier of the custom agent
351+
*/
352+
name: string;
353+
/**
354+
* Human-readable display name
355+
*/
356+
displayName: string;
357+
/**
358+
* Description of the agent's purpose
359+
*/
360+
description: string;
361+
} | null;
362+
}
363+
364+
export interface SessionAgentGetCurrentParams {
365+
/**
366+
* Target session identifier
367+
*/
368+
sessionId: string;
369+
}
370+
371+
export interface SessionAgentSelectResult {
372+
/**
373+
* The newly selected custom agent
374+
*/
375+
agent: {
376+
/**
377+
* Unique identifier of the custom agent
378+
*/
379+
name: string;
380+
/**
381+
* Human-readable display name
382+
*/
383+
displayName: string;
384+
/**
385+
* Description of the agent's purpose
386+
*/
387+
description: string;
388+
};
389+
}
390+
391+
export interface SessionAgentSelectParams {
392+
/**
393+
* Target session identifier
394+
*/
395+
sessionId: string;
396+
/**
397+
* Name of the custom agent to select
398+
*/
399+
name: string;
400+
}
401+
402+
export interface SessionAgentDeselectResult {}
403+
404+
export interface SessionAgentDeselectParams {
405+
/**
406+
* Target session identifier
407+
*/
408+
sessionId: string;
409+
}
410+
411+
export interface SessionCompactionCompactResult {
412+
/**
413+
* Whether compaction completed successfully
414+
*/
415+
success: boolean;
416+
/**
417+
* Number of tokens freed by compaction
418+
*/
419+
tokensRemoved: number;
420+
/**
421+
* Number of messages removed during compaction
422+
*/
423+
messagesRemoved: number;
424+
}
425+
426+
export interface SessionCompactionCompactParams {
427+
/**
428+
* Target session identifier
429+
*/
430+
sessionId: string;
431+
}
432+
317433
/** Create typed server-scoped RPC methods (no session required). */
318434
export function createServerRpc(connection: MessageConnection) {
319435
return {
@@ -369,5 +485,19 @@ export function createSessionRpc(connection: MessageConnection, sessionId: strin
369485
start: async (params: Omit<SessionFleetStartParams, "sessionId">): Promise<SessionFleetStartResult> =>
370486
connection.sendRequest("session.fleet.start", { sessionId, ...params }),
371487
},
488+
agent: {
489+
list: async (): Promise<SessionAgentListResult> =>
490+
connection.sendRequest("session.agent.list", { sessionId }),
491+
getCurrent: async (): Promise<SessionAgentGetCurrentResult> =>
492+
connection.sendRequest("session.agent.getCurrent", { sessionId }),
493+
select: async (params: Omit<SessionAgentSelectParams, "sessionId">): Promise<SessionAgentSelectResult> =>
494+
connection.sendRequest("session.agent.select", { sessionId, ...params }),
495+
deselect: async (): Promise<SessionAgentDeselectResult> =>
496+
connection.sendRequest("session.agent.deselect", { sessionId }),
497+
},
498+
compaction: {
499+
compact: async (): Promise<SessionCompactionCompactResult> =>
500+
connection.sendRequest("session.compaction.compact", { sessionId }),
501+
},
372502
};
373503
}
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
import { describe, expect, it } from "vitest";
6+
import type { CustomAgentConfig } from "../../src/index.js";
7+
import { createSdkTestContext } from "./harness/sdkTestContext.js";
8+
9+
describe("Agent Selection RPC", async () => {
10+
const { copilotClient: client } = await createSdkTestContext();
11+
12+
it("should list available custom agents", async () => {
13+
const customAgents: CustomAgentConfig[] = [
14+
{
15+
name: "test-agent",
16+
displayName: "Test Agent",
17+
description: "A test agent",
18+
prompt: "You are a test agent.",
19+
},
20+
{
21+
name: "another-agent",
22+
displayName: "Another Agent",
23+
description: "Another test agent",
24+
prompt: "You are another agent.",
25+
},
26+
];
27+
28+
const session = await client.createSession({ customAgents });
29+
30+
const result = await session.rpc.agent.list();
31+
expect(result.agents).toBeDefined();
32+
expect(Array.isArray(result.agents)).toBe(true);
33+
expect(result.agents.length).toBe(2);
34+
expect(result.agents[0].name).toBe("test-agent");
35+
expect(result.agents[0].displayName).toBe("Test Agent");
36+
expect(result.agents[0].description).toBe("A test agent");
37+
expect(result.agents[1].name).toBe("another-agent");
38+
39+
await session.destroy();
40+
});
41+
42+
it("should return null when no agent is selected", async () => {
43+
const customAgents: CustomAgentConfig[] = [
44+
{
45+
name: "test-agent",
46+
displayName: "Test Agent",
47+
description: "A test agent",
48+
prompt: "You are a test agent.",
49+
},
50+
];
51+
52+
const session = await client.createSession({ customAgents });
53+
54+
const result = await session.rpc.agent.getCurrent();
55+
expect(result.agent).toBeNull();
56+
57+
await session.destroy();
58+
});
59+
60+
it("should select and get current agent", async () => {
61+
const customAgents: CustomAgentConfig[] = [
62+
{
63+
name: "test-agent",
64+
displayName: "Test Agent",
65+
description: "A test agent",
66+
prompt: "You are a test agent.",
67+
},
68+
];
69+
70+
const session = await client.createSession({ customAgents });
71+
72+
// Select the agent
73+
const selectResult = await session.rpc.agent.select({ name: "test-agent" });
74+
expect(selectResult.agent).toBeDefined();
75+
expect(selectResult.agent.name).toBe("test-agent");
76+
expect(selectResult.agent.displayName).toBe("Test Agent");
77+
78+
// Verify getCurrent returns the selected agent
79+
const currentResult = await session.rpc.agent.getCurrent();
80+
expect(currentResult.agent).not.toBeNull();
81+
expect(currentResult.agent!.name).toBe("test-agent");
82+
83+
await session.destroy();
84+
});
85+
86+
it("should deselect current agent", async () => {
87+
const customAgents: CustomAgentConfig[] = [
88+
{
89+
name: "test-agent",
90+
displayName: "Test Agent",
91+
description: "A test agent",
92+
prompt: "You are a test agent.",
93+
},
94+
];
95+
96+
const session = await client.createSession({ customAgents });
97+
98+
// Select then deselect
99+
await session.rpc.agent.select({ name: "test-agent" });
100+
await session.rpc.agent.deselect();
101+
102+
// Verify no agent is selected
103+
const currentResult = await session.rpc.agent.getCurrent();
104+
expect(currentResult.agent).toBeNull();
105+
106+
await session.destroy();
107+
});
108+
109+
it("should return empty list when no custom agents configured", async () => {
110+
const session = await client.createSession();
111+
112+
const result = await session.rpc.agent.list();
113+
expect(result.agents).toEqual([]);
114+
115+
await session.destroy();
116+
});
117+
});
118+
119+
describe("Session Compact RPC", async () => {
120+
const { copilotClient: client } = await createSdkTestContext();
121+
122+
it("should compact session history after messages", async () => {
123+
const session = await client.createSession();
124+
125+
// Send a message to create some history
126+
await session.sendAndWait({ prompt: "What is 2+2?" });
127+
128+
// Compact the session
129+
const result = await session.rpc.compaction.compact();
130+
expect(typeof result.success).toBe("boolean");
131+
expect(typeof result.tokensRemoved).toBe("number");
132+
expect(typeof result.messagesRemoved).toBe("number");
133+
134+
await session.destroy();
135+
}, 60000);
136+
});

test/harness/replayingCapiProxy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -632,6 +632,7 @@ function normalizeUserMessage(content: string): string {
632632
return content
633633
.replace(/<current_datetime>.*?<\/current_datetime>/g, "")
634634
.replace(/<reminder>[\s\S]*?<\/reminder>/g, "")
635+
.replace(/Please create a detailed summary of the conversation so far\. The history is being compacted[\s\S]*/, "${compaction_prompt}")
635636
.trim();
636637
}
637638

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
models:
2+
- claude-sonnet-4.5
3+
conversations:
4+
- messages:
5+
- role: system
6+
content: ${system}
7+
- role: user
8+
content: What is 2+2?
9+
- role: assistant
10+
content: 2+2 = 4
11+
- role: user
12+
content: ${compaction_prompt}
13+
- role: assistant
14+
content: >-
15+
<overview>
16+
17+
The user asked a simple arithmetic question ("What is 2+2?") which was answered directly (4). No technical
18+
work, code changes, or file modifications were requested or performed. This appears to be a minimal test or
19+
verification exchange before any substantive work begins.
20+
21+
</overview>
22+
23+
24+
<history>
25+
26+
1. The user asked "What is 2+2?"
27+
- Provided the answer: 4
28+
- No follow-up actions were needed
29+
</history>
30+
31+
32+
<work_done>
33+
34+
No work has been performed. No files were created, modified, or deleted. This was purely an informational
35+
exchange with no code changes or system modifications.
36+
37+
38+
Current state:
39+
40+
- Working directory: ${workdir}
41+
42+
- Not a git repository
43+
44+
- No files have been accessed or modified
45+
46+
- No todos or plan have been created
47+
48+
</work_done>
49+
50+
51+
<technical_details>
52+
53+
- Environment is Windows (Windows_NT), requiring backslash path separators
54+
55+
- Session workspace available for artifacts (e.g., plan.md)
56+
57+
- SQL database available but not yet initialized (no tables created)
58+
59+
- Available tools: git, curl, gh
60+
61+
- No technical work has been performed yet, so no technical details have been discovered
62+
63+
</technical_details>
64+
65+
66+
<important_files>
67+
68+
No files have been accessed or modified during this conversation.
69+
70+
</important_files>
71+
72+
73+
<next_steps>
74+
75+
No work is currently in progress. Awaiting user's next request for any substantive task.
76+
77+
</next_steps>
78+
79+
80+
<checkpoint_title>Simple arithmetic question answered</checkpoint_title>

0 commit comments

Comments
 (0)