Skip to content

Commit e234c21

Browse files
xtmqbenbrandt
andauthored
feat(unstable): Add providers/* support (#138)
* feat(unstable): Add `providers/list`, `providers/set`, and `providers/disable` support * Cleanups --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
1 parent b0e1d04 commit e234c21

2 files changed

Lines changed: 354 additions & 0 deletions

File tree

src/acp.test.ts

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,15 @@ import {
4343
DidFocusDocumentNotification,
4444
ForkSessionRequest,
4545
ForkSessionResponse,
46+
ListProvidersRequest,
47+
ListProvidersResponse,
4648
ListSessionsRequest,
4749
ListSessionsResponse,
4850
ResumeSessionRequest,
4951
ResumeSessionResponse,
52+
SetProvidersRequest,
53+
DisableProvidersRequest,
54+
DisableProvidersResponse,
5055
CreateElicitationRequest,
5156
CreateElicitationResponse,
5257
CompleteElicitationNotification,
@@ -1698,6 +1703,225 @@ describe("Connection", () => {
16981703
expect(closeResponse).toEqual({});
16991704
});
17001705

1706+
it("handles providers request lifecycle", async () => {
1707+
let receivedSetRequest: SetProvidersRequest | undefined;
1708+
let receivedDisableRequest: DisableProvidersRequest | undefined;
1709+
1710+
class TestClient implements Client {
1711+
async writeTextFile(
1712+
_: WriteTextFileRequest,
1713+
): Promise<WriteTextFileResponse> {
1714+
return {};
1715+
}
1716+
async readTextFile(
1717+
_: ReadTextFileRequest,
1718+
): Promise<ReadTextFileResponse> {
1719+
return { content: "" };
1720+
}
1721+
async requestPermission(
1722+
_: RequestPermissionRequest,
1723+
): Promise<RequestPermissionResponse> {
1724+
return { outcome: { outcome: "selected", optionId: "allow" } };
1725+
}
1726+
async sessionUpdate(_: SessionNotification): Promise<void> {}
1727+
}
1728+
1729+
class TestAgent implements Agent {
1730+
async initialize(_: InitializeRequest): Promise<InitializeResponse> {
1731+
return {
1732+
protocolVersion: 1,
1733+
agentCapabilities: { loadSession: false, providers: {} },
1734+
authMethods: [],
1735+
};
1736+
}
1737+
async newSession(_: NewSessionRequest): Promise<NewSessionResponse> {
1738+
return { sessionId: "test-session" };
1739+
}
1740+
async authenticate(_: AuthenticateRequest): Promise<void> {}
1741+
async prompt(_: PromptRequest): Promise<PromptResponse> {
1742+
return { stopReason: "end_turn" };
1743+
}
1744+
async cancel(_: CancelNotification): Promise<void> {}
1745+
1746+
async unstable_listProviders(
1747+
_: ListProvidersRequest,
1748+
): Promise<ListProvidersResponse> {
1749+
return {
1750+
providers: [
1751+
{
1752+
id: "main",
1753+
supported: ["anthropic", "openai"],
1754+
required: true,
1755+
current: {
1756+
apiType: "anthropic",
1757+
baseUrl: "https://api.anthropic.com",
1758+
},
1759+
},
1760+
{
1761+
id: "openai",
1762+
supported: ["openai"],
1763+
required: false,
1764+
},
1765+
{
1766+
id: "azure",
1767+
supported: ["azure"],
1768+
required: false,
1769+
current: null,
1770+
},
1771+
],
1772+
};
1773+
}
1774+
1775+
async unstable_setProvider(params: SetProvidersRequest): Promise<void> {
1776+
receivedSetRequest = params;
1777+
}
1778+
1779+
async unstable_disableProvider(
1780+
params: DisableProvidersRequest,
1781+
): Promise<DisableProvidersResponse> {
1782+
receivedDisableRequest = params;
1783+
return {};
1784+
}
1785+
}
1786+
1787+
const agentConnection = new ClientSideConnection(
1788+
() => new TestClient(),
1789+
ndJsonStream(clientToAgent.writable, agentToClient.readable),
1790+
);
1791+
const clientConnection = new AgentSideConnection(
1792+
() => new TestAgent(),
1793+
ndJsonStream(agentToClient.writable, clientToAgent.readable),
1794+
);
1795+
1796+
void clientConnection;
1797+
1798+
const listResponse = await agentConnection.unstable_listProviders({});
1799+
expect(listResponse.providers).toEqual([
1800+
{
1801+
id: "main",
1802+
supported: ["anthropic", "openai"],
1803+
required: true,
1804+
current: {
1805+
apiType: "anthropic",
1806+
baseUrl: "https://api.anthropic.com",
1807+
},
1808+
},
1809+
{
1810+
id: "openai",
1811+
supported: ["openai"],
1812+
required: false,
1813+
},
1814+
{
1815+
id: "azure",
1816+
supported: ["azure"],
1817+
required: false,
1818+
current: null,
1819+
},
1820+
]);
1821+
expect("current" in listResponse.providers[1]).toBe(false);
1822+
1823+
const setResponse = await agentConnection.unstable_setProvider({
1824+
id: "main",
1825+
apiType: "openai",
1826+
baseUrl: "https://llm-gateway.corp.example.com/openai/v1",
1827+
headers: {
1828+
Authorization: "Bearer token",
1829+
"X-Request-Source": "test-client",
1830+
},
1831+
});
1832+
expect(setResponse).toEqual({});
1833+
expect(receivedSetRequest).toEqual({
1834+
id: "main",
1835+
apiType: "openai",
1836+
baseUrl: "https://llm-gateway.corp.example.com/openai/v1",
1837+
headers: {
1838+
Authorization: "Bearer token",
1839+
"X-Request-Source": "test-client",
1840+
},
1841+
});
1842+
1843+
const disableResponse = await agentConnection.unstable_disableProvider({
1844+
id: "openai",
1845+
});
1846+
expect(disableResponse).toEqual({});
1847+
expect(receivedDisableRequest).toEqual({ id: "openai" });
1848+
});
1849+
1850+
it("rejects providers requests when agent does not implement handlers", async () => {
1851+
class TestClient implements Client {
1852+
async writeTextFile(
1853+
_: WriteTextFileRequest,
1854+
): Promise<WriteTextFileResponse> {
1855+
return {};
1856+
}
1857+
async readTextFile(
1858+
_: ReadTextFileRequest,
1859+
): Promise<ReadTextFileResponse> {
1860+
return { content: "" };
1861+
}
1862+
async requestPermission(
1863+
_: RequestPermissionRequest,
1864+
): Promise<RequestPermissionResponse> {
1865+
return { outcome: { outcome: "selected", optionId: "allow" } };
1866+
}
1867+
async sessionUpdate(_: SessionNotification): Promise<void> {}
1868+
}
1869+
1870+
class TestAgent implements Agent {
1871+
async initialize(_: InitializeRequest): Promise<InitializeResponse> {
1872+
return {
1873+
protocolVersion: 1,
1874+
agentCapabilities: { loadSession: false },
1875+
authMethods: [],
1876+
};
1877+
}
1878+
async newSession(_: NewSessionRequest): Promise<NewSessionResponse> {
1879+
return { sessionId: "test-session" };
1880+
}
1881+
async authenticate(_: AuthenticateRequest): Promise<void> {}
1882+
async prompt(_: PromptRequest): Promise<PromptResponse> {
1883+
return { stopReason: "end_turn" };
1884+
}
1885+
async cancel(_: CancelNotification): Promise<void> {}
1886+
}
1887+
1888+
const agentConnection = new ClientSideConnection(
1889+
() => new TestClient(),
1890+
ndJsonStream(clientToAgent.writable, agentToClient.readable),
1891+
);
1892+
const clientConnection = new AgentSideConnection(
1893+
() => new TestAgent(),
1894+
ndJsonStream(agentToClient.writable, clientToAgent.readable),
1895+
);
1896+
1897+
void clientConnection;
1898+
1899+
await expect(
1900+
agentConnection.unstable_listProviders({}),
1901+
).rejects.toMatchObject({
1902+
code: -32601,
1903+
data: { method: "providers/list" },
1904+
});
1905+
1906+
await expect(
1907+
agentConnection.unstable_setProvider({
1908+
id: "main",
1909+
apiType: "openai",
1910+
baseUrl: "https://api.openai.com/v1",
1911+
}),
1912+
).rejects.toMatchObject({
1913+
code: -32601,
1914+
data: { method: "providers/set" },
1915+
});
1916+
1917+
await expect(
1918+
agentConnection.unstable_disableProvider({ id: "main" }),
1919+
).rejects.toMatchObject({
1920+
code: -32601,
1921+
data: { method: "providers/disable" },
1922+
});
1923+
});
1924+
17011925
it("handles NES notifications", async () => {
17021926
const notificationLog: unknown[] = [];
17031927

src/acp.ts

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,30 @@ export class AgentSideConnection {
110110
const result = await agent.authenticate(validatedParams);
111111
return result ?? {};
112112
}
113+
case schema.AGENT_METHODS.providers_list: {
114+
if (!agent.unstable_listProviders) {
115+
throw RequestError.methodNotFound(method);
116+
}
117+
const validatedParams = validate.zListProvidersRequest.parse(params);
118+
return agent.unstable_listProviders(validatedParams);
119+
}
120+
case schema.AGENT_METHODS.providers_set: {
121+
if (!agent.unstable_setProvider) {
122+
throw RequestError.methodNotFound(method);
123+
}
124+
const validatedParams = validate.zSetProvidersRequest.parse(params);
125+
const result = await agent.unstable_setProvider(validatedParams);
126+
return result ?? {};
127+
}
128+
case schema.AGENT_METHODS.providers_disable: {
129+
if (!agent.unstable_disableProvider) {
130+
throw RequestError.methodNotFound(method);
131+
}
132+
const validatedParams =
133+
validate.zDisableProvidersRequest.parse(params);
134+
const result = await agent.unstable_disableProvider(validatedParams);
135+
return result ?? {};
136+
}
113137
case schema.AGENT_METHODS.logout: {
114138
if (!agent.unstable_logout) {
115139
throw RequestError.methodNotFound(method);
@@ -911,6 +935,70 @@ export class ClientSideConnection implements Agent {
911935
);
912936
}
913937

938+
/**
939+
* **UNSTABLE**
940+
*
941+
* This capability is not part of the spec yet, and may be removed or changed at any point.
942+
*
943+
* Lists providers that can be configured by the client.
944+
*
945+
* This method is only available if the agent advertises the `providers` capability.
946+
*
947+
* @experimental
948+
*/
949+
async unstable_listProviders(
950+
params: schema.ListProvidersRequest,
951+
): Promise<schema.ListProvidersResponse> {
952+
return await this.connection.sendRequest(
953+
schema.AGENT_METHODS.providers_list,
954+
params,
955+
);
956+
}
957+
958+
/**
959+
* **UNSTABLE**
960+
*
961+
* This capability is not part of the spec yet, and may be removed or changed at any point.
962+
*
963+
* Replaces the configuration for a provider.
964+
*
965+
* This method is only available if the agent advertises the `providers` capability.
966+
*
967+
* @experimental
968+
*/
969+
async unstable_setProvider(
970+
params: schema.SetProvidersRequest,
971+
): Promise<schema.SetProvidersResponse> {
972+
return (
973+
(await this.connection.sendRequest(
974+
schema.AGENT_METHODS.providers_set,
975+
params,
976+
)) ?? {}
977+
);
978+
}
979+
980+
/**
981+
* **UNSTABLE**
982+
*
983+
* This capability is not part of the spec yet, and may be removed or changed at any point.
984+
*
985+
* Disables a provider.
986+
*
987+
* This method is only available if the agent advertises the `providers` capability.
988+
*
989+
* @experimental
990+
*/
991+
async unstable_disableProvider(
992+
params: schema.DisableProvidersRequest,
993+
): Promise<schema.DisableProvidersResponse> {
994+
return (
995+
(await this.connection.sendRequest(
996+
schema.AGENT_METHODS.providers_disable,
997+
params,
998+
)) ?? {}
999+
);
1000+
}
1001+
9141002
/**
9151003
* Terminates the current authenticated session.
9161004
*
@@ -1976,6 +2064,48 @@ export interface Agent {
19762064
authenticate(
19772065
params: schema.AuthenticateRequest,
19782066
): Promise<schema.AuthenticateResponse | void>;
2067+
/**
2068+
* **UNSTABLE**
2069+
*
2070+
* This capability is not part of the spec yet, and may be removed or changed at any point.
2071+
*
2072+
* Lists providers that can be configured by the client.
2073+
*
2074+
* This method is only available if the agent advertises the `providers` capability.
2075+
*
2076+
* @experimental
2077+
*/
2078+
unstable_listProviders?(
2079+
params: schema.ListProvidersRequest,
2080+
): Promise<schema.ListProvidersResponse>;
2081+
/**
2082+
* **UNSTABLE**
2083+
*
2084+
* This capability is not part of the spec yet, and may be removed or changed at any point.
2085+
*
2086+
* Replaces the configuration for a provider.
2087+
*
2088+
* This method is only available if the agent advertises the `providers` capability.
2089+
*
2090+
* @experimental
2091+
*/
2092+
unstable_setProvider?(
2093+
params: schema.SetProvidersRequest,
2094+
): Promise<schema.SetProvidersResponse | void>;
2095+
/**
2096+
* **UNSTABLE**
2097+
*
2098+
* This capability is not part of the spec yet, and may be removed or changed at any point.
2099+
*
2100+
* Disables a provider.
2101+
*
2102+
* This method is only available if the agent advertises the `providers` capability.
2103+
*
2104+
* @experimental
2105+
*/
2106+
unstable_disableProvider?(
2107+
params: schema.DisableProvidersRequest,
2108+
): Promise<schema.DisableProvidersResponse | void>;
19792109
/**
19802110
* Terminates the current authenticated session.
19812111
*

0 commit comments

Comments
 (0)